Kaggle⾃然语⾔处理⼊门之推特灾难⽂本分类
董卿是小三前⾔
第⼀次处理⽂本的时候还是做毕业论⽂时,那个时候什么也不懂,为了⽅便,⽤EXCEl进⾏了⽂本的预处理,然后⽤在线词云进⾏了画图和分词,导出的结果⽤ROST-CM做了情感分析和社会⽹络分析,最后⽤Gephi做了⼀个图,留下了不⼩的遗憾,好像⾮计算机专业的我就是⼊不了这个门。所以在Kaggle⼊门的路上看见NLP相关的东西总是有点忐忑,但是好奇⼼还是驱使我点开了这个数据集,这也成了我Kaggle⼊门以来花的时间最多的⼀个项⽬。
数据集描述
该任务全名为Natural Language Processing with Disaster Tweets。它的数据集与其他的kaggle数据集⼀样,分为
train.csv,test.csv, samle_submission.csv三个⽂件。训练集⽂件中包含了7613条数据,由id列、关键词列、地点列、⽂字内容列、标签列组成。其中地点列空缺较多,⼤概不能提取出什么信息,关键词列表⽰⽂字内容列的关键词是什么,⽐如:
这⼀条推⽂说Aba这个地⽅发⽣了⼀场⽕灾,这个ablaze就是指着⽕的意思,它是整条内容的关键词,表⽰这条推特是灾害发⽣类的推特,因此它被标记为1。当然同时这个词语也可以成为“把⼈惹⽕”中的关键词,这种就是灾害⽆关的推特,它应该被标记为0。所以我们的任务就是建⽴⼀个模型,能够把真正发⽣灾害的推特出来标记为1,区别出那些⽆关灾害的推特标记为0。 ⽽test.csv⽂件中就有3263条这样的推特等待我们标记。
此外,被标记0的数据有4000多条,⽽标记1的数据有3000多条,这也就预⽰着可能我们的分类器会往更保守的⽅向进⾏预测。
数据预处理
周末短信祝福
数据预处理是⾄关重要的部分,现在回头思考才发现,其实什么样的输⼊决定了你应该进⾏什么样的预处理。什么意思呢?⾸先我们肯定要进⾏⽂本清洗,这⾥举例⼀些有问题的⽂本内容:
@sunkxssedharry will you wear shorts for race ablaze ?
学生票高铁打几折
这样的⽂本让⼈觉得头⼤,我们可能会理所当然地把@xxxx,#xxxxx,httpxxxxxx,以及没有列在上⾯的&xxxx和这个乱码聣脹x都去掉。但是,在经过试验之后会发现,其实这些我们看来的噪声在机器那边可能也是有价值的可以学习的特征。
事情是这样的,最开始的时候我试了kaggle官⽹的⽰例教程,它在不做数据清洗的情况下,直接⽤sklearn的CountVectorizer转换器把所有的⽂字变成了0,1编码,这样得到3万多不同的特征,做了⼀个岭回归,最后得到的分数有0.78057,我觉得这个教程它有问题好吧,结果在我经过⼀番数据清洗建⽴tf-idf的向量表⽰之后,再喂给分类器的得分竟然还没有它⾼! 原因其实有两个,⼀个就是⼀开始我没有注意这个任务要求评估的是F1分数,即精确率和召回率的综合估计,另外⼀个就是特征的缩减!我清洗后形成的tf-idf向量只有1万2千的特征,在我不断的放宽标准到1万4千的特征的时候,我的XGBoost分类器的分数终于超过了教程。。。有的时候特征的数量真的是可以为所欲为的。
以上为发牢骚,本例我试了三种数据处理⽅法,
第⼀种是td-idf转换,⾸先得对⽂本进⾏简单清洗和停⽤词处理,再分词合并,转换为td-idf向量,即该词在语句出现的频率和在整篇语料钟出现频率的综合指标,然后把它喂给连续的分类器(预测结果为概率值,如xgboost和lightgbm)(分数0.78302)。
第⼆种就是CountVectorizer,只对⽂本进⾏简单清洗,不进⾏去除停⽤词等操作,再喂给朴素贝叶斯分类器中的BernoulliNB(分数
0.79681)。
第三种同时也是效果最棒的⼀种⽅法就是⽤⾕歌预训练好的BERT模型和配套分词py⽂件进⾏深度学习(分数0.84186)。
可以看出不同的模型需要的数据预处理⽅法不尽相同,想要万能的⽅案不太可能,只能⾃⼰⼀种种尝试过才知道优劣。
理想⽅案
这⾥我只介绍第⼆种⽅案,第三种不在我们的考虑之内,尽管我⽤Kaggle的背景板做了,因为深度学习真的很吃显卡和时间,⽽这个BERT 模型包含了335,142,913个训练参数,我的可怜巴巴的1660ti根本跑不起来,⽤kaggle背景板虽然能跑,但整个训练过程(我只训练了三次模型)花了我⼤概20分钟,
别说调整具体参数了,虽然它的效果确实是最好的,我跑出来的成绩到达了101名,排除前⾯那些作弊的⼤概是极限成绩了。
第⼆种朴素贝叶斯的⽅法可以算作常态⽅法下的极限成绩(排除深度学习和特殊特征构建)。
那么和我之前的⽂章⼀样,我把可执⾏的源代码完整的贴出来,⾸先是简单的⽂本清洗:
import numpy as np
import pandas as pd
import re
#读取数据集
train_df = pd.read_csv("/newstart/code/NLP/train.csv")
test_df = pd.read_csv("/newstart/code/NLP/test.csv")
#写清洗函数
def clean_text(rawText):
rawText[:] = [re.sub(r'https?:\/\/.*\/\w*', 'URL', text) for text in rawText]
rawText[:] = [re.sub(r'@\w+([-.]\w+)*', '', text) for text in rawText]
rawText[:] = [re.sub(r'&\w+([-.]\w+)*', '', text) for text in rawText]
#调⽤函数就地⼯作
clean_text(train_df['text'])
clean_text(test_df['text'])黄子佼女友
这⾥把htttp转化为了URL统⼀标识,去掉了@和&这两个相对于#来说没有价值的噪⾳,除了在测试中发现这条规律,其实在数据探索初期也可以发现,@后跟的都是⽤户名,&后跟的⼤部分都是amp,⽽#号后⾯可能还会跟⼀些关键词。所以这⾥我们保留了#xxxx这个相对有⽤的特征。
from sklearn import feature_extraction,model_selection,preprocessing
count_vectorizer = CountVectorizer()      #创建转换器
example_train_vectors = count_vectorizer.fit_transform(train_df["text"][0:5]) #构建⽰例转换向量
print(example_train_vectors[0].todense().shape)  #打印出⽰例的维度
print(example_train_vectors[0].todense())            #打印出⽰例向量具体内容
train_vectors = count_vectorizer.fit_transform(train_df["text"])  #进⾏训练集⽂本的拟合和转换
test_vectors = ansform(test_df["text"])      #进⾏测试集⽂本的转换
运⾏结果如下:
(1, 54)
[[0 0 0 1 1 1 0 0 0 0 0 0 1 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0
0 0 0 1 0 0 0 0 0 0 0 0 0 1 1 0 1 0]]
可以看到前五⾏⽂本内容总共有54个单词,第⼀⾏已经被转换成了54个词的表⽰形式,0代表这个词语没有出现,1代表这个词语有出现,第⼀⾏总共有13个1,也就是13个词构成的⽂本。另外需要说明的是之所以测试集上只⽤了transform⽽不是fit_transform是因为我们假设训练集和测试集分布相同。
del_selection import train_test_split
ics import accuracy_score
from sklearn.naive_bayes import BernoulliNB
y_label = train_df['target']  #提取标签
X_train, X_test, y_train, y_test = train_test_split(train_vectors, y_label, test_size=0.1, random_state=7) #划分训练集和测试集
clf = BernoulliNB() #构建朴素贝叶斯分类器
clf.fit(train_vectors,y_label)  #拟合模型
这⾥⽤BernoulliNB贝叶斯分类器⽽不⽤另外两种,⾸先因为它是离散值,所以不能⽤GaussianNB,其次MultinomialNB的结果稍差于BernoulliNB。
import sklearn
y_pred = clf.predict(X_test) #进⾏⾃⼰划分的验证集上的预测
acc = accuracy_score(y_test, y_pred) #准确率
print("Accuracy: %.2f%%" % (acc * 100.0))
f1 = ics.f1_score(y_test, y_pred) #F1分数
print("f1_score: %.2f%%" % (f1 * 100.0))
英语b级考试技巧from  ics import classification_report
print(classification_report(y_test, y_pred)) #形成报表
结果如下:
可以看到我们在1分类上的召回率和F1分数都⽐较低,前⾯也说过因为1分类较少,导致我们分类器可能做出保守判断,没有把全部的“犯⼈”都抓出来。此外这⾥的表现虽然⽐MultinomialNB⾼了九个百分点,但提交到kaggle结果⾥就差了0.0⼏,说明kaggle的测试集上其实不是很符合这个伯努利分布。
import seaborn as sns
ics import accuracy_score, confusion_matrix
import matplotlib.pyplot as plt
conf_mat = confusion_matrix(y_test, y_pred) #构建混淆矩阵
fig, ax = plt.subplots(figsize=(10,8))
sns.heatmap(conf_mat,cmap="Oranges") #画热⼒图,颜⾊为orange
plt.ylabel('actually',fontsize=18) #纵轴为实际上的类别
plt.xlabel('predict',fontsize=18) #横轴为预测的类别
由于我们这⾥是⼆分类矩阵,所以画这样⼀个图好像没啥意义,但多分类任务就会看的⽐较清楚⽐如哪类和哪类混淆了导致判断错误⽐较多。
高考第二天祝福语
sample_submission = pd.read_csv("/newstart/code/NLP/sample_submission.csv")
Y = clf.predict(test_vectors)
sample_submission["target"] = Y
sample_submission.head()
_csv("submission17.csv", index=False)
最后还是⼀样的⽣成csv⽂件提交,这⾥利⽤现成的直接改⼀列⽐之前新建⼀个要⽅便不少。
总结
在这次分类中呢,我们只是尽可能利⽤了训练集中包含的⽂本内容列,没有对关键词和地点列进⾏深挖,也没有做情感分析和主题建模,可能还有进⼀步挖掘的空间吧。另外在探索的时候发现,停⽤词库的选择其实也会影响到最后的结果,⽐如genism它将fire加⼊了停⽤词库,⽽nltk的停⽤词库没有把它包含进来。还有深度学习这个东西是真的很吃硬件啊!特别是LSTM模型训练的太慢了。。最后,很多东西不能只觉得它是什么样就是什么样,要试过才知道,有时候你认为的噪声反⽽是有⽤的信息呢。