背景介绍
在我们日常生活中,经常会受到各种垃圾邮件,譬如来自商家的广告、打折促销信息、澳门博彩邮件、理财推广信息等,一般来说邮件客户端都会设置一定的关键词屏蔽这种垃圾邮件,或者对邮件进行归类,但是总会有一些漏网之鱼。 不过,自己手动做一个垃圾邮件分类器也并不是什么难事。传统的机器学习算法通常会采用朴素贝叶斯、支持向量机等算法对垃圾邮件进行过滤,今天我们主要讲如何用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爱好者社区”,了解相关信息可以关注“ Python爱好者社区”