背景介紹
在我們日常生活中,經常會受到各種垃圾郵件,譬如來自商家的廣告、打折促銷資訊、澳門博彩郵件、理财推廣資訊等,一般來說郵件用戶端都會設定一定的關鍵詞屏蔽這種垃圾郵件,或者對郵件進行歸類,但是總會有一些漏網之魚。 不過,自己手動做一個垃圾郵件分類器也并不是什麼難事。傳統的機器學習算法通常會采用樸素貝葉斯、支援向量機等算法對垃圾郵件進行過濾,今天我們主要講如何用PaddlePaddle手寫一個垃圾郵件分類器。當然,在講PaddlePaddle做垃圾郵件處理之前,先回顧一下傳統的機器學習算法是如何對垃圾郵件進行分類的。
了解資料集
首先先了解一下今天的資料集:trec06c。trec06c是一個公開的垃圾郵件語料庫,由國際文字檢索會議提供,分為英文資料集(trec06p)和中文資料集(trec06c),其中所含的郵件均來源于真實郵件保留了郵件的原有格式和内容。檔案下載下傳位址:trec06c檔案格式:
trec06c
│
└───data
│ │ 000
│ │ 001
│ │ ...
│ └───215
└───delay
│ │ index
└───full
│ │ index
檔案内容:
垃圾郵件示例:本公司有部分普通發票(商品銷售發票)增值稅發票及海關代征增值稅專用繳款書及其它服務行業發票,公路、内河運輸發票。可以以低稅率為貴公司代開,本公司具有内、外貿生意實力,保證我司開具的票據的真實性。 希望可以合作!共同發展!敬侯您的來電洽談、咨詢! 聯系人:李先生 聯系電話:13632588281 如有打擾望諒解,祝商琪。正常郵件示例:講的是孔子後人的故事。一個老上司回到家鄉,跟兒子感情不和,跟貪财的孫子孔為本和睦。老上司的弟弟魏宗萬是趕馬車的。有個洋妞大概是考察民俗的,在他們家過年。孔為本總想出國,被爺爺教育了。最後,一家人基本和解。 順便問另一類電影,北京青年電影制片廠的。
資料預處理
拿到資料後我們可以很清楚的看到郵件的内容,但并不是所有的内容都是我們需要的,在這裡我們僅提取了郵件中的中文來作為訓練語料。如果仔細觀察的話,會發現不是所有的郵件都能直接打開,資料的編碼格式也需要轉換成utf-8格式友善我們後面訓練使用。是以我們需要對原始資料做一些資料預處理,包括以下幾個内容。
基本步驟
轉換源資料編碼格式為utf-8格式
過濾字元
去除所有非中文字元,如标點符号、英文字元、數字、網站連結等特殊字元。
過濾停用詞
對郵件内容進行分詞處理
訓練代碼
下面是具體的代碼 transfer.py:
# -*- coding: utf-8 -*-
#Created by huxiaoman 2018.1.28
#transfer.py:生成spam和ham資料
import jieba
import sys
import os
import re
# 判斷郵件中的字元是否是中文
def check_contain_chinese(check_str):
for ch in check_str.decode('utf-8'):
if u'\u4e00' <= ch <= u'\u9fff':
return True
return False
# 加載郵件資料的label
def load_label_files(label_file):
label_dict ={}
for line in open(label_file).readlines():
list1 = line.strip().split("..")
label_dict[list1[1].strip()] = list1[0].strip()
return label_dict
# 加載停用詞詞表
def load_stop_train(stop_word_path):
stop_dict = {}
for line in open(stop_word_path).readlines():
line = line.strip()
stop_dict[line] = 1
return stop_dict
# 讀取郵件資料,并轉換為utf-8格式,生成spam和ham樣本
def read_files(file_path,label_dict,stop_dict,spam_file_path,ham_file_path):
parents = os.listdir(file_path)
spam_file = open(spam_file_path,'a')
ham_file = open(ham_file_path,'a')
for parent in parents:
child = os.path.join(file_path,parent)
if os.path.isdir(child):
read_files(child,label_dict,stop_dict,spam_file_path,ham_file_path)
else:
print child[10:]
label = "unk"
if child[10:] in label_dict:
label = label_dict[child[10:]]
# deal file
temp_list = []
for line in open(child).readlines():
line = line.strip().decode("gbk",'ignore').encode('utf-8')
if not check_contain_chinese(line):
continue
seg_list = jieba.cut(line, cut_all=False)
for word in seg_list:
if word in stop_dict:
continue
else:
temp_list.append(word)
line = " ".join(temp_list)
print label
if label == "spam":
spam_file.write(line.encode("utf-8","ignore") + "\n")
if label == "ham":
ham_file.write(line.encode("utf-8","ignore")+"\n")
# 生成word2vec詞表
def generate_word2vec(file_path,label_dict,stop_dict,word_vec):
parents = os.listdir(file_path)
fh1 = open(word_vec,'a')
i = 0
for parent in parents:
child = os.path.join(file_path,parent)
if os.path.isdir(child):
generate_word2vec(child,label_dict,stop_dict,word_vec)
else:
print child[10:]
i += 1
print i
label = "unk"
if child[10:] in label_dict:
label = label_dict[child[10:]]
# deal file
temp_list = []
for line in open(child).readlines():
line = line.strip().decode("gbk",'ignore').encode('utf-8')
if not check_contain_chinese(line):
continue
if len(line) == 0:
continue
seg_list = jieba.cut(line, cut_all=False)
for word in seg_list:
if word in stop_dict:
continue
else:
temp_list.append(word)
line = " ".join(temp_list)
fh1.write(line.encode("utf-8","ingore")+"\n")
if __name__=="__main__":
file_path = sys.argv[1]
label_path = sys.argv[2]
stop_word_path = "stop_words.txt"
word_vec_path = "word2vec.txt"
spam_data = "spam.txt"
ham_data = "ham.txt"
label_dict = load_label_files(label_path)
stop_dict = load_stop_train(stop_word_path)
read_files(file_path,label_dict,stop_dict,spam_data,ham_data)
運作腳本
run.sh:
bashif [ $1 = "test" ]; then
echo "test"
python transfer.py ../test/ ../trec06c/full/index
else
echo "whole"
python transfer.py ../trec06c/data/ ../trec06c/full/index
fi
運作方式:
sh run.sh
運作結果:
ham.txt: 正樣本,正常郵件。共21373條資料。
示例:我 就 鬧 不 明白 了 隻要 你 本人 不介意 跟 你 爸爸媽媽 有 何幹 為啥 要說 呢 ..... 首先 謝謝 大家 安慰 我 。 但是 我 确實 很 難受 , 我 有 自己 的 苦衷 。 我 不敢 和 我 媽媽 說 的 這種 情況 。 我 媽媽 是 那種 特别 容易 擔心 的 那種 類型 。 而且 我 又 不 在 她 身邊 。 我家 是 外地 的 。 如果 和 媽媽 說 了 , 她 一定 不會 同意 我 和 在 一起 的 。 媽媽 對 身體健康 看 的 特别 重要 。 有 一年 姐夫 那年 經常 流鼻血 , 媽媽 都 特别 擔心 , 老 催 姐姐 帶 着 去 看看 。
spam.txt: 負樣本,垃圾郵件。共41627條資料。
示例:您好 以下 是 特别 為 閣下 發 的 香港 資訊 圖檔 、 景點 等 不 知道 閣下 是否 喜 希望 沒有 打擾到 閣下 如果 無法 看到 下面 内容 請 稍侯 或者 直接 進入 香港 行網 域名論壇 位址 真誠 為您服務
word2vec.txt: 包含所有郵件分詞的内容,為Word2Vec提供訓練預料。共63000條資料。
示例:我 覺得 , 負債 不要緊 , 最 重要 的 是 能 負得起 這個 責任 來 , 欠 了 那麼 多錢 , 至少 對 當初 拿出 愛心 來 的 網友 們 有 個 交待 , 還 , 還是 不 還 了 , 或者 , 是 有 這個 心 但 實在 沒 能力 , 說明 一聲 還 都 好 不要 連 ID 都 不 激活 了 , 連 手機号 都 換 了 … … 别說 外地 的 了 , 就 連 北京 的 網友 都 找 不到 他 … … 他 當時 在 水木 fl 版 的 那陣 , 我 旁觀 了 全過程 。
生成詞向量
傳統方法的局限性
我們知道,分詞後的資料是不能直接拿到模型裡去訓練的,我們需要把詞語轉換成詞向量才能進行模型的訓練,這樣一個詞可以有一個多元的詞向量組成。 傳統的方法是one-hot encoding,即用一個長向量來表示一個詞,向量的長度為詞典的大小,向量的分量隻有一個1,其餘全為0,1的位置即對應改詞在詞典中的位置,如電腦表示為:[0 0 0 0 0 1 0 0 0 0 ],耳機表示為[0 0 0 0 0 0 0 1 0 ]這種方式如果采用稀疏存儲,表達簡潔,占用空間少,但是這種方法也有幾個缺點,一是容易受維數災難的困擾,尤其是将其用于 Deep Learning的一些算法時;二是不能很好地刻畫詞與詞之間的相似性,即任意兩個詞之間都是孤立的。光從這兩個向量中看不出兩個詞是否有關系,損失大部分資訊,導緻結果會有較大偏差。
Word2Vec方法的優勢
在1968年Hinton又提出了Distributed REpresentation,可以One-hot encoding的缺點。其基本想法是直接用一個普通的向量表示一個詞,這種向量一般長成這個樣子:[0.792, −0.177, −0.107, 0.109, −0.542, ...],也就是普通的向量表示形式。次元以 50 維和 100 維比較常見。當然一個詞怎麼表示成這麼樣的一個向量需要通過訓練得到,訓練方法較多,word2vec是最常見的一種。需要注意的是,每個詞在不同的語料庫和不同的訓練方法下,得到的詞向量可能是不一樣的。詞向量一般維數不高,一般情況下指定1000、500維就可以了,是以用起來維數災難的機會現對于one-hot representation表示就大大減少了。
由于是用向量表示,而且用較好的訓練算法得到的詞向量的向量一般是有空間上的意義的,也就是說,将所有這些向量放在一起形成一個詞向量空間,而每一向量則為該空間中的一個點,在這個空間上的詞向量之間的距離度量也可以表示對應的兩個詞之間的“距離”。所謂兩個詞之間的“距離”,就是這兩個詞之間的文法,語義之間的相似性。
一個比較不錯的應用方法是,得到詞向量後,假如對于某個詞A,想找出這個詞最相似的詞,在建立好詞向量後的情況,對計算機來說,隻要拿這個詞的詞向量跟其他詞的詞向量一一計算歐式距離或者cos距離,得到距離最小的那個詞,就是它最相似的。
是以在這裡我們選擇了word2vec方法來訓練生成詞向量。關于word2vec的原理大家可以在網上搜尋學習,此處不再贅述。
實作代碼
在資料預進行中我們生成的word2vec.txt就可以放到此處訓練word2vec模型生成詞向量了,具體實作代碼如下: word2vec.py
# -*- coding: utf-8 -*-
# Created by huxiaoman 2018.1.28
# word2vec.py:生成word2vec模型
import os
import sys
import numpy as np
from gensim.models.word2vec import Word2Vec
from gensim.corpora.dictionary import Dictionary
import codecs
reload(sys)
sys.setdefaultencoding( "utf-8" )
class MySentences(object):
def __init__(self, dirname):
self.dirname = dirname
def __iter__(self):
for fname in os.listdir(self.dirname):
for line in codecs.open(os.path.join(self.dirname, fname),"r", encoding="utf-8",errors="ignore"):
yield line.strip().split()
# word2vec.txt資料的位址
train_path = "rawData/"
# 生成的word2vec模型的位址
model_path = "/modelPath/"
sentences = MySentences(train_path)
# 此處min_count=5代表5元模型,size=100代表詞向量次元,worker=15表示15個線程
model = Word2Vec(sentences,min_count = 5,size=100,workers=15)
#儲存模型
model.save(model_path+'/Word2vec_model.pkl')
運作方式
python word2vec.py
運作結果
Word2vec_model.pkl
模型訓練
生成正負樣本資料并将詞語全部轉化為詞向量後我們就可以把資料灌倒模型裡進行訓練了,本篇中将采用傳統的機器學習算法svm來進行訓練。
具體步驟
加載資料集
劃分訓練集train、驗證集val與測試集test
定義訓練模型,并訓練
驗證準确率
# 建構svm模型,加載資料等代碼詳見github
def get_svm_model(x_train,y_train,x_val,y_val):
model = SVC(C=1,kernel='rbf',max_iter=10,gamma=1,probability=True)
model.fit(x_train,y_train)
pred=model.predict(x_val)
fpr,tpr,thresholds = roc_curve(y_val, pred, pos_label=2)
score = metrics.f1_score(y_val,pred)
print score
python train_svm.py
0.73343221
小結
本篇文章作為用PaddlePaddle處理垃圾郵件實戰系列的預熱,主要講了如何對文本資料進行資料預處理與過濾,如何生成詞向量以及用傳統的機器學習方法--支援向量機訓練模型,得到的準确率為0.73343221。其結果的好壞取決于詞典的大小,詞向量次元的大小,svm的基本參數的調整,在實際操作過程中還需要不斷的調參才能達到最優的效果。下一篇我們将帶領大家如何用PaddlePaddle來做垃圾郵件處理,用深度學習的方法對垃圾郵件進行分類,看看效果是否比傳統的機器學習方法要更好,性能和速度是否能有一定的提升。
原文釋出時間為:2018-07-20
本文作者:Cahrlotte77
本文來自雲栖社群合作夥伴“
Python愛好者社群”,了解相關資訊可以關注“
”