目录
背景
代码
1. 数据准备-1阶段
2. 数据探索
3. 数据准备-2阶段
4. 矢量化(向量化)
5. 建立模型
6. 模型评价
模型评价
1. 分别评价
2. 总体评价
背景
基于前文 (《【机器学习】Lesson 4 - 朴素贝叶斯(NB)文本分类》)对于垃圾邮件数据集的分类处理,增加 K邻居分类器(KNN)/支持向量机(SVC)/随机森林 算法模型,并通过生成准确性报告和绘制混淆矩阵,对不同模型的性能进行比较。同时丰富数据预处理部分,增加词云展现等,完整代码及跑通效果见文档绑定资源。
本文在代码解释部分会跳过与前文雷同部分,详细可以在本文绑定资源中下载完整跑通源码查看。
代码
用大白话来讲的话,就是:
我们需要用着 5572 条邮件训练一个预测模型,以达到对邮件【是否】属于【垃圾邮件】进行预测分析。为了达到这一目的,我们选取的数据集是已经人为标注为非垃圾邮件("ham")和垃圾邮件("spam")过的。随后将数据分为一大半(训练集)和一小半(测试集),用训练集训练一个模型,然后用模型预测测试集,再用得到的结果与测试集已有标注进行比对,然后得到召回率、正确率等等评估模型性能的评分结果数据。
具体按照一定步骤如下:
1. 数据准备-1阶段
- 导包、导数据
- 预处理:即根据得到的数据集情况,删除多余的列、填补空值等等
- 数据转化:如果是中文标签的话,换成数字型,机器才看得懂。如本例中将 'spam' 转化为 1,'ham' 转化为 0。
## 数据处理类
import numpy as np
import pandas as pd
import warnings## 数据可视化类
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
#from matplotlib.colors import ListedColormap #创建颜色映射表import wordcloud
from wordcloud import WordCloud## 自然语言处理类
import re #处理文本字符串,如查找、替换、分割和匹配
import nltk #文本预处理(如分词、词干提取、停用词过滤)、词性标注、命名实体识别、情感分析等各种自然语言处理任务
from nltk.corpus import stopwords #文本预处理
from nltk.tokenize import word_tokenize #分割文本
from nltk.stem.porter import PorterStemmer #词干提取算法
from nltk.stem import WordNetLemmatizer #去标点符号
from collections import Counterfrom sklearn.preprocessing import MinMaxScaler,LabelEncoder
# MinMaxScaler 能将类别型数据转换为数字
# LabelEncoder 进行特征放缩
from sklearn.feature_extraction.text import TfidfVectorizer #将原生文档转化为txt矩阵## 分析算法类
from sklearn.naive_bayes import MultinomialNB #朴素贝叶斯
from sklearn.ensemble import RandomForestClassifier # 随机森林
from sklearn.neighbors import KNeighborsClassifier # KNN
from sklearn.svm import SVC # 支持向量机## 算法优化类
from sklearn.pipeline import Pipeline #流水线操作
from sklearn.model_selection import GridSearchCV,cross_val_score,train_test_split
# GridSearchCV 网格搜索,cross_val_score 交叉验证,train_test_split 数据划分## 算法评分类
from sklearn import metrics
from sklearn.metrics import precision_score, recall_score, plot_confusion_matrix, classification_report, accuracy_score, f1_score# 加载数据集
data = pd.read_csv("D:/project/Jupyter/csdn/AI_ML/datasets/Lesson4_SMS.csv")data.info()# 去除后面三列无意义列
to_drop = ["Unnamed: 2","Unnamed: 3","Unnamed: 4"]
data = data.drop(data[to_drop], axis=1)# 重命名前两列
data.rename(columns = {"v1":"Target", "v2":"Text"}, inplace = True)
data.head()
2. 数据探索
根据已有数据的情况,对数据进行一些统计探索,必要时可以进行数据可视化,使探索更加直观,根据展现出的情况对信息进行处理。
- 数据可视化:初步,从种类记数、高频词排序、占比等角度绘制图形。
- 特征工程:目的是最大限度地从原始数据中提取特征以供算法和模型使用。本例中将邮件内容按照字符数、词数、句子数,对文本内容进行拆分,并进行可视化碳素两两之间的数量关系。
- 异常值处理:此时出现一些明显不对劲的数据,为异常值,如下图红框部分,则需要根据其所在范围进行剔除,提高模型的准确性。
#first of all let us evaluate the target and find out if our data is imbalanced or not
plt.figure(figsize=(6,4))
fg = sns.countplot(x= data["Target"])
fg.set_title("Count Plot of Classes")
fg.set_xlabel("Classes")
fg.set_ylabel("Number of Data points")## 词云
# 数据编码
data['spam'] = data['Target'].map( {'spam': 1, 'ham': 0} ).astype(int)
data.head(5)# 提取出内容列的字符数
data["No_of_Characters"] = data["Text"].apply(len)# 分离好坏类的短信,并以句子字符数为横轴绘图
data.hist(column='No_of_Characters', by='Target', bins=60, figsize=(12, 4))
plt.xlim(-40, 950)
plt.xlabel('Length') # 设置横轴标签
plt.ylabel('Count') # 设置纵轴标签plt.show()data_ham = data[data['spam'] == 0].copy()
data_spam = data[data['spam'] == 1].copy()def show_wordcloud(data_spam_or_ham, title):Text = ' '.join(data_spam_or_ham['Text'].astype(str).tolist())stopwords = set(wordcloud.STOPWORDS)fig_wordcloud = wordcloud.WordCloud(stopwords=stopwords,background_color='lightgrey',colormap='viridis', width=800, height=600).generate(Text)plt.figure(figsize=(10,7), frameon=True)plt.imshow(fig_wordcloud) plt.axis('off')plt.title(title, fontsize=20 )plt.show()show_wordcloud(data_ham, "Ham messages")show_wordcloud(data_spam, "Spam messages")## 特征工程
#添加列:字符数、单词数和句子数# data["No_of_Characters"] = data["Text"].apply(len)
# 字符数前面做词云的时候已经加了,遂注释掉data["No_of_Words"]=data.apply(lambda row: nltk.word_tokenize(row["Text"]), axis=1).apply(len)
# 创建新列"No_of_Words",apply 将一个函数应用到 DataFrame 的每一行(axis=1),定义了一个匿名函数lambda接受行作为输入
# 使用 word_tokenize 函数对该行中的 Text 列进行分词,返回一个单词列表,使用 apply(len) 计算分词结果的长度,即单词的数量data["No_of_sentence"]=data.apply(lambda row: nltk.sent_tokenize(row["Text"]), axis=1).apply(len)
# sent_tokenize 函数返回的是句子列表data.describe().T # .T 是 DataFrame 的转置操作,将行和列互换plt.figure(figsize=(12,8))
fg = sns.pairplot(data=data, hue="Target")
plt.show(fg)
3. 数据准备-2阶段
如果是数据型数据的话,这一步应该在第一步的数据准备里的,但是由于本篇代码属于自然语言处理,数据清洗需要放置于数据探索流程之后。其他的数据准备工作也相对来说复杂一些。
- 数据清洗:本例中,需要删除标点符号和数字,将所有字母改为小写。由于本篇目的为判断文本的属性,所以着重分析的是实词的感情色彩,数量含义可忽略。
- 文本分解:“token” 是指文本中的一个基本单位,文本通常需要转换成数字表示才能作为输入传递给算法进行处理。将文本句子分解为 token,方便后续将文本转换为数字化的向量,便于读取。
- 删除停用词:停用词是经常出现的单词(例如 Few、is、an 等)。这些词在句子结构中有意义,但对 NLP 中的语言处理贡献不大。为了消除处理中的冗余,需要将删除这些内容。标准选用 NLTK 库内的默认停用词组。
- 词形还原:词干提取是获取单词词根形式的过程。单词的词干是通过删除单词的前缀或后缀而创建的,词干提取会将其带回词根。词形还原还将单词转换为其词根形式。
# 查看数据
print("\033[1m\u001b[45;1m The First 5 Texts:\033[0m",*data["Text"][:5], sep = "\n")# 编写数据清理函数
def Clean(Text):sms = re.sub('[^a-zA-Z]', ' ', Text) #Replacing all non-alphabetic characters with a spacesms = sms.lower() #converting to lowecasesms = sms.split()sms = ' '.join(sms)return smsdata["Clean_Text"] = data["Text"].apply(Clean)# 查看清理后数据
print("\033[1m\u001b[45;1m The First 5 Texts after cleaning:\033[0m",*data["Clean_Text"][:5], sep = "\n")data["Tokenize_Text"]=data.apply(lambda row: nltk.word_tokenize(row["Clean_Text"]), axis=1)print("\033[1m\u001b[45;1m The First 5 Texts after Tokenizing:\033[0m",*data["Tokenize_Text"][:5], sep = "\n")# 删除停用词
def remove_stopwords(text):stop_words = set(stopwords.words("english"))filtered_text = [word for word in text if word not in stop_words]return filtered_textdata["Nostopword_Text"] = data["Tokenize_Text"].apply(remove_stopwords)print("\033[1m\u001b[45;1m The First 5 Texts after removing the stopwords:\033[0m",*data["Nostopword_Text"][:5], sep = "\n")lemmatizer = WordNetLemmatizer()def lemmatize_word(text):#word_tokens = word_tokenize(text)# provide context i.e. part-of-speechlemmas = [lemmatizer.lemmatize(word, pos ='v') for word in text]return lemmasdata["Lemmatized_Text"] = data["Nostopword_Text"].apply(lemmatize_word)
print("\033[1m\u001b[45;1m The First 5 Texts after lemitization:\033[0m",*data["Lemmatized_Text"][:5], sep = "\n")
4. 矢量化(向量化)
在 NLP 中,清理后的数据需要转换为数字格式,其中每个单词都由一个矩阵表示。这也称为词嵌入或词向量化。
- 创建词形还原文本语料库
- 将语料库转换为向量形式
- 对 Target 中的类进行标签编码
#词频 (TF) = (文档中某个词的频率)/(文档中词的总数)
#逆文档频率 (IDF) = log( (文档总数)/(包含词 t 的文档数)
#I将使用 TfidfVectorizer() 对预处理后的数据进行矢量化。#创建文本特征语料库
corpus= []
for i in data["Lemmatized_Text"]:msg = ' '.join([row for row in i])corpus.append(msg)corpus[:5]
print("\033[1m\u001b[45;1m The First 5 lines in corpus :\033[0m",*corpus[:5], sep = "\n")# 矢量化
tfidf = TfidfVectorizer()
X = tfidf.fit_transform(corpus).toarray()
# 查看特征
X.dtype# 对目标进行标签编码并将其用作 y
label_encoder = LabelEncoder()
data["Target"] = label_encoder.fit_transform(data["Target"])
5. 建立模型
本文使用了 4 种不同的模型分别对数据集进行分类,均是从 sklearn 中提取的已经封装好的模型,故可以直接构建函数,实现分别训练和交叉验证,并得到正确率。
- 将特征和目标设置为 X 和 y,拆分测试集和训练集
- 为四个不同的分类构建模型:朴素贝叶斯(NB);K邻居分类器(KNN);支持向量机(SVC);随机森林
- 将所有模型拟合到训练数据集上
- 对所有模型的训练集进行交叉验证以确保准确性
y = data["Target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# 将他们投入不同分类模型
classifiers = [MultinomialNB(), RandomForestClassifier(),KNeighborsClassifier(), SVC()]
for cls in classifiers:cls.fit(X_train, y_train)# 创建模型的对应字典,便于后续的函数读取
pipe_dict = {0: "NaiveBayes", 1: "RandomForest", 2: "KNeighbours",3: "SVC"}# 交叉验证
for i, model in enumerate(classifiers):cv_score = cross_val_score(model, X_train,y_train,scoring="accuracy", cv=10)print("%s: %f " % (pipe_dict[i], cv_score.mean()))
6. 模型评价
对训练好的模型进行性能评估,以了解模型在未见过的新数据上的表现。本文中使用的评价方法为:生成准确性报告以及混淆矩阵。
- 准确性报告:列出表格,从 准确率、召回率、F1 分数、测试集准确率、训练集准确率 5个维度得出数据结论,便于一目了然地纵向对比。
- 混淆矩阵:比较模型时,TN和TP对角线两数值越大,FP和FN对角线两数值越小,模型性能越好。分类越准确混淆矩阵分为四块,分别为:
名称(位置) | 含义 |
TN(左上) | 非垃圾短信被正确地预测为非垃圾短信 |
FP(右上) | 非垃圾短信被错误地预测为垃圾短信(误报) |
FN(左下) | 垃圾短信被错误地预测为非垃圾短信(漏报) |
TP(右下) | 垃圾短信被正确地预测为垃圾短信 |
precision =[]
recall =[]
f1_score = []
trainset_accuracy = []
testset_accuracy = []for i in classifiers:pred_train = i.predict(X_train)pred_test = i.predict(X_test)prec = metrics.precision_score(y_test, pred_test)recal = metrics.recall_score(y_test, pred_test)f1_s = metrics.f1_score(y_test, pred_test)train_accuracy = model.score(X_train,y_train)test_accuracy = model.score(X_test,y_test)#评分precision.append(prec)recall.append(recal)f1_score.append(f1_s)trainset_accuracy.append(train_accuracy)testset_accuracy.append(test_accuracy)# 初始化一个列表
data = {'Precision':precision,
'Recall':recall,
'F1score':f1_score,
'Accuracy on Testset':testset_accuracy,
'Accuracy on Trainset':trainset_accuracy}
# 创建表格
Results = pd.DataFrame(data, index =["NaiveBayes", "RandomForest", "KNeighbours","SVC"])#在同一图中绘制多个分类模型的混淆矩阵,以便比较分类模型的性能
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12,8))for cls, ax in zip(classifiers, axes.flatten()):plot_confusion_matrix(cls, X_test, y_test, ax=ax)ax.title.set_text(type(cls).__name__)
plt.tight_layout()
plt.show()
模型评价
结合代码生成的准确性报告,以及混淆矩阵,对本文使用的四种模型进行评价分析。
1. 分别评价
-
朴素贝叶斯 (NB)
- 准确率(Precision): 完美(1.0)。
- Recall: 较低(0.705882),说明漏检率较高,有一定比例的垃圾短信未被识别(40个FN)。
- F1 Score: 0.827586,主要是召回率拖累了整体分数,但是依然是较高的。
- 总结: 适合不在意漏检的情况,适合需要更高精度的应用。
-
- 准确率(Precision): 完美(1.0)。
- Recall: 较高(0.823529),FN较少(24个),说明对垃圾短信识别较好。
- F1 Score: 0.903226,最高的F1分数,整体效果最佳。
- 总结: 精确率和召回率平衡良好,适合对精度和召回率要求较高的任务。
-
K 近邻分类 (KNN)
- 准确率(Precision): 0.977778,相对最低,但是依然是很高的准确率。
- Recall: 非常低(0.323529),FN较多(92个),说明漏检率非常高。
- F1 Score: 0.486188,表现较差,尤其召回率影响较大。
- 总结: 效果最差,不适合垃圾短信检测等需要较高召回率的任务。
-
支持向量机 (SVC)