中文短文本情感分析 web 应用开发记录(一)

“中文短文本情感分析 web 应用”是我带领的小组在《软件工程》这门课的大作业选题。按照这门课大作业的传统风格,就是做一个比较寻常的网站/APP,不过我这次提议另辟蹊径,一是我个人对情感分析这个方面比较感兴趣,想要借此机会进行了解和实践;二是组内成员都志在继续深造,做一个带有科研意味的项目或许能为简历添色。

先简单介绍一下我们的项目。该 web 应用可分析用户输入的中文短文本中蕴含的情感,并输出数值化结果。前端使用 Vue.js 开发,后台使用 Flask 开发。算法部分目前包含自主实现的词袋模型和引入的 SnowNLP。项目地址如下(目前服务器关了,想要尝试效果可以将项目 clone 到本地,照着 README 运行):

截止这篇博文写作时,这个项目算是成型,也在 Github 上获得了 7 个 star。但是也遇到了一些问题,当然最大的问题就是分析效果不好。效果不好的原因包括很多,后文会一一介绍,当然其中一个是我们目前的算法部分还没有涉及到深度学习,不过我正在争取在近期用 LSTM 等实现效果较好的模型。

这篇博文首先会简单介绍情感分析的概念、技术和难点。之后针对我们的项目,详细介绍我们使用的算法、迄今为止遇到的问题和一些改进措施。

什么是情感分析?

情感分析(Sentiment analysis)或意见挖掘(Opinion mining)是对人群对于产品、服务、组织、个体、问题、事件、话题等实体的意见、情感、情绪、评价、态度的计算研究。鉴于网络社交媒体的风行,我们拥有了海量的情绪化数据。情感分析已经成为自然语言处理中最有吸引力的研究领域之一,它与管理科学、社会科学领域有交集。情感分析在现实中的典型应用包括舆情分析、民意调查、产品意见调查、预测股票市场行情等等。

最典型的情感分析会分析一段文本对某个对象的情感是正面的还是负面。更细致的处理包括情感的分类(快乐、愤怒、恐惧、悲哀等)和情感的程度。意见挖掘可能还需要从文本中挖掘出对象的属性,再分析对应属性的情感。

早期的情感分析技术包括监督方法(支持向量机、最大熵、朴素贝叶斯等监督机器学习方法)和非监督方法(包括利用情感词汇、语法分析和句法模式的各种方法),而深度学习走红后,其应用到情感分析的出色效果催生大量基于深度学习的情感分析研究[1]。

中文情感分析的难点

以下是我在查阅资料后总结的一些中文情感分析的难点:

  1. 很多情感的表达是隐晦的,没有特别明显的代表性词语。例如“蓝屏”这个词一般不会出现在情感词典之中,但这个词明显表达了不满的情绪。因此需要另外根据具体领域构建针对性的情感词典。
  2. 同一个词在不同语境或领域里有不同的意思,可能表达不同的情感态度,例如“我家洗衣机声音很大”这些很可能是差评,而“我家音响声音很大”很可能就是好评。反讽的表达同理。
  3. 网络流行语等新词也会影响情感分析,比如“给力”、“不明觉厉”、“累觉不爱”等,这些词利用传统的分词一般都会被切开,而且会影响词性标注,如果想避免只能加入人工干预,修改分词的粒度和词性标注的结果。
  4. 以上三条在做英语情感分析时也会遇到。但是中文独有的问题是缺少高质量的有标注数据集。相比起来,英文情感分析有公认的数据集 IMDB。此外,可能还需要一些高质量的情感词典,英文有 SentiWordNet,但是中文的词典资源质量不高,不细致,另外缺乏主客观词典。当然,因为有监督的机器学习方法的风行,现在情感分析也不一定非要情感词典。

因为我们的项目主要还是拿文本分类的方法进行处理,所以以上问题不是都遇到了,仅停留在了解层面,如有谬误欢迎指出。

初步尝试

数据集

首先谈谈数据。根据前文所述,中文的有标注数据集很难找。我前两周和做过类似工作的学长交流时,他的做法是自行标注。而我们选择的做法是自己用 Python 编写爬虫爬取各类含评论的网站,将评论正文作为数据,评论的星级作为标签:

我们最终爬取了豆瓣的 146856 条电影评论和蜂窝网的 174055 条酒店评论作为我们的数据集。你可以在 sentiment-analysis-webapp/training/ 找到这两份数据以及训练模型的代码。

我们希望最终给出的结果也能反应情感的程度,而不是单纯的正负二元分类。因此,我们没有对标签做后续处理。

算法部分实现过程

分词

既然是使用监督机器学习算法,那么自然需要特征。计算机是看不懂自然语言的,从文本中提取特征最终的结果一般是将文本转换为向量,而这一步不管是使用什么算法,都需要首先对文本进行分词。

成熟的中文分词工具还是比较多的,包括 FudanNLP、结巴分词、THULAC、Stanford CoreNLP。其中 Stanford CoreNLP 是重量级工具,需要自备数据集、数据字典等,不太方便;另外三个工具经过简单的比较后,我们最终选用了结巴分词,因为确实很容易使用,分词效果也很好。

分词的相关代码:

1
2
3
4
5
6
import jieba

def chinese_word_cut(mytext):
return " ".join(jieba.cut(mytext))

X['cutted_comment']=X.comment.apply(chinese_word_cut)

向量化

我们的算法都是使用 scikit-learn 实现的,其中从文本中提取特征的方法在sklearn.feature_extraction.text包中。

比较典型的自然语言向量化方法有词袋模型(带或不带 TF-IDF)、词嵌入等等,在这里可以简单的介绍一下。

词袋模型

词袋模型(bag of words)是信息检索领域常用的文档表示方法。其前提是不考虑词语的出现顺序,也不考虑词语和前后词语之间的连接。文档中每个单词的出现都是独立的,不依赖于其它单词是否出现。这样,每个句子被编码成一个 $R^{|V| \times 1}$向量,其中 $|V|$是语料库中所有单词的数量。所有单词组成一个词汇表,每个句子向量的第 $i$ 位即是词汇表中第 $i$ 个词在句子中出现的次数。

词袋模型可以以单个词语做单位,也可以以 n 个连续的单词做单位(被称为 n-gram)。词袋模型的优点是原理简单,容易实现;其缺点是文本中单词顺序和上下文所蕴含的信息被完全丢弃。

词袋模型的相关代码如下:

1
2
3
4
5
6
7
from sklearn.feature_extraction.text import CountVectorizer

max_df=0.8 #在超过这一比例的文档中出现的关键词过于平凡,去除掉
min_df=3 #在低于这一数量的文档中出现的关键词过于独特,去除掉

vect = CountVectorizer(max_df=max_df, min_df=min_df, token_pattern=u'(?u)\\b[^\\d\\W]\\w+\\b')
term_matrix = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())
TF-IDF

TF-IDF 是在词袋模型的基础上度量词语重要性的指标。其基于基本假设:对区别文档最有意义的词语应该是那些在文档中出现频率高,而在整个文档集合的其他文档中出现频率少的词语,所以如果特征空间坐标系取TF词频作为测度,就可以体现同类文本的特点

根据以上思想,我们通过 TF-IDF 这个指标来寻找重要的词语。TF(Term Frequency)指词项频率,用于计算该词描述文档内容的能力;IDF(Inverse Document Frequency)指逆文档频率,用于计算该词区分文档的能力。TF-IDF 通过以下方法计算:假定文档集中有 $N$ 篇文档,$f_{ij}$ 为词项 $i$ 在文档 $j$ 中出现的频率(即次数),则词项 $i$ 在文档 $j$ 中的词项频率 $TF_{ij}$ 定义为

$$TF_{ij} = \frac{f_{ij}}{max_kf_{kj}}$$

这里有一个归一化的操作,通过除以同一文档中出现最多的词项(可能不考虑停用词的频率)的频率来计算。

假定词项 $i$ 在文档集的 $n_i$ 篇文档中出现,那么词项 $i$ 的 IDF 定义如下:

$$IDF_i = log_2{\frac{N}{n_i}}$$

于是词项 $i$ 在文档中的得分被定义为 $TF_{ij} \times IDF_i$。相关代码如下(其实就是将sklearn.feature_extraction.text提供的CountVectorizer换为TfidfVectorizer):

1
2
3
4
5
6
7
from sklearn.feature_extraction.text import TfidfVectorizer

max_df=0.8 #在超过这一比例的文档中出现的关键词过于平凡,去除掉
min_df=3 #在低于这一数量的文档中出现的关键词过于独特,去除掉

vect = TfidfVectorizer(max_df=max_df, min_df=min_df, token_pattern=u'(?u)\\b[^\\d\\W]\\w+\\b')
term_matrix = pd.DataFrame(vect.fit_transform(X_train.cutted_comment).toarray(), columns=vect.get_feature_names())

TF-IDF 在通用情况下效果一般不错,但是也存在一些问题:(1)单纯地认为文本频数小的单词就越重要,文本频数大的单词就越无用;(2)没有体现出单词的位置信息(例如在 Web 文档中处于网页不同位置或标签中的词应该有不同的权重)。

词嵌入

词嵌入(Word Embedding)是指把一个维数为所有词的数量的高维空间(one-hot 形式表示的词)“嵌入”到一个维数低得多的连续向量空间中,每个单词或词组被映射为实数域上的向量。因为我们暂时还没用到相关技术,所以不对学习词嵌入的方法进行介绍了(可能会在下一篇博文谈论)。想要了解更多可以看自然语言处理与词嵌入 - 吴恩达《深度学习》系列课程笔记

停用词

谈一下停用词的问题。无论是处理中文还是英文,都需要处理停用词。停用词指那些用于辅助表达但本身不携带任何含义的词,比如英文中的定冠词“the”。这些词经常在文本中大量出现,如果将其和那些包含信息更丰富的词汇放在一起统计,就容易对我们把握文本的特征形成干扰。所以在文本分类中,一般要首先在文本数据中去掉这些停用词。

scikit-learn 中自带了英文停用词。如果想做中文停用词的处理,可以用一些机构开源的停用词表。哈工大、四川大学、百度都有类似的词表,可以在这里找到。

不过一个值得讨论的问题是,我们做的是情感分析工作,而非单纯的文本分类。实际上,很多语气词和标点等都是停用词,但是却对句子的情感表达有着很大的影响。因此,一般在做情感分析工作时,倾向于不去处理停用词。我们会在后文的效果展示中讨论是否去除停用词对我们实际的模型效果的影响。

还是放一下处理停用词的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_custom_stopwords(stop_words_file):
with open(stop_words_file) as f:
stopwords = f.read()
stopwords_list = stopwords.split('\n')
custom_stopwords_list = [i for i in stopwords_list]
return custom_stopwords_list

stop_words_file = "stopwords.dat" # 停用词表文件名
stopwords = get_custom_stopwords(stop_words_file)

# 注意这里增加了一个选项
vect = CountVectorizer(max_df = max_df,
min_df = min_df,
token_pattern=u'(?u)\\b[^\\d\\W]\\w+\\b',
stop_words=frozenset(stopwords))

分类器

我们初期使用的分类算法是朴素贝叶斯,相关代码如下:

1
2
3
from sklearn.naive_bayes import MultinomialNB
nb = MultinomialNB()
nb.fit(X_train.cutted_comment, y_train)

管道和模型持久化

scikit-learn 提供管道功能,可以将处理流程中的工作顺序连接,隐藏其中的功能,从外部一次调用,就能完成顺序定义的全部工作。这样做可以简化调用过程,减少出错几率。

同时,scikit-learn 也支持模型持久化,即将训练好的模型保存到硬盘,待后台程序启动时可以直接加载到内存。这样,就不必每次启动后台程序时都要花费大量时间和资源进行训练了。相关代码如下:

1
2
3
4
5
6
from sklearn.pipeline import make_pipeline
from sklearn.externals import joblib
pipe = make_pipeline(vect, nb)
pipe.fit(X_train.cutted_comment, y_train)
y_pred=pipe.predict(X_test.cutted_comment)
joblib.dump(pipe, "hotel_comment.pkl")

效果展示

为了量化模型效果,需要模型实际预测输出和样本真是输出之间的差异(即误差)。因为过拟合的存在,训练误差不适合作为模型选择标准,因此需要从数据集中划分出与训练集互斥的测试集,以测试误差作为泛化误差的近似。

这里,我们选用交叉验证法(cross-validation),这是一种十分常用的模型评估方法。k 折交叉验证(k-fold CV)指划分 k 个互斥子集,每次用 k-1 个子集作训练集,余下一个子集作测试集,进行 k 次训练与测试并取均值。其优点是结果准确,Kaggle 的参赛者大多会提交 CV 分最高的模型,而非在官方提供的测试集上得分最高的模型;缺点是计算量比较大。

常用的 k 值包括 5、10、20。这里我们统一用 5 折交叉验证来分析各种搭配得到的模型效果。其中所使用的停用词为这份

对于豆瓣数据集,各种模型的评分如下:

  • CountVectorizer + 不去除停用词:0.505020743656528
  • CountVectorizer + 去除停用词:0.4937080095876466
  • TfidfVectorizer + 不去除停用词:0.49594151903667905

而对于酒店数据集,各种模型的评分如下:

  • CountVectorizer + 不去除停用词:0.5133750057452774
  • CountVectorizer + 去除停用词:0.5060018311093455
  • TfidfVectorizer + 不去除停用词:0.5297950445100629

可以看到,去除停用词的效果普遍比不去除停用词要差。而词袋模型和 TF-IDF 的效果比较结果则与数据集有关,这也说明需要针对具体问题选择合适的学习算法。

问题和改进措施

从之前 CV 的分数,可以判断我们模型的正确率大概在 50% 左右,相比起在 5 种给分中随机预测的 20% 看似要好很多,但实际表现为不管输入的是正面还是负面情感句子、程度如何,基本上都给 5 分,偶尔会给 4 分。

样本类别不均衡

从模型经常给出高分预测的表现,我们很容易做出样本中正负类别样本不均衡的判断。实际上,我们的酒店评论数据集中,有 148939 条标签值大于 3,而只有 6895 条标签值小于 3,比例达到 21.6 : 1。豆瓣数据集情况略好,但正负面评论数比例也达到了 5.69 : 1。数据集中样本类别不均衡导致训练出的模型偏向于给出正面预测,以减小损失函数。

处理样本不均衡的方式包括以下几种:

  • 尝试其他评价指标:当样本类别不均衡时,准确度这个指标不但没有说服力,反而会对分类器的好坏产生误导。这时,精确率(Precision)召回率(Recall)以及 F1 得分(F1 Score,精确率与召回率的调和平均)作为评价指标会更加有效。

  • 对数据集进行过采样和欠采样

    • 过采样(over-sampling)指采样个数大于该类样本个数,适用于小类的数据样本。具体表现为添加部分样本的副本;
    • 欠采样(under-sampling)指采样个数小于该类样本个数,适用于大类的数据样本。具体表现为删除部分样本。
  • 数据增强:数据增强是 CV 中经常使用的方式,联系到 NLP 领域,可以将文本进行句子顺序打乱、句内词序打乱、同义词替换等操作来增加相应类别的样本量。

模型评估

使用 scikit-learn 提供的方法,我们来计算一下在原始酒店数据集上用 TfidfVectorizer + 不去除停用词训练出来的模型的精确率、召回率和 F1 得分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from sklearn import metrics
metrics.accuracy_score(y_test, y_pred)
'''
0.5307027623293653
'''
metrics.confusion_matrix(y_test, y_pred)
'''
array([[ 0, 0, 151, 237, 27],
[ 0, 0, 227, 1017, 106],
[ 0, 0, 530, 3603, 507],
[ 0, 1, 327, 12089, 6894],
[ 0, 0, 58, 7266, 10474]])
'''
metrics.precision_score(y_test, y_pred, average='macro')
'''
0.29816554278872476
'''
metrics.recall_score(y_test, y_pred, average='micro')
'''
0.5307027623293653
'''
metrics.f1_score(y_test, y_pred, average='weighted')
'''
0.5048778200537771
'''

可以看到精确率相对来说比较低。

模型选择

解决了样本类别不均衡以及模型评估的问题,我们很容易想到,我们一开始采用的朴素贝叶斯是否是在这个问题上表现最好的机器学习模型?SVM、随机森林或者 Kaggle 前任大杀器 XGBoost 会不会有更出色的结果?

To be continued…

特征选择

文本分类中常见的特征选择方法除开 TF-IDF 外,还包括信息增益、互信息、期望交叉熵、CHI 统计等[2]。根据清华大学刘知远老师在博士阶段的研究,TF-IDF 具有较强的普适性,能够满足绝大部分的中文场景下的需求。不过也有其他研究工作表明使用信息增益的性能表现也很优秀。由于我们之后会使用深度学习模型在做端到端的学习,并且特征选择不是目前的瓶颈,因此这里不进行深入研究。

结语

通过这个项目,我感觉我确实很喜欢情感分析这个研究方向。我也因此承担了绝大部分的算法研究开发工作,查阅了很多文献和网络资料,由此对情感分析研究现状乃至自然语言处理的一些基础知识有了比较深入的了解,可谓受益匪浅。

比较可惜的是,我们在课程作业结题时实现的版本还不尽人意,而且因为事务忙碌和自身安排原因,深度学习模型计划了很久都没有动手实现。写这篇博文一是对现有工作做一个总结,同时有些算法写的比较详细,巩固一下所学知识;二就是督促自己尽早实现基于深度学习的模型,届时也会出该系列的第二篇博文。也欢迎看到这里的读者在 Github 和这里继续关注我们的工作。

参考资料

引用文献

参考书籍

  • 《互联网大规模数据挖掘与分布式处理》 1.3.1

网络资料