基於SKLearn的SVM模型垃圾郵件分類——代碼實現及優化


一. 前言

由於最近有一個郵件分類的工作需要完成,研究了一下基於SVM的垃圾郵件分類模型。參照這位作者的思路(https://blog.csdn.net/qq_40186809/article/details/88354825),使用trec06c這個公開的垃圾郵件語料庫(https://plg.uwaterloo.ca/~gvcormac/treccorpus06/)作為數據進行建模。並對代碼進行優化,提升訓練速度。

工作過程如下:

1,數據預處理,提取每一封郵件的內容,進行分詞,數據清洗。

2,選取特征,將郵件內容轉換為特征向量。

3,使用sklearn建立SVM模型。

4,代碼調整及優化。

 

二.數據預處理

trec06c這個數據集的數據比較特殊,由215個文件夾組成,每個文件夾下方包含300個編碼為GBK的郵件文件,都為原始郵件數據。共21766個正樣本,42854個負樣本,其樣本的正負性由文件夾下的index文件所標識。下方就是一個垃圾郵件(負樣本)的示例:

首先按照自己的需要,將index文件進行處理,控制正負樣本的數量比例持平,得到新的索引文件CN_index_ham、CN_index_spam,其內容為郵件的相對位置索引: 

經過觀察,各郵件文件的前半部分為其收發、通信的基本信息,后半部分才是郵件的具體內容,兩部分之間以一個空行進行間隔。因此郵件預處理的思路為按行讀取整個郵件,搜索其第一個空行,並將該空行之后的每一行內容進行記錄、分詞、篩選停用詞等操作。

在預處理過程中還需解決以下2個小問題:

①編碼問題,雖然說文件確定是GBK編碼格式,但仍有部分奇奇怪怪的字符無法正確解碼,因此使用try操作將readline()操作進行包裹,遇到編碼有問題的內容直接讀取下一行。

②由於問題①使用try操作,在except中直接continue,使得在讀取郵件內容時,如果郵件最后一行的編碼有問題,直接continue進入下一次循環,而下一次循環已到文件末尾,沒有東西可讀,程序會反復進行readline()操作並跳入except,陷入無限循環。為解決這個問題,設置一個tag,在每進行一次except操作時,將此tag += 1,如果連續循環20次仍無新內容讀入,則結束文件讀取。

該部分代碼如下。

 1 #coding:utf-8
 2 import jieba
 3 import pandas as pd
 4 import numpy as np
 5 
 6 from sklearn.feature_extraction.text import CountVectorizer
 7 from sklearn.svm import SVC
 8 from sklearn.model_selection import train_test_split
 9 from sklearn.externals import joblib
10 
11 path_list_spam = []
12 with open('./data/CN_index_spam','r',encoding='utf-8') as fin:
13     for line in fin.readlines():
14         path_list_spam.append(line.strip())
15 
16 path_list_ham = []
17 with open('./data/CN_index_ham','r',encoding='utf-8') as fin:
18     for line in fin.readlines():
19         path_list_ham.append(line.strip())
20 
21 stopWords = []
22 with open('./data/CN_stopWord','r',encoding='utf-8') as fin:
23     for i in fin.readlines():
24         stopWords.append(i.strip())
25 
26 # 定義一些超參數
27 MAX_EMAIL_LENGTH = 200   #最長郵件長度
28 THRESHOLD = len(path_list_ham)/50   # 超過這么多次的詞匯,入選dict
29 
30 path_list_spam = path_list_spam[:5000]  # 將正例、負例樣本取子集,先各取5000個做實驗
31 path_list_ham = path_list_ham[:5000]
32 # 下方定義數值類字符串檢驗函數 ,預處理時需要將數值信息清洗掉
33 def is_number(s):
34     try:
35         float(s)
36         return True
37     except ValueError:
38         pass
39     try:
40         import unicodedata
41         unicodedata.numeric(s)
42         return True
43     except (TypeError, ValueError):
44         pass
45     return False

上方代碼完成了讀取正例、負例樣本序列,讀取停用詞-stopword的工作。並且定義了兩個超參數,MAX_EMAIL_LENGTH為郵件讀取詞匯最差長度,這里設為200,避免讀取到2/3千字的超長郵件,占據過大內存;THRESHOLD是特征詞入選閾值,如當THRESHOLD=20時,某個詞匯在超過20封郵件中出現過,則將它列為特征詞之一。定義is_number()函數來判斷某個字符串是否為數字,以便於將其清洗出去。

 1 def email_cut(path_list):
 2     emali_str_list = []
 3     for i in range(len(path_list)):
 4         print('====== ',i,' =======')
 5         print(path_list[i])
 6         with open(path_list[i],'r',encoding='gbk') as fin:
 7             words = []
 8             begin_tag = 0
 9             wrong_tag = 0
10             while(True):
11                 if wrong_tag > 20 or len(words)>MAX_EMAIL_LENGTH:
12                     break
13                 try:
14                     line = fin.readline()
15                     wrong_tag = 0
16                 except:
17                     wrong_tag += 1
18                     continue
19                 if (not line):
20                     break
21                 if(begin_tag == 0):
22                     if(line=='\n'):
23                         begin_tag = 1
24                     continue
25                 else:
26                     l = jieba.cut(line.strip())
27                     ll = list(l)
28                     for word in ll:
29                         if word not in stopWords and word != '\n' and word != '\t' and word != ' ' and not is_number(word):  # 
30                             words.append(word)
31                         if len(words)>MAX_EMAIL_LENGTH:  # 一封email最大詞匯量設置
32                             break
33             wordStr = ' '.join(words)
34             emali_str_list.append(wordStr)
35     return emali_str_list

上方函數email_cut() 的輸入參數為 path_list_ham 以及 path_list_spam,該函數根據這些郵件的path地址,將其信息按行進行讀取,並使用jieba進行分詞,清洗掉轉義字符以及數值類字符串,最終將所有郵件的數據存入 emali_str_list 進行返回。

 

三.特征選取

在這一步中,使用 sklearn 中的 CountVectorizer 類輔助。統計所有郵件數據中出現的詞匯,並對這些詞匯進行篩選,選出現次數出大於 THRESHOLD 的部分,組成詞匯表,並對郵件文本數據進行轉換,以向量形式表示。

 1 def textToMatrix(text):
 2     cv = CountVectorizer()
 3     cv.fit(text)
 4     vocabulary = cv.vocabulary_
 5     vector = cv.transform(text)
 6     result = pd.DataFrame(vector.toarray())
 7     del(vector) # 及時刪除以節省內存空間
 8     features = []# 儲存特征值
 9     for key, value in vocabulary.items():  # key, value 示例  '孔子', 23772  即 詞匯,字符串 的形式
10         if result[value].sum() >= THRESHOLD:
11             features.append(key) # 加入詞匯表
12             result.rename(columns={value:key}, inplace=True) # 本來的列名是索引值value,現在改成key ('孔子'、'后人'、'家鄉' ..等詞匯)
13     return result[features]  # 縮減特征矩陣規模,僅將特征詞匯表中的列留下

在上方函數中,使用CountVectorizer()將郵件內容(即包含n條字符串的List,每個字符串代表一封郵件)進行統計,獲取詞匯列表,並將郵件內容進行轉換,轉換成一個稀疏矩陣,該郵件沒有出現過的詞匯索引下方對應的值為0,出現過的詞匯索引下方對應的值為該詞在本郵件中出現過的次數。在for循環中,查看詞匯在所有郵件中出現的次數是否大於THRESHOLD ,如大於,則將該位置的列首索引替換為該詞匯本身(key為詞匯,value為詞語本身),最后對大的郵件特征矩陣進行精簡,僅留下特征詞所屬的列進行返回。最終返回的結果大概是下面這種樣式:

最上方一行漢語詞匯為特征詞匯,下面每一行數據代表一封Email的內容,其數值代表對應詞匯在這個Email中的出現次數。可以看出,SVM不能對語句的順序關系進行學習,不同的Email內容可能對應着同樣的特征向量結果。例如:“我想要吃大蘋果” 與“吃蘋果想要大我” 對應的特征向量是一模一樣的。不過一般來講問題不大,畢竟研表究明,漢字的序順並不能影閱響讀嘛。

 

四.建立SVM模型

最后,使用sklearn的SVC模塊對所有郵件的特征向量進行建模訓練。

 1 ham_str_all = email_cut(path_list_ham)
 2 spam_str_all = email_cut(path_list_spam)
 3 allWord = []
 4 allWord.extend(ham_str_all)
 5 allWord.extend(spam_str_all)
 6 labels = []#標簽
 7 labels.extend(np.ones(len(path_list_ham)))
 8 labels.extend(np.zeros(len(path_list_spam)))
 9 vector = textToMatrix(allWord)#獲取特征向量
10 print(vector)
11 feature = list(vector.columns)
12 print("feature length: ",len(feature))
13 with open('./model/CN_features.txt', 'w', encoding="UTF-8") as f:
14     s = ' '.join(feature)
15     f.write(s)
16 svm = SVC(kernel='linear', C=0.5, random_state=0)  # 線性核,C的值較小時可以允許一些錯誤 可選核: 'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'
17 # 將數據分成測試集和訓練集
18 X_train, X_test, y_train, y_test = train_test_split(vector, labels, test_size=0.3, random_state=0)
19 svm.fit(X_train, y_train)
20 print(svm.score(X_test, y_test))
21 model = joblib.dump(svm,'./model/svm_model.m')

首先是讀取正例郵件和反例郵件,並生成其對應的label序列,將郵件轉化為由特征向量組成的matrix(在本例中,特征詞匯正好有256個,也就是說特征向量的維度為256),保存特征詞匯,使用SVC模塊建立SVM模型,分離訓練集與測試集,擬合訓練,對測試集進行計算評分后保存模型。

 

五.代碼調整及優化

整個實踐建模的過程其實到上面已經結束了,但在實際使用的過程中,發現有下面2個問題。

①訓練速度極慢,5000個正樣本+5000個負樣本需要訓練2個小時。這完全不是svm的訓練速度,而是神經網絡的訓練速度了。在參考的那篇博客中,作者(Ning_wxh)也提到,他的機器只能各取600個正樣本/反樣本進行訓練,再多機器就受不了了。

②內存消耗太大,我電腦16GB的內存都被占滿,不停的從虛擬內存中進行數據交換。下圖內存占用圖中,周期型的鋸齒狀波動表明了實體內存在與虛擬內存作交換。

 先說第②個問題。這個問題通過設置 MAX_EMAIL_LENGTH(郵件最大詞匯數目) 和 增加 THRESHOLD 的值來實現的。設置郵件最大詞匯數目為200,避免將幾千字的Email內容全部讀入;而最開始的THRESHOLD值設置為10,最終的特征向量維度為900+,特征向量過於稀疏,便將THRESHOLD設置為樣本總數的50分之1,即100,將維度降為256。此外在textToMatrix()函數中,將vector變量及時刪除,清空內存開銷。這3個步驟,在正/負樣本數量都為5000時,將內存消耗控制在10GB以下。

再說第①個問題。經過不停的錨點調試,發現時間消耗最大的一步語句是textToMatrix()函數中的: result.rename(columns={value:key}, inplace=True)  語句。這條語句的意思是將pd.DataFrame的某列列名進行替換,由value替換為key。由於我們的原始詞匯較多,導致有40000多列數據,定位value列的過程開銷較大,導致較大的時間開銷。原因已經找到,解決這個問題的思路由兩個:一是對列名構建索引,以便快速定位;二是重新構建一個新的pd.DataFrame數據表,將改名操作批量進行。

這里選擇第二種思路,就是空間換時間嘛,重寫textToMatrix()函數如下:

 1 def textToMatrix(text):
 2     cv = CountVectorizer()
 3     cv.fit(text)
 4     vocabulary = cv.vocabulary_
 5     vector = cv.transform(text)
 6     result = pd.DataFrame(vector.toarray())
 7     del(vector)
 8     features = []# 儲存特征值
 9     origin_data = np.zeros((len(result),1)) # 新建的數據表
10     for key, value in vocabulary.items():
11         if result[value].sum() >= THRESHOLD:
12             features.append(key)
13             origin_data = np.column_stack((origin_data,np.array(result[value])))  # 按列堆疊到新數據表
14     origin_data = origin_data[:,1:]  # 刪掉初始化的第一列全0數據
15     print('origin_data shape: ',origin_data.shape)
16     origin_data = pd.DataFrame(origin_data)    # 轉換為DataFrame對象
17     origin_data.columns = features   # 批量修改列名
18     print('features length: ',len(features))
19     return origin_data 

最終,僅耗時2分鍾便完成SVM模型的訓練,比優化代碼之前速度提高了60倍。在測試集上的預測精度為0.93666,即93.6%的准確率,也算是比較實用了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM