zoukankan      html  css  js  c++  java
  • 基于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=='
    '):
    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 != '
    ' and word != '	' 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%的准确率,也算是比较实用了。

  • 相关阅读:
    C#Task和async/await使用
    jQuery实现C#CheckBoxList模糊搜索
    [转载].NET ASP.NET 中web窗体(.aspx)利用ajax实现局部刷新
    MD5加密方法HashPasswordForStoringInConfigFile(string,string)过时问题处理方法
    .NET WebFrom跨时区项目时间问题处理方法
    C# 中的Async 和 Await 的用法详解
    cron表达式
    ASP.NET MVC实现单用户登录
    一个简单便捷的树形显示Ztree
    身份证代码
  • 原文地址:https://www.cnblogs.com/NosenLiu/p/14876563.html
Copyright © 2011-2022 走看看