天天看點

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

2018年9月27日筆記

jieba中文叫做結巴,是一款中文分詞工具,官方文檔連結:https://github.com/fxsjy/jieba

TfidfVectorizer中文叫做___ 詞頻逆文檔頻率向量化模型,是用來文章内容向量化的工具,官方文檔連結:http://sklearn.apachecn.org/cn/0.19.0/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html

LogisticRegression中文叫做邏輯回歸模型___,是一種基礎、常用的分類方法。

建議讀者安裝anaconda,這個內建開發環境自帶了很多包。

到2018年9月27日仍為最新版本的anaconda下載下傳連結: https://pan.baidu.com/s/1pbzVbr1ZJ-iQqJzy1wKs0A 密碼: g6ex

官網下載下傳位址:https://repo.anaconda.com/archive/Anaconda3-5.2.0-Windows-x86_64.exe

下面代碼的開發環境為jupyter notebook,使用在jupyter notebook中的截圖表示運作結果。

0.打開jupyter

在桌面建立檔案夾命名為基于TfidfVectorizer的垃圾分類,如下圖所示:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

打開基于TfidfVectorizer的垃圾郵件分類檔案夾,在按住Shift鍵的情況下,點選滑鼠右鍵,出現如下圖所示。

選擇在此處打開PowerShell視窗,之後會在此路徑下打開PowerShell。

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

在PowerShell中輸入指令并運作:jupyter notebook

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

PowerShell運作指令後,會自動打開網頁,點選如下圖所示網頁中的按鈕:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

spam中文叫做垃圾郵件

代碼檔案重命名為spamMailTest,重命名按鈕位置如下圖所示:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

1.資料下載下傳

資料檔案下載下傳連結: https://pan.baidu.com/s/1kqOFq8Ou_2D3fIKp0l62qQ 提取碼: eu5x

壓縮檔案trec06c.zip當中含有64000多個包含郵件内容的文本檔案。

使用軟體Winrar無法解壓,使用軟體7zip或Bandizip可以解壓,需要3分鐘左右。

選擇解壓到trec06c,如下圖所示:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

解壓完成後,檔案夾中目錄結構如下圖所示:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

2.資料觀察

檢視檔案需要安裝Notepad++,安裝軟體後滑鼠右擊檔案,從Notepad++中打開按鈕如下圖所示:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

trec06c檔案夾中的data/000/001檔案内容如下圖所示。

篇幅有限,本文作者隻示範其中一篇郵件的内容。

通過檢視多篇郵件的内容,發現郵件頭和郵件内容以一個空行分隔。

在代碼中找到第一個

\n\n

分隔成2段,第1段為郵件頭,第2段為郵件内容。

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

trec06c檔案夾中的full/index檔案内容如下圖所示。

每1行按照空格分隔成2段,第1段是郵件是否為垃圾郵件,标簽值為spam則是垃圾郵件,标簽值為ham則是正常郵件;

第2段是此郵件對應的路徑,代碼中通過字元串切片和拼接稍作修改成為能夠讀取檔案的路徑。

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

3.資料準備

3.1 預測目标值

預測目标值指派給變量y,代碼如下:

with open('./trec06c/full/index') as file:
    y = [k.split()[0] for k in file.readlines()]
print(len(y))           

複制

上面一段代碼的運作結果如下:

64620

從上面的運作結果可以看出,共有64620個樣本。

3.2 郵件文本檔案路徑

郵件文本檔案路徑清單指派給變量filePath_list。

本文作者使用3種方法擷取文本檔案路徑,讀者可以參考。

3.2.1 使用trec06c檔案夾中的full/index檔案

第1種是直接使用trec06c檔案夾中的full/index檔案,代碼如下:

with open('./trec06c/full/index') as file:
    filePath_list = ['./trec06c' + k.strip().split()[1][2:] for k in file.readlines()]
print(filePath_list[0])
print(filePath_list[1])           

複制

上面一段代碼的運作結果如下:

64620

./trec06c/data/000/000

./trec06c/data/000/001

3.2.2 定義getFilePathList函數

第2種是定義getFilePathList函數,函數中主要使用os.path.listdir、os.path.isdir、os.path.isdir這3個方法,擷取根目錄下的所有檔案。

os.path.listdir方法需要1個參數,參數是路徑,參數資料類型為字元串,方法可以找出路徑下所有檔案夾和檔案。

os.path.isdir方法需要1個參數,參數是路徑,參數資料類型為字元串,方法可以判斷此路徑是否為檔案夾。

os.path.isfile方法需要1個參數,參數是路徑,參數資料類型為字元串,方法可以判斷此路徑是否為檔案。

import os
import time

def getFilePathList(rootDir):    
    filePath_list = []    
    fileOrDir_list = os.listdir(rootDir) #列出檔案夾下所有的目錄與檔案    
    for fileOrDir in fileOrDir_list:           
        path = os.path.join(rootDir, fileOrDir) 
        if os.path.isdir(path):              
            filePath_list.extend(getFilePathList(path))           
        if os.path.isfile(path):              
            filePath_list.append(path)    
    return filePath_list

startTime = time.time()
filePath_list = getFilePathList('./trec06c/data')
print(len(filePath_list))
print(filePath_list[0])
print(filePath_list[1])
print('function use %.2f seconds' %(time.time()-startTime))           

複制

上面一段代碼的運作結果如下:

64620

./trec06c/data\000\000

./trec06c/data\000\001

function use 24.47 seconds

3.2.3 定義getFilePathList2函數

第3種是定義getFilePathList2函數,函數中主要使用os.walk方法,擷取目錄下所有的檔案。

os.walk方法的傳回結果的資料類型是清單,清單中的元素的資料類型是元組。

元組的第1個元素為表示路徑的字元串;

元組的第2個元素為第1個元素所表示路徑下的檔案夾;

元組的第3個元素為第1個元素所表示路徑下的檔案;

import os
import time

def getFilePathList2(rootDir):
    filePath_list = []
    for walk in os.walk(rootDir):
        part_filePath_list = [os.path.join(walk[0], file) for file in walk[2]]
        filePath_list.extend(part_filePath_list)
    return filePath_list

startTime = time.time()
filePath_list = getFilePathList2('./trec06c/data')
print(len(filePath_list))
print(filePath_list[0])
print(filePath_list[1])
print('function use %.2f seconds' %(time.time()-startTime))           

複制

上面一段代碼的運作結果如下:

64620

./trec06c/data\000\000

./trec06c/data\000\001

function use 0.64 seconds

3.2.4 本節小結

對比3.2.2節和3.2.3節的運作時間,使用内置的os.walk方法效率高出很多。

24.47/0.64=38.2344

,在效率上,使用os.walk方法為自己實作方法的38倍。

3.3 郵件内容

3.3.1 加載郵件内容

本文作者在此項目開發中,采用快速疊代開發政策。

第1個疊代版本丢棄郵件頭隻使用郵件内容作為特征,就取得98%左右的準确率。

郵件内容清單指派給變量mailContent_list,代碼如下:

mailContent_list = []
for filePath in filePath_list:
    with open(filePath, errors='ignore') as file:
        file_str = file.read()
        mailContent = file_str.split('\n\n', maxsplit=1)[1] 
        mailContent_list.append(mailContent)
print(mailContent_list[1])           

複制

上面一段代碼的運作結果如下:

講的是孔子後人的故事。一個老上司回到家鄉,跟兒子感情不和,跟貪财的孫子孔為本和睦。

老上司的弟弟魏宗萬是趕馬車的。

有個洋妞大概是考察民俗的,在他們家過年。

孔為本總想出國,被爺爺教育了。

最後,一家人基本和解。

順便問另一類電影,北京青年電影制片廠的。中越戰背景。一軍人被介紹了一個對象,去相親。女方是軍隊醫院的護士,猶豫不決,總是在回憶戰場上負傷的男友,好像還沒死。最後

男方表示了解,歸隊了。

從上面的運作結果可以看出,擷取郵件内容清單成功。

3.3.2 正規表達式去除多餘空格

re.sub('\s+', ' ', k)

可以把變量k中多個空格或換行替換為單個空格。

import re 

mailContent_list = [re.sub('\s+', ' ', k) for k in mailContent_list]           

複制

4.分詞

4.1 加載停頓詞

停頓詞檔案下載下傳連結: https://pan.baidu.com/s/1JWQFy84wN_jhU9H2P1Ig7g 提取碼: uk4m

停頓詞檔案stopwords.txt檔案下載下傳完成後,放在代碼檔案的同級目錄。

with open('./stopwords.txt', encoding='utf8') as file:
    file_str = file.read()
    stopword_list = file_str.split('\n')
    stopword_set = set(stopword_list)
print(len(stopword_list))
print(len(stopword_set))           

複制

上面一段代碼的運作結果如下:

1233

1231

從上面的運作結果可以看出,停頓詞清單中有2個停頓詞重複。

4.2 去除停頓詞效率對比

jie.lcut(mail)

list(jie.cut(mail))

兩種寫法效果相同。

每篇郵件的分詞結果的資料類型為清單,本文作者使用了2種方法去除分詞結果中的停頓詞。

不去除停頓詞的代碼寫法

cutWords = [k for k in jieba.lcut(mail)]

第1種去除停頓詞的代碼寫法

cutWords = [k for k in jieba.lcut(mail) if k not in stopword_list]

import time
import jieba 

cutWords_list = []
startTime = time.time()
i = 0
for mail in mailContent_list[:3000]:
    cutWords = [k for k in jieba.lcut(mail) if k not in stopword_list]
    cutWords_list.append(cutWords)
    i += 1
    if i % 1000 == 0:
        print('前%d篇郵件分詞共花費%.2f秒' %(i, time.time()-startTime))           

複制

上面一段代碼的運作結果如下:

前1000篇郵件分詞共花費14.74秒

前2000篇郵件分詞共花費27.88秒

前3000篇郵件分詞共花費43.70秒

第2種去除停頓詞的代碼寫法

cutWords = [k for k in jieba.lcut(mail) if k not in stopword_set]

import time
import jieba 

cutWords_list = []
startTime = time.time()
i = 0
for mail in mailContent_list[:3000]:
    cutWords = [k for k in jieba.lcut(mail) if k not in stopword_set]
    cutWords_list.append(cutWords)
    i += 1
    if i % 1000 == 0:
        print('前%d篇郵件分詞共花費%.2f秒' %(i, time.time()-startTime))           

複制

上面一段代碼的運作結果如下:

前1000篇郵件分詞共花費5.50秒

前2000篇郵件分詞共花費10.37秒

前3000篇郵件分詞共花費16.20秒

從上面2種方法運作時間的對比可以看出,判斷1個元素是否在集合中比判斷1個元素是否在清單中效率要高。

判斷1個元素是否在集合中,使用hash算法,時間複雜度為O(1);

判斷1個元素是否在清單中,使用循環周遊對比的方法,時間複雜度為O(n)。

在此次分詞結果去除停頓詞的實踐中,使用判斷1個元素是否在集合中的方法,效率是判斷1個元素是否在清單中的3倍左右。

64000多篇郵件分詞去除停頓詞共花費350秒左右,即6分鐘左右。

4.3 儲存分詞結果

第1行代碼導入pickle庫

第3行代碼open方法中的'wb'表示檔案以二進制形式寫入。

第4行代碼調用pickle.dump方法将python中的對象儲存到檔案中。

import pickle

with open('cutWords_list.pickle', 'wb') as file:
    pickle.dump(cutWords_list, file)           

複制

4.4 加載分詞結果

本文作者提供已經完成的分詞結果,下載下傳連結: https://pan.baidu.com/s/1bjPgrsXKkovdgbdpzNXOmQ 提取碼: x71b

壓縮檔案cutWords_list.zip下載下傳完成後,其中的檔案cutWords_list.pickle解壓到代碼檔案同級目錄。

import pickle

with open('cutWords_list.pickle', 'rb') as file:
    cutWords_list = pickle.load(file)           

複制

5.TfidfVectorizer模型

調用sklearn.feature_extraction.text庫的TfidfVectorizer方法執行個體化模型對象。

TfidfVectorizer方法需要3個參數。

第1個參數是分詞結果,資料類型為清單,其中的元素也為清單;

第2個關鍵字參數min_df是詞頻低于此值則忽略,資料類型為int或float;

第3個關鍵字參數max_df是詞頻高于此值則忽略,資料類型為Int或float。

檢視TfidfVectorizer方法的更多參數用法,官方文檔連結:http://sklearn.apachecn.org/cn/0.19.0/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(cutWords_list, min_df=100, max_df=0.25)           

複制

6.訓練資料準備

模型需要的訓練資料是純數字的特征矩陣和預測目标值。

特征矩陣通過TfidfVectorizer模型獲得,預測目标值是标簽編碼的結果。

6.1 特征矩陣

第1行代碼調用TfidfVectorizer對象的fit_transform方法獲得特征矩陣;

第2行代碼列印檢視TfidfVectorizer對象的詞表大小;

第3行代碼檢視特征矩陣的形狀。

X = tfidf.fit_transform(mailContent_list)
print('詞表大小:', len(tfidf.vocabulary_))
print(X.shape)           

複制

6.2 預測目标值

第1行代碼導入sklearn.preprocessing庫的LabelEncoder類;

第3行代碼調用LabelEncoder()執行個體化标簽編碼對象;

第4行代碼調用标簽編碼對象的fit_transform方法擷取預測目标值。

from sklearn.preprocessing import LabelEncoder

labelEncoder = LabelEncoder()
y_encode = labelEncoder.fit_transform(y)           

複制

7.邏輯回歸模型

7.1 模型訓練

最後1行代碼ndarray對象的round方法表示小數點保留位數。

from sklearn.linear_model import LogisticRegressionCV
from sklearn.model_selection import train_test_split

train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.2)
logistic_model = LogisticRegressionCV()
logistic_model.fit(train_X, train_y)
logistic_model.score(test_X, test_y).round(4)           

複制

上面一段代碼的運作結果如下:

0.9791

7.2 模型儲存

儲存模型需要先安裝pickle庫,安裝指令:

pip install pickle

調用pickle庫的dump方法儲存模型,需要2個參數。

第1個參數是儲存的對象,可以為任意資料類型,因為有3個模型需要儲存,是以下面代碼第1個參數是字典。

第2個參數是儲存的檔案對象,資料類型為_io.BufferedWriter

import pickle

with open('allModel.pickle', 'wb') as file:
    save = {
        'labelEncoder' : labelEncoder,
        'tfidfVectorizer' : tfidf,
        'logistic_model' : logistic_model
    }
    pickle.dump(save, file)           

複制

7.3 模型加載

本文作者提供已經完成的模型檔案,下載下傳連結: https://pan.baidu.com/s/1lMbDgxBrGPsXACA_26c75g 提取碼: vve6

import pickle

with open('allModel.pickle', 'rb') as file:
    allModel = pickle.load(file)
    labelEncoder = allModel['labelEncoder']
    tfidfVectorizer = allModel['tfidfVectorizer']
    logistic_model = allModel['logistic_model']           

複制

8.模型評估

8.1 交叉驗證

from sklearn.linear_model import LogisticRegressionCV
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import ShuffleSplit

cv_split = ShuffleSplit(n_splits=5)
logisticCV_model = LogisticRegressionCV()
score_ndarray = cross_val_score(logisticCV_model, X, y, cv=cv_split)
print(score_ndarray)
print(score_ndarray.mean())           

複制

上面一段代碼的運作結果如下:

[0.97833488 0.97756113 0.97384711 0.97709687 0.97709687]

0.9767873723305479

8.2 混淆矩陣

此節代碼能夠成功運作的前提是先運作7.1節或7.3節的代碼。

繪制混淆矩陣的代碼如下:

from sklearn.metrics import confusion_matrix
import pandas as pd

predict_y = logistic_model.predict(X)
pd.DataFrame(confusion_matrix(y, predict_y),
            columns=labelEncoder.classes_,
            index=labelEncoder.classes_)           

複制

上面一段代碼的運作結果如下圖所示:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

8.3 報告表

繪制precision、recall、f1-score、support報告表,代碼如下:

import numpy as np
from sklearn.metrics import precision_recall_fscore_support

def eval_model(y_true, y_pred, labels):
    # 計算每個分類的Precision, Recall, f1, support
    p, r, f1, s = precision_recall_fscore_support(y_true, y_pred)
    # 計算總體的平均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']]

eval_model(y, predict_y, labelEncoder.classes_)           

複制

上面一段代碼的運作結果如下:

基于jieba、TfidfVectorizer、LogisticRegression的垃圾郵件分類

image.png

9.結論

本文是作者第3個NLP小型項目,資料共有64000多條。

經過交叉驗證,模型平均得分為0.98左右。

最後在全部樣本的f1-score名額為0.98,總體來說這個分類模型較優秀,能夠投入實際應用。