基于tensorflow+CNN的垃圾郵件文本分類

2018年10月12日筆記

tensorflow是谷歌google的深度學(xué)習(xí)框架如绸,tensor中文叫做張量宦言,flow叫做流门驾。
CNN是convolutional neural network的簡(jiǎn)稱娃豹,中文叫做卷積神經(jīng)網(wǎng)絡(luò)憔涉。
文本分類是NLP(自然語(yǔ)言處理)的經(jīng)典任務(wù)。

0.編程環(huán)境

操作系統(tǒng):Win10
python版本:3.6
集成開(kāi)發(fā)環(huán)境:jupyter notebook
tensorflow版本:1.6

1.致謝聲明

本文是作者學(xué)習(xí)《使用卷積神經(jīng)網(wǎng)絡(luò)以及循環(huán)神經(jīng)網(wǎng)絡(luò)進(jìn)行中文文本分類》的成果箭跳,感激前輩晨另;
github鏈接:https://github.com/gaussic/text-classification-cnn-rnn

2.配置環(huán)境

使用卷積神經(jīng)網(wǎng)絡(luò)模型要求有較高的機(jī)器配置,如果使用CPU版tensorflow會(huì)花費(fèi)大量時(shí)間谱姓。
讀者在有nvidia顯卡的情況下借尿,安裝GPU版tensorflow會(huì)提高計(jì)算速度50倍。
安裝教程鏈接:https://blog.csdn.net/qq_36556893/article/details/79433298
如果沒(méi)有nvidia顯卡屉来,但有visa信用卡路翻,請(qǐng)閱讀我的另一篇文章《在谷歌云服務(wù)器上搭建深度學(xué)習(xí)平臺(tái)》,鏈接:http://www.reibang.com/p/893d622d1b5a

3.下載并解壓數(shù)據(jù)集

數(shù)據(jù)集下載鏈接: https://pan.baidu.com/s/10QtokJ8_tkK6I3GifalxWg 提取碼: uytb
壓縮文件CNN垃圾郵件分類中有3個(gè)文件茄靠,如下圖所示:

image.png

1.cnn_package.ipynb是本文的代碼文件茂契,讀者可以直接運(yùn)行
2.mailContent_list.pickle文件是本文作者處理好的郵件內(nèi)容文件,可以用pickle.load方法加載慨绳;
3.mailLabel_list.pickle文件是本文作者處理好的郵件標(biāo)簽文件账嚎,可以用pickle.load方法加載莫瞬。
數(shù)據(jù)集中共有2種分類:垃圾郵件用spam表示,正常郵件用ham表示郭蕉。
垃圾郵件樣本40000多條,正常郵件樣本20000多條喂江,樣本總共60000多條召锈。

4.完整代碼

完整代碼已經(jīng)在數(shù)據(jù)集文件中給出,即cnn_package.ipynb文件获询;
從工程開(kāi)發(fā)的角度考慮涨岁,本文作者封裝了一個(gè)類TextClassification,對(duì)于樣本數(shù)量在10萬(wàn)左右的分類任務(wù)較為適用吉嚣。
后面章節(jié)中將講解實(shí)現(xiàn)細(xì)節(jié)梢薪。

5.編寫類TextClassification

認(rèn)為編寫類有困難,可以先閱讀本文作者的另外一篇文章《基于tensorflow+CNN的新浪新聞文本分類》尝哆,鏈接:http://www.reibang.com/p/b1000d5345bb秉撇,這一篇文章的代碼沒(méi)有進(jìn)行封裝,而且講解較為詳細(xì)秋泄。
讀者閱讀下文中的行數(shù)時(shí)琐馆,可以先把代碼復(fù)制到j(luò)upyter notebook的代碼塊中。
在代碼塊中按Esc鍵恒序,進(jìn)入命令模式瘦麸,代碼塊左邊的豎線會(huì)顯示藍(lán)色,如下圖所示:

image.png

在命令模式下歧胁,點(diǎn)擊L鍵滋饲,會(huì)顯示代碼行數(shù)。
推薦博客《Text-CNN 文本分類》從模型原理上輔助理解喊巍,鏈接:https://blog.csdn.net/chuchus/article/details/77847476
本文作者解釋每行代碼含義如下:
第1-15行代碼導(dǎo)入程序運(yùn)行必需的庫(kù)屠缭;
第16-27行代碼定義文本設(shè)置類TextConfig,經(jīng)過(guò)本文作者實(shí)踐玄糟,這是1種較為合適的編碼方式勿她;
第30-42行代碼定義類中方法config,實(shí)例化TextConfig對(duì)象后阵翎,將其中的參數(shù)賦值給模型對(duì)象逢并;
第44-62行代碼定義對(duì)象實(shí)例化方法,如果參數(shù)數(shù)量是2個(gè)郭卫,其中第1個(gè)參數(shù)是內(nèi)容列表content_list砍聊,第2個(gè)參數(shù)是標(biāo)簽列表label_list,則調(diào)用sklearn.preprocessing庫(kù)的train_test_split方法劃分訓(xùn)練集贰军、測(cè)試集玻蝌;
如果參數(shù)數(shù)量是4個(gè)蟹肘,其中第1個(gè)參數(shù)是訓(xùn)練集內(nèi)容列表train_content_list,第2個(gè)參數(shù)是訓(xùn)練集標(biāo)簽列表train_label_list俯树,第3個(gè)參數(shù)是測(cè)試集內(nèi)容列表test_content_list帘腹,第4個(gè)參數(shù)是測(cè)試集標(biāo)簽列表test_label_list,則都將它們保存為對(duì)象的屬性许饿;
第63行表示調(diào)用第65-67行代碼的類中方法autoGetNumClasses阳欲,即自動(dòng)獲取類別的數(shù)量,并賦值給對(duì)象的屬性num_classes陋率;
第69-73行代碼定義類中方法getVocabularyList球化,將列表中的字符串合并為1個(gè)字符串a(chǎn)llContent_str,調(diào)用collections庫(kù)的Counter方法瓦糟,把a(bǔ)llContent_str作為參數(shù)筒愚,即對(duì)字符串中的字符做統(tǒng)計(jì)計(jì)數(shù),最后返回出現(xiàn)次數(shù)排名前vocabulary_size的數(shù)菩浙,即前5000的數(shù)巢掺;
第75-84行代碼定義類中方法prepareData,即準(zhǔn)備數(shù)據(jù)芍耘,根據(jù)數(shù)據(jù)的實(shí)際情況調(diào)整模型屬性:詞匯表大小vocab_size址遇、序列長(zhǎng)度seq_length、字轉(zhuǎn)id字典word2id_dict斋竞、標(biāo)簽編碼對(duì)象labelEncoder倔约;
第86-87行代碼定義類中方法content2idList,即文本內(nèi)容轉(zhuǎn)換為id列表坝初;
第89-92行代碼定義類中方法content2X浸剩,將文本內(nèi)容列表content_list轉(zhuǎn)換為特征矩陣X;
第94-97行代碼定義類中方法label2Y鳄袍,將文本標(biāo)簽列表label_list轉(zhuǎn)換為預(yù)測(cè)目標(biāo)值Y绢要,具體方法是先調(diào)用LabelEncoder對(duì)象的transform方法做標(biāo)簽編碼,然后調(diào)用kr.utils.to_categorical方法做Ont-Hot編碼拗小;
第99-118行代碼定義類中方法buildModel重罪,即搭建卷積神經(jīng)網(wǎng)絡(luò)模型,再次提醒哀九,理解此部分代碼有困難剿配,可以先理解本文作者沒(méi)有封裝的代碼,鏈接:http://www.reibang.com/p/b1000d5345bb阅束;
第120-145行代碼定義類中方法trainModel呼胚,即訓(xùn)練模型;
模型總共迭代訓(xùn)練num_iteration次息裸,即5000次蝇更,每隔250次打印步數(shù)step沪编、模型在測(cè)試集的損失值loss_value和準(zhǔn)確率accuracy_value
第147-153行代碼定義類中方法predict,傳入?yún)?shù)是文本內(nèi)容content_list年扩,方法返回結(jié)果是標(biāo)簽列表label_list蚁廓;
第155-162行代碼定義類中方法predictAll,此方法作用是避免預(yù)測(cè)的樣本過(guò)多導(dǎo)致顯存不足常遂,所以分批預(yù)測(cè)纳令,每批100個(gè)樣本,使用列表的extend方法將每批的預(yù)測(cè)結(jié)果合并克胳;
第164-170行代碼定義類中方法printConfusionMatrix,即打印模型在測(cè)試集的預(yù)測(cè)混淆矩陣圈匆;
第172-204行代碼定義類中方法printReportTable漠另,即打印模型在測(cè)試集的預(yù)測(cè)報(bào)告表。

from sklearn.model_selection import train_test_split
import pickle
from collections import Counter
import tensorflow.contrib.keras as kr
from sklearn.preprocessing import LabelEncoder
import tensorflow as tf
import random
import numpy as np
import pandas as pd
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_recall_fscore_support
import warnings
warnings.filterwarnings('ignore')
import time

class TextConfig():
    vocab_size = 5000
    seq_length = 600
    embedding_dim = 64  # 詞向量維度
    num_filters = 256  # 卷積核數(shù)目
    kernel_size = 5  # 卷積核尺
    hidden_dim = 128  # 全連接層神經(jīng)元
    dropout_keep_prob = 0.5  # dropout保留比例
    learning_rate = 1e-3  # 學(xué)習(xí)率
    batch_size = 32  # 每批訓(xùn)練大小
    num_iteration = 5000 #迭代次數(shù)
    print_per_batch = num_iteration / 20 #打印間隔

class TextClassification():
    def config(self):
        textConfig = TextConfig()
        self.vocab_size = textConfig.vocab_size
        self.seq_length = textConfig.seq_length
        self.embedding_dim = textConfig.embedding_dim
        self.num_filters = textConfig.num_filters
        self.kernel_size = textConfig.kernel_size
        self.hidden_dim = textConfig.hidden_dim
        self.dropout_keep_prob = textConfig.dropout_keep_prob
        self.learning_rate = textConfig.learning_rate
        self.batch_size = textConfig.batch_size
        self.print_per_batch = textConfig.print_per_batch
        self.num_iteration = textConfig.num_iteration
    
    def __init__(self, *args):
        self.config()
        if len(args) == 2:
            content_list = args[0]
            label_list = args[1]
            train_X, test_X, train_y, test_y = train_test_split(content_list, label_list)
            self.train_content_list = train_X
            self.train_label_list = train_y
            self.test_content_list = test_X
            self.test_label_list = test_y
            self.content_list = self.train_content_list + self.test_content_list
        elif len(args) == 4:
            self.train_content_list = args[0]
            self.train_label_list = args[1]
            self.test_content_list = args[2]
            self.test_label_list = args[3]
            self.content_list = self.train_content_list + self.test_content_list
        else:
            print('false to init TextClassification object')
        self.autoGetNumClasses()
    
    def autoGetNumClasses(self):
        label_list = self.train_label_list + self.test_label_list
        self.num_classes = np.unique(label_list).shape[0]
    
    def getVocabularyList(self, content_list, vocabulary_size):
        allContent_str = ''.join(content_list)
        counter = Counter(allContent_str)
        vocabulary_list = [k[0] for k in counter.most_common(vocabulary_size)]
        return ['PAD'] + vocabulary_list

    def prepareData(self):
        vocabulary_list = self.getVocabularyList(self.content_list, self.vocab_size)
        if len(vocabulary_list) < self.vocab_size:
            self.vocab_size = len(vocabulary_list)
        contentLength_list = [len(k) for k in self.train_content_list]
        if max(contentLength_list) < self.seq_length:
            self.seq_length = max(contentLength_list)
        self.word2id_dict = dict([(b, a) for a, b in enumerate(vocabulary_list)])
        self.labelEncoder = LabelEncoder()
        self.labelEncoder.fit(self.train_label_list)

    def content2idList(self, content):
        return [self.word2id_dict[word] for word in content if word in self.word2id_dict]

    def content2X(self, content_list):
        idlist_list = [self.content2idList(content) for content in content_list]
        X = kr.preprocessing.sequence.pad_sequences(idlist_list, self.seq_length)
        return X

    def label2Y(self, label_list):
        y = self.labelEncoder.transform(label_list)
        Y = kr.utils.to_categorical(y, self.num_classes)
        return Y

    def buildModel(self):
        tf.reset_default_graph()
        self.X_holder = tf.placeholder(tf.int32, [None, self.seq_length])
        self.Y_holder = tf.placeholder(tf.float32, [None, self.num_classes])
        embedding = tf.get_variable('embedding', [self.vocab_size, self.embedding_dim])
        embedding_inputs = tf.nn.embedding_lookup(embedding, self.X_holder)
        conv = tf.layers.conv1d(embedding_inputs, self.num_filters, self.kernel_size)
        max_pooling = tf.reduce_max(conv, reduction_indices=[1])
        full_connect = tf.layers.dense(max_pooling, self.hidden_dim)
        full_connect_dropout = tf.contrib.layers.dropout(full_connect, keep_prob=self.dropout_keep_prob)
        full_connect_activate = tf.nn.relu(full_connect_dropout)
        softmax_before = tf.layers.dense(full_connect_activate, self.num_classes)
        self.predict_Y = tf.nn.softmax(softmax_before)
        cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(labels=self.Y_holder, logits=softmax_before)
        self.loss = tf.reduce_mean(cross_entropy)
        optimizer = tf.train.AdamOptimizer(self.learning_rate)
        self.train = optimizer.minimize(self.loss)
        self.predict_y = tf.argmax(self.predict_Y, 1)
        isCorrect = tf.equal(tf.argmax(self.Y_holder, 1), self.predict_y)
        self.accuracy = tf.reduce_mean(tf.cast(isCorrect, tf.float32))

    def trainModel(self):
        self.prepareData()
        self.buildModel()
        init = tf.global_variables_initializer()
        self.session = tf.Session()
        self.session.run(init)
        train_X = self.content2X(self.train_content_list)
        train_Y = self.label2Y(self.train_label_list)
        test_X = self.content2X(self.test_content_list)
        test_Y = self.label2Y(self.test_label_list)
        startTime = time.time()
        for i in range(self.num_iteration):
            selected_index = random.sample(list(range(len(train_Y))), k=self.batch_size)
            batch_X = train_X[selected_index]
            batch_Y = train_Y[selected_index]
            self.session.run(self.train, {self.X_holder: batch_X, self.Y_holder: batch_Y})
            step = i + 1
            if step % self.print_per_batch == 0 or step == 1:
                selected_index = random.sample(list(range(len(test_Y))), k=200)
                batch_X = test_X[selected_index]
                batch_Y = test_Y[selected_index]
                loss_value, accuracy_value = self.session.run([self.loss, self.accuracy],\
                    {self.X_holder: batch_X, self.Y_holder: batch_Y})
                used_time = time.time() - startTime
                print('step:%d loss:%.4f accuracy:%.4f used time:%.2f seconds' %
                      (step, loss_value, accuracy_value, used_time))

    def predict(self, content_list):
        if type(content_list) == str:
            content_list = [content_list]
        batch_X = self.content2X(content_list)
        predict_y = self.session.run(self.predict_y, {self.X_holder:batch_X})
        predict_label_list = self.labelEncoder.inverse_transform(predict_y)
        return predict_label_list

    def predictAll(self):
        predict_label_list = []
        batch_size = 100
        for i in range(0, len(self.test_content_list), batch_size):
            content_list = self.test_content_list[i: i + batch_size]
            predict_label = self.predict(content_list)
            predict_label_list.extend(predict_label)
        return predict_label_list

    def printConfusionMatrix(self):
        predict_label_list = self.predictAll()
        df = pd.DataFrame(confusion_matrix(self.test_label_list, predict_label_list),
                     columns=self.labelEncoder.classes_,
                     index=self.labelEncoder.classes_)
        print('\n Confusion Matrix:')
        print(df)

    def printReportTable(self):
        predict_label_list = self.predictAll()
        reportTable = self.eval_model(self.test_label_list,
                                 predict_label_list,
                                 self.labelEncoder.classes_)
        print('\n Report Table:')
        print(reportTable)
        
    def eval_model(self, y_true, y_pred, labels):
        # 計(jì)算每個(gè)分類的Precision, Recall, f1, support
        p, r, f1, s = precision_recall_fscore_support(y_true, y_pred)
        # 計(jì)算總體的平均Precision, Recall, f1, support
        tot_p = np.average(p, weights=s)
        tot_r = np.average(r, weights=s)
        tot_f1 = np.average(f1, weights=s)
        tot_s = np.sum(s)
        res1 = pd.DataFrame({
            u'Label': labels,
            u'Precision': p,
            u'Recall': r,
            u'F1': f1,
            u'Support': s
        })
        res2 = pd.DataFrame({
            u'Label': ['總體'],
            u'Precision': [tot_p],
            u'Recall': [tot_r],
            u'F1': [tot_f1],
            u'Support': [tot_s]
        })
        res2.index = [999]
        res = pd.concat([res1, res2])
        return res[['Label', 'Precision', 'Recall', 'F1', 'Support']]

6.調(diào)用類中方法

第1-6行代碼調(diào)用pickle庫(kù)的load方法讀取pickle文件中的數(shù)據(jù)跃赚;
第7行代碼實(shí)例化TextClassification對(duì)象笆搓;
第8行代碼調(diào)用模型對(duì)象的trainModel方法,即做模型訓(xùn)練纬傲;
第9行代碼調(diào)用模型對(duì)象的printConfusionMatrix方法满败,即打印混淆矩陣;
第10行代碼調(diào)用模型對(duì)象的printReportTable方法叹括,即打印報(bào)告表算墨;

import pickle

with open('mailContent_list.pickle', 'rb') as file:
    content_list = pickle.load(file)
with open('mailLabel_list.pickle', 'rb') as file:
    label_list = pickle.load(file)
model = TextClassification(content_list, label_list)
model.trainModel()
model.printConfusionMatrix()
model.printReportTable()

上面一段代碼的運(yùn)行結(jié)果如下圖所示,警告部分不影響程序運(yùn)行

image.png

7.總結(jié)

1.本文是作者第6個(gè)NLP小型項(xiàng)目汁雷,數(shù)據(jù)共有60000多條净嘀。
2.分類模型的評(píng)估指標(biāo)F1score為0.994左右,總體來(lái)說(shuō)這個(gè)分類模型很優(yōu)秀侠讯,能夠投入實(shí)際應(yīng)用挖藏。
3.本文進(jìn)行了類的封裝,小型中文文本分類項(xiàng)目經(jīng)過(guò)數(shù)據(jù)處理得到內(nèi)容列表content_list和標(biāo)簽列表label_list之后厢漩,即可直接使用類做模型訓(xùn)練和預(yù)測(cè)膜眠,并且得到詳細(xì)的預(yù)測(cè)結(jié)果報(bào)告表。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末溜嗜,一起剝皮案震驚了整個(gè)濱河市宵膨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粱胜,老刑警劉巖柄驻,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異焙压,居然都是意外死亡鸿脓,警方通過(guò)查閱死者的電腦和手機(jī)抑钟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)野哭,“玉大人在塔,你說(shuō)我怎么就攤上這事〔η” “怎么了蛔溃?”我有些...
    開(kāi)封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)篱蝇。 經(jīng)常有香客問(wèn)我贺待,道長(zhǎng),這世上最難降的妖魔是什么零截? 我笑而不...
    開(kāi)封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任麸塞,我火速辦了婚禮,結(jié)果婚禮上涧衙,老公的妹妹穿的比我還像新娘哪工。我一直安慰自己,他們只是感情好弧哎,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布雁比。 她就那樣靜靜地躺著,像睡著了一般撤嫩。 火紅的嫁衣襯著肌膚如雪偎捎。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天非洲,我揣著相機(jī)與錄音鸭限,去河邊找鬼。 笑死两踏,一個(gè)胖子當(dāng)著我的面吹牛败京,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梦染,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼赡麦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了帕识?” 一聲冷哼從身側(cè)響起泛粹,我...
    開(kāi)封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肮疗,沒(méi)想到半個(gè)月后晶姊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伪货,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年们衙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钾怔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蒙挑,死狀恐怖宗侦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忆蚀,我是刑警寧澤矾利,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站馋袜,受9級(jí)特大地震影響男旗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜欣鳖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一剑肯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧观堂,春花似錦、人聲如沸呀忧。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)而账。三九已至胰坟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泞辐,已是汗流浹背笔横。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咐吼,地道東北人吹缔。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像锯茄,于是被迫代替她去往敵國(guó)和親厢塘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容