NLP之文本分类实战入门超详细教程
前言该实战任务是对豆瓣评分的预测。在这个项目中,我们通过豆瓣评论预测该电影的评分。给定的输入为一段文本,输出为具体的评分。实际上就是一个文本分类任务。在这个项目中,我们需要做:文本的预处理,如停用词的过滤,低频词的过滤,特殊符号的过滤等文本转化成向量,将使用三种方式,分别为tf-idf, word2vec以及BERT向量。训练逻辑回归和朴素贝叶斯模型,并做交叉验证评估模型的准确率一、数据加载1.加
目录
前言
该实战任务是对豆瓣评分的预测。在这个项目中,我们通过豆瓣评论预测该电影的评分。给定的输入为一段文本,输出为具体的评分。实际上就是一个文本分类任务。在这个项目中,我们需要做:
- 文本的预处理,如停用词的过滤,低频词的过滤,特殊符号的过滤等
- 文本转化成向量,将使用三种方式,分别为tf-idf, word2vec以及BERT向量。
- 逻辑回归模型训练,并做交叉验证
- 评估模型的准确率
一、数据加载
1.加载包
首先是加载库,具体这些库函数的作用会在下文使用到的时候说明。
#导入数据处理的基础包
import numpy as np
import pandas as pd
#导入用于计数的包
from collections import Counter
#导入tf-idf相关的包
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
#导入模型评估的包
from sklearn import metrics
#导入与word2vec相关的包
from gensim.models import KeyedVectors
#导入与bert embedding相关的包,关于mxnet包下载的注意事项参考实验手册
from bert_embedding import BertEmbedding
import mxnet
#包tqdm是用来对可迭代对象执行时生成一个进度条用以监视程序运行过程
from tqdm import tqdm
#导入其他一些功能包
import requests
import os
2.读取数据
接下来是通过pd.read_csv函数读取数据,该函数是用来读取csv格式的文件,将表格数据转化成dataframe格式。由于我们仅需要评论和评分这两列,所以通过索引取出对应的数据。
#读取数据
data = pd.read_csv('data/DMSC.csv')
#观察数据格式
data.head()
#输出数据的一些相关信息
data.info()
#只保留数据中我们需要的两列:Comment列和Star列
data = data[['Comment','Star']]
#观察新的数据的格式
data.head()
输出结果:
ID | Movie_Name_EN | Movie_Name_CN | Crawl_Date | Number | Username | Date | Star | Comment | Like | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | Avengers Age of Ultron | 复仇者联盟2 | 2017-01-22 | 1 | 然潘 | 2015-05-13 | 3 | 连奥创都知道整容要去韩国。 | 2404 |
1 | 10 | Avengers Age of Ultron | 复仇者联盟2 | 2017-01-22 | 11 | 影志 | 2015-04-30 | 4 | “一个没有黑暗面的人不值得信任。” 第二部剥去冗长的铺垫,开场即高潮、一直到结束,会有人觉... | 381 |
2 | 20 | Avengers Age of Ultron | 复仇者联盟2 | 2017-01-22 | 21 | 随时流感 | 2015-04-28 | 2 | 奥创弱爆了弱爆了弱爆了啊!!!!!! | 120 |
3 | 30 | Avengers Age of Ultron | 复仇者联盟2 | 2017-01-22 | 31 | 乌鸦火堂 | 2015-05-08 | 4 | 与第一集不同,承上启下,阴郁严肃,但也不会不好看啊,除非本来就不喜欢漫威电影。场面更加宏大... | 30 |
4 | 40 | Avengers Age of Ultron | 复仇者联盟2 | 2017-01-22 | 41 | 办公室甜心 | 2015-05-10 | 5 | 看毕,我激动地对友人说,等等奥创要来毁灭台北怎么办厚,她拍了拍我肩膀,没事,反正你买了两份... | 16 |
Comment | Star | |
---|---|---|
0 | 连奥创都知道整容要去韩国。 | 3 |
1 | “一个没有黑暗面的人不值得信任。” 第二部剥去冗长的铺垫,开场即高潮、一直到结束,会有人觉... | 4 |
2 | 奥创弱爆了弱爆了弱爆了啊!!!!!! | 2 |
3 | 与第一集不同,承上启下,阴郁严肃,但也不会不好看啊,除非本来就不喜欢漫威电影。场面更加宏大... | 4 |
4 | 看毕,我激动地对友人说,等等奥创要来毁灭台北怎么办厚,她拍了拍我肩膀,没事,反正你买了两份... | 5 |
二、文本处理
1.去除无用字符
通过正则匹配,去除表情和其他字符、残留冒号和符号以及空格。
# TODO1: 去掉一些无用的字符,自行定一个字符几何,并从文本中去掉
# your to do
#去除字母数字表情和其它字符
import re
def clear_character(sentence):
pattern1='[a-zA-Z0-9]'
pattern2 = re.compile(u'[^\s1234567890::' + '\u4e00-\u9fa5]+')
pattern3='[’!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~]+'
line1=re.sub(pattern1,'',sentence) #去除英文字母和数字
line2=re.sub(pattern2,'',line1) #去除表情和其他字符
line3=re.sub(pattern3,'',line2) #去除去掉残留的冒号及其它符号
new_sentence=''.join(line3.split()) #去除空白
return new_sentence
data["comment_processed"]=data['Comment'].apply(clear_character)
data.head()
输出结果:
Comment | Star | comment_processed | |
---|---|---|---|
0 | 连奥创都知道整容要去韩国。 | 3 | 连奥创都知道整容要去韩国 |
1 | “一个没有黑暗面的人不值得信任。” 第二部剥去冗长的铺垫,开场即高潮、一直到结束,会有人觉... | 4 | 一个没有黑暗面的人不值得信任第二部剥去冗长的铺垫开场即高潮一直到结束会有人觉得只剩动作特技不... |
2 | 奥创弱爆了弱爆了弱爆了啊!!!!!! | 2 | 奥创弱爆了弱爆了弱爆了啊 |
3 | 与第一集不同,承上启下,阴郁严肃,但也不会不好看啊,除非本来就不喜欢漫威电影。场面更加宏大... | 4 | 与第一集不同承上启下阴郁严肃但也不会不好看啊除非本来就不喜欢漫威电影场面更加宏大单打与团战又... |
4 | 看毕,我激动地对友人说,等等奥创要来毁灭台北怎么办厚,她拍了拍我肩膀,没事,反正你买了两份... | 5 | 看毕我激动地对友人说等等奥创要来毁灭台北怎么办厚她拍了拍我肩膀没事反正你买了两份旅行保险惹 |
2.文本分词
通过jieba分词,对输入的文本进行分词。
# TODO2: 导入中文分词包jieba, 并用jieba对原始文本做分词
import jieba
def comment_cut(content):
# TODO: 使用结巴完成对每一个comment的分词
# seg = jieba.lcut(content)
seg = list(jieba.cut(content.strip()))
return seg
# 输出进度条
tqdm.pandas(desc='apply')
data['comment_processed'] = data['comment_processed'].progress_apply(comment_cut)
# 观察新的数据的格式
data.head()
输出结果:
Comment | Star | comment_processed | |
---|---|---|---|
0 | 连奥创都知道整容要去韩国。 | 3 | [连, 奥创, 都, 知道, 整容, 要, 去, 韩国] |
1 | “一个没有黑暗面的人不值得信任。” 第二部剥去冗长的铺垫,开场即高潮、一直到结束,会有人觉... | 4 | [一个, 没有, 黑暗面, 的, 人, 不, 值得, 信任, 第二部, 剥去, 冗长, 的,... |
2 | 奥创弱爆了弱爆了弱爆了啊!!!!!! | 2 | [奥创, 弱, 爆, 了, 弱, 爆, 了, 弱, 爆, 了, 啊] |
3 | 与第一集不同,承上启下,阴郁严肃,但也不会不好看啊,除非本来就不喜欢漫威电影。场面更加宏大... | 4 | [与, 第一集, 不同, 承上启下, 阴郁, 严肃, 但, 也, 不会, 不, 好看, 啊,... |
4 | 看毕,我激动地对友人说,等等奥创要来毁灭台北怎么办厚,她拍了拍我肩膀,没事,反正你买了两份... | 5 | [看毕, 我, 激动, 地, 对, 友人, 说, 等等, 奥创, 要, 来, 毁灭, 台北,... |
3.去除停用词
载入停用词表,并将停用词去除。
# TODO3: 设定停用词并从文本中去掉停用词
# 下载中文停用词表至data/stopWord.json中,下载地址:https://github.com/goto456/stopwords/
if not os.path.exists('data/stopWord.json'):
stopWord = requests.get("https://raw.githubusercontent.com/goto456/stopwords/master/cn_stopwords.txt")
with open("data/stopWord.json", "wb") as f:
f.write(stopWord.content)
# 读取下载的停用词表,并保存在列表中
with open("data/stopWord.json","r",encoding='utf-8') as f:
stopWords = f.read().split("\n")
# 去除停用词
def rm_stop_word(wordList):
# your code, remove stop words
# TODO
filtered_words = [word for word in wordList if word not in stopWords]
return filtered_words
#这行代码中.progress_apply()函数的作用等同于.apply()函数的作用,只是写成.progress_apply()函数才能被tqdm包监控从而输出进度条。
data['comment_processed'] = data['comment_processed'].progress_apply(rm_stop_word)
# 观察新的数据的格式
data.head()
输出结果:
Comment | Star | comment_processed | |
---|---|---|---|
0 | 连奥创都知道整容要去韩国。 | 3 | [奥创, 知道, 整容, 韩国] |
1 | “一个没有黑暗面的人不值得信任。” 第二部剥去冗长的铺垫,开场即高潮、一直到结束,会有人觉... | 4 | [一个, 没有, 黑暗面, 值得, 信任, 第二部, 剥去, 冗长, 铺垫, 开场, 高潮,... |
2 | 奥创弱爆了弱爆了弱爆了啊!!!!!! | 2 | [奥创, 弱, 爆, 弱, 爆, 弱, 爆] |
3 | 与第一集不同,承上启下,阴郁严肃,但也不会不好看啊,除非本来就不喜欢漫威电影。场面更加宏大... | 4 | [第一集, 不同, 承上启下, 阴郁, 严肃, 不会, 好看, 本来, 喜欢, 漫威, 电影... |
4 | 看毕,我激动地对友人说,等等奥创要来毁灭台北怎么办厚,她拍了拍我肩膀,没事,反正你买了两份... | 5 | [看毕, 激动, 友人, 说, 奥创, 毁灭, 台北, 厚, 拍了拍, 肩膀, 没事, 反正... |
4.去除低频词
通过pandas索引循环comment列,将所有词合并到一个列表里。然后通过Counter统计词频数,并将词频小于10的词去除。
# TODO4: 去除低频词, 去掉词频小于10的单词,并把结果存放在data['comment_processed']里
import jieba
import re
import pandas as pd
from collections import Counter
data.head()
list_set = []
for i in range(len(data)):
for j in data.iloc[i]['comment_processed']:
list_set.extend(j)
words_count = Counter(list_set)
min_threshold=10
my_dict = {k: v for k, v in words_count.items() if v < min_threshold}
filteredA = Counter(my_dict)
# 去除低频词
def rm_low_frequence_word(wordList):
# your code, remove stop words
# TODO
filtered_words = [word for word in wordList if word not in filteredA]
return filtered_words
#这行代码中.progress_apply()函数的作用等同于.apply()函数的作用,只是写成.progress_apply()函数才能被tqdm包监控从而输出进度条。
data['comment_processed'] = data['comment_processed'].progress_apply(rm_low_frequence_word)
data.head()
5.划分训练集和测试集
选择语料库中的20%作为测试数据,剩下的作为训练数据。把数据分为训练集和测试集。 comments_train(list)保存用于训练的文本,comments_test(list)保存用于测试的文本。 y_train, y_test是对应的标签(1,2,3,4,5)
from sklearn.model_selection import train_test_split
X = data['comment_processed']
y = data['Star']
test_ratio = 0.2
comments_train, comments_test, y_train, y_test = train_test_split(X, y, test_size=test_ratio, random_state=0)
print(comments_train.head(),y_train.head)
输出结果:
104861 [没, 看过, 小说, 真的, 电影, 折服, 两大, 女主, 美, 演技, 超棒, 可能,... 190626 [继宫, 老, 之后, 一个, 期待, 导演, 剧情, 画面, 没得说, 最后, 哗哗, 流... 198677 [誉, 画面, 找不出, 赞点, 43, 岁, 新海, 诚, 应该, 做出, 更, 里子, 东西] 207320 [兔子, 好萌] 106219 [希望, 最后, 我能, 活成, 样子] Name: comment_processed, dtype: object <bound method NDFrame.head of 104861 5 190626 4 198677 3 207320 5 106219 4 .. 176963 4 117952 2 173685 3 43567 5 199340 4 Name: Star, Length: 170004, dtype: int64>
三、把文本转换成向量的形式
在这个部分我们会采用三种不同的方式将文本转化为向量:
- 使用tf-idf向量
- 使用word2vec
- 使用bert向量
转换成向量之后,再进行模型的训练。
1.把文本转换成tf-idf向量
通过sklearn的feature_extraction.text.TfidfTransformer模块把训练文本和测试文本转换成tf-idf向量。由于TfidfTransformer模块输出的词列表用空格连接,而不是逗号连接,所以需要先转化下列表形式。
from sklearn.feature_extraction.text import TfidfTransformer
comments_train1 = [' '.join(i) for i in comments_train]
comments_test1 = [' '.join(i) for i in comments_test]
print(comments_train[0:5])
print(comments_train1[0:5])
tfidf2 = TfidfTransformer()
counter = CountVectorizer(analyzer='word')
# counts = counter.fit_transform(comments_train1)
tfidf_train = tfidf2.fit_transform(counter.fit_transform(comments_train1))
tfidf_test=tfidf2.transform(counter.transform(comments_test1))
print(tfidf_train.shape,tfidf_test.shape)
输出结果:
104861 [没, 看过, 小说, 真的, 电影, 折服, 两大, 女主, 美, 演技, 超棒, 可能,... 190626 [继宫, 老, 之后, 一个, 期待, 导演, 剧情, 画面, 没得说, 最后, 哗哗, 流... 198677 [誉, 画面, 找不出, 赞点, 43, 岁, 新海, 诚, 应该, 做出, 更, 里子, 东西] 207320 [兔子, 好萌] 106219 [希望, 最后, 我能, 活成, 样子] Name: comment_processed, dtype: object ['没 看过 小说 真的 电影 折服 两大 女主 美 演技 超棒 可能 一个 女孩儿 一段 会 吃醋 深爱 友谊', '继宫 老 之后 一个 期待 导演 剧情 画面 没得说 最后 哗哗 流 眼泪 略显 尴尬 完 以后 终于 明白 日本 细腻 风格 电影界 占有 重要 一席之地 以前 知道 有人 推崇 日本 电影 现在 明白 治愈 系 目前 来看 无人 能出 其右', '誉 画面 找不出 赞点 43 岁 新海 诚 应该 做出 更 里子 东西', '兔子 好萌', '希望 最后 我能 活成 样子'] (170004, 87026) (42502, 87026)
2.把文本转换成word2vec向量
由于训练出一个高效的word2vec词向量往往需要非常大的语料库与计算资源,所以我们通常不自己训练Wordvec词向量,而直接使用网上开源的已训练好的词向量。sgns.zhihu.word是从Chinese-Word-Vectors下载到的预训练好的中文词向量文件。
通过KeyedVectors.load_word2vec_format()函数加载预训练好的词向量文件。
对于每个句子,生成句子的向量。具体的做法是对包含在句子中的所有单词的向量做平均。
model = KeyedVectors.load_word2vec_format('data/sgns.zhihu.word')
model['今天']
vocabulary = model.vocab
vec_lem=model['鲁迅'].shape[0]
def comm_vec(c):
vec_com=np.zeros(vec_lem)
coun=0
for w in c:
if w in model:
vec_com+=model[w]
coun+=1
return vec_com/coun
word2vec_train=np.vstack(comments_train.progress_apply(comm_vec))
word2vec_test=np.vstack(comments_test.progress_apply(comm_vec))
3.把文本转换成bert向量
transformers是huggingface提供的预训练模型库,可以轻松调用API来得到的词向量。
接下来主要介绍如何调用transformers库生成中文词向量。使用的预训练模型是Bert_mini,一个中文文本的预训练模型。通过BertTokenizer,BertModel函数将词转化为向量。
接下来就是通过process_word()函数将得到的字向量拼成词向量,然后再通过comm_vec()函数将词向量拼成句向量。
from transformers import BertTokenizer,BertModel
import torch
import logging
# set cuda
gpu = 0
use_cuda = gpu >= 0 and torch.cuda.is_available()
print(use_cuda)
if use_cuda:
torch.cuda.set_device(gpu)
device = torch.device("cuda", gpu)
else:
device = torch.device("cpu")
logging.info("Use cuda: %s, gpu id: %d.", use_cuda, gpu)
bert_model_dir='bert-mini'
tokenizer=BertTokenizer.from_pretrained(bert_model_dir)
Bertmodel=BertModel.from_pretrained(bert_model_dir)
word=['今天我']
input_id=tokenizer(word,padding=True,truncation=True,max_length=10,return_tensors='pt')
result=Bertmodel(input_id['input_ids'])
# print(result)
vec_len=len(result[0][0][1])
# vec_len=result[0,1,0].shape[0]
print(vec_len)
def process_word(w):
vec_com=np.zeros(vec_len)
num=len(w)
input_id=tokenizer(w,padding=True,truncation=True,max_length=10,return_tensors='pt')
res=Bertmodel(input_id['input_ids'])
k=len(res[0][0])
for i in range(k):
# print(res[0][0][i].detach().numpy())
vec_com+=res[0][0][i].detach().numpy()
return vec_com/k
def comm_vec(c):
vec_com=np.zeros(vec_len)
coun=0
for w in c:
if w in model:
vec_com+=process_word(w)
coun+=1
break
return vec_com/coun
bert_train=np.vstack(comments_train.progress_apply(comm_vec))
bert_test=np.vstack(comments_test.progress_apply(comm_vec))
print (tfidf_train.shape, tfidf_test.shape)
print (word2vec_train.shape, word2vec_test.shape)
print (bert_train.shape, bert_test.shape)
输出结果:
(170004, 87026) (42502, 87026)
(170004, 300) (42502, 300)
(170004, 256) (42502, 256)
四、训练模型以及评估
对如上三种不同的向量表示法,分别训练逻辑回归模型,需要做:
- 搭建模型
- 训练模型(并做交叉验证)
- 输出最好的结果
导入逻辑回归以及交叉验证的包。
# 导入逻辑回归的包
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
1.使用tf-idf向量训练
clf=LogisticRegression()
param_grid = {
'C': [0.01,0.1, 1.0, 2.0,10,100],
'penalty' : ['l1', 'l2']
}
grid_search = GridSearchCV(estimator=clf,
param_grid=param_grid,
scoring='accuracy',
cv=5,
n_jobs=-1)
grid_search.fit(tfidf_train, y_train)
print(grid_search.best_params_)
print(grid_search.best_score_)
lr_best=LogisticRegression(penalty='l2',C=2)
lr_best.fit(tfidf_train, y_train)
tf_idf_y_pred=lr_best.predict(tfidf_test)
print('TF-IDF LR test accuracy %s' % metrics.accuracy_score(y_test, tf_idf_y_pred))
#逻辑回归模型在测试集上的F1_Score
print('TF-IDF LR test F1_score %s' % metrics.f1_score(y_test, tf_idf_y_pred,average="macro"))
输出结果:
{'C': 1.0, 'penalty': 'l2'} 0.4747594131314477
TF-IDF LR test accuracy 0.47851865794550846 TF-IDF LR test F1_score 0.43133900686271376
2.使用word2vec向量训练
clf=LogisticRegression()
param_grid = {
'C': [0.01,0.1, 1.0, 2.0,10,100],
'penalty' : ['l1', 'l2'],
'solver':['liblinear','lbfgs','sag','saga']
}
grid_search = GridSearchCV(estimator=clf,
param_grid=param_grid,
scoring='accuracy',
cv=5,
n_jobs=-1)
grid_search.fit(word2vec_train, y_train)
print(grid_search.best_params_)
print(grid_search.best_score_)
lr_best=LogisticRegression(penalty='l1',C=100,solver='saga')
lr_best.fit(word2vec_train, y_train)
word2vec_y_pred=lr_best.predict(word2vec_test)
print('Word2vec LR test accuracy %s' % metrics.accuracy_score(y_test, word2vec_y_pred))
#逻辑回归模型在测试集上的F1_Score
print('Word2vec LR test F1_score %s' % metrics.f1_score(y_test, word2vec_y_pred,average="macro"))
输出结果:
{'C': 1.0, 'penalty': 'l2', 'solver': 'lbfgs'}
0.4425013587835652
Word2vec LR test accuracy 0.4447555409157216
Word2vec LR test F1_score 0.37275840165350765
3.使用Bert向量训练
clf=LogisticRegression()
param_grid = {
'C': [0.01,0.1, 1.0, 2.0,10,100],
'penalty' : ['l1', 'l2'],
'solver':['liblinear','lbfgs','sag','saga']
}
grid_search = GridSearchCV(estimator=clf,
param_grid=param_grid,
scoring='accuracy',
cv=5,
n_jobs=-1)
grid_search.fit(bert_train, y_train)
print(grid_search.best_params_)
print(grid_search.best_score_)
输出结果:
{'C': 2.0, 'penalty': 'l2', 'solver': 'sag'} 0.4072104199357458
Bert LR test accuracy 0.4072090725142346 Bert LR test F1_score 0.3471216422860073
总结
通过不同方式获取的词向量进行训练,五分类的准确率都在百分之四十多,差距不大,可能是仅仅用到了逻辑回归模型,效果并没有多大提升。所以接下来可以从以下几个方面提升效果:
- 句子向量融合方法改进
- 解决类别不平衡问题
- 词向量模型再训练
- jieba分词效果改进
- 用深度神经网络进行分类
以上就是文本分类史上最详细入门教程,码字不易,希望大家能够多多点赞收藏,如果需要代码或模型的朋友,可以在评论区留言或者私信。
参考:
贪心学院自然语言处理
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)