基于SKLearn的SVM模型垃圾邮件分类——代码实现及优化

NosenLiu 2021-06-11 原文


基于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%的准确率,也算是比较实用了。

 

发表于
2021-06-11 21:27 
牛云杰 
阅读(3
评论(0
编辑 
收藏 
举报

 

版权声明:本文为NosenLiu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/NosenLiu/p/14876563.html

基于SKLearn的SVM模型垃圾邮件分类——代码实现及优化的更多相关文章

  1. 大白话5分钟带你走进人工智能-第一节开篇介绍以及线性回归简介篇

                                                            […]...

  2. 机器学习简介

    一、人工智能与机器学习 说到人工智能,就不得不提图灵测试。图灵测试是阿兰图灵在1950年提出的一个关于机器是否 […]...

  3. 机器学习笔记(6) 线性回归

    先从最简单的例子开始,假设我们有一组样本(如下图的一个个黑色的圆点),只有一个特征,如下图,横轴是特征值,纵轴 […]...

  4. 模型评估

    文章从模型评估的基本概念开始,分别介绍了常见的分类模型的评估指标和回归模型的评估指标以及这些指标的局限性。部分 […]...

  5. 机器学习、NLP、Python和Math最好的150余个教程(建议收藏)

    机器学习、NLP、Python和Math最好的150余个教程(建议收藏) 编辑 | MingMing   尽管 […]...

  6. 认识:人工智能AI 机器学习 ML 深度学习DL – 在某一天老去

    认识:人工智能AI 机器学习 ML 深度学习DL 人工智能 人工智能(Artificial Intellige […]...

  7. 机器学习|二进制粒子群优化算法应用于特征选择问题

    1.特征选择 比如有一个M行(N+1)列的数据集,每一行代表一组数据,对于每一列,前N列代表N个特征,最后一列 […]...

  8. 一种海量社交短文本的热点话题发现方法

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯QQ大数据 发表于云+社区专栏 随着社交 […]...

随机推荐

  1. 企业级管理软件快速开发平台-极致业务基础平台-可视化工作流设计

    前几篇博介绍了极致业务基础平台的框架及一些开发效果详细见下面的地址   极致业务基础平台简要介绍: http: […]...

  2. 重启iis的命令是什么?三种简单的重启方式

    第一种、界面操作   打开“控制面板”->“管理工具”->“服务”。找到“IIS Admin Se […]...

  3. 一口气说出 Redis 16 个常见使用场景!

    1、缓存 String类型 例如:热点数据缓存(例如报表、明星出轨),对象缓存、全页缓存、可以提升热点数据的访 […]...

  4. [Interview] 程序员如何制作一份漂亮的面试简历

    简历模板 html, body, div, span, applet, object, iframe, h1, […]...

  5. javascript判断碰撞检测

    javascript判断碰撞检测 点与矩形的碰撞检测 <pre> /** * * @param x […]...

  6. 最长回文子串

    最长回文子串 最长回文子串 输入一个字符串,找到回文、子串、最长,输出任意一个      详细思路 对于每一个 […]...

  7. nodejs安装

    下载安装地址:https://nodejs.org/en/download/ 查询安装系统:[root@VM-4-3-centos /]# unameLinux[root@VM-4-3-centos /]# uname -mx86_64...

  8. MySQL学习 int(2)和int(4)的区别

    问题 int(1) int(4) 中的作用是做什么 概述 int(1),int(4)括号中的数字是为了填充长度 […]...

展开目录

目录导航