前言
最近拿了一份IMDb影评数据做练习,对(英文的)自然语言处理(Natural Language Processing,NLP)有了初步的认识,同时对特征工程和Stacking有了更深的理解。
这篇文章记录一下这次练习的过程。
一些需要说明的:
- 数据来源:Sentiment Labelled Sentences Data Set - UCI ML Repository
- 建模目标:构建影评分类器,判断一份影评是正面还是负面的
- 建模结果:分类器整体准确率近0.8(基于0.56的分类阈值)
- Tutorial来源:Spooky Author Identification - Kaggle
- 分析环境:
Python 3.6
- 使用到的库:
numpy
,pandas
,matplotlib
,seaborn
,string
,nltk
,sklearn
,xgboost
数据集
官方介绍:
This dataset contains sentences labelled with positive or negative sentiment.
The sentences come from three different websites/fields:
imdb.com
amazon.com
yelp.com
For each website, there exist 500 positive and 500 negative sentences. Those were selected randomly for larger datasets of reviews.
这里我只用到了IMDb的1000条影评,它具体长这样:
每一行表示一条影评,comment列显示影评的具体内容,is_pos列显示该观众喜欢(is_pos = 1)还是不喜欢(is_pos = 0)这部电影。
is_pos是此次分析的目标特征,其中,正面和负面影评各占50%:
我们需要做的是,根据这些已有的影评数据构建一个分类模型,这个模型能够根据一份影评返回一个结果:这位观众喜欢/不喜欢这部电影。
文本特征提取
由于原始影评是非结构化的,算法不能对其直接进行学习,所以我们需要通过特征工程,提取出数据中的结构化信息,从而构建学习模型。
我基于原始文本,构建了43个新特征:
1. Features related to texts
- 词数(number of words):#words,即一份影评总共有多少个词
- 非重复词占比(ratio of unique words):#unique words / #words
- Stop words占比(ratio of stopwords):#stop words / #words,其中,stop words指的是在英文中一些出现频率很高但没有特殊含义的常用词,比如a,the
- 标点符号占比(ratio of punctuations):#punctuations / #words
- 名词占比(ratio of nouns):#nouns / #words
- 形容词占比(ratio of adj):#adj / #words
- 动词占比(ratio of verbs):#verbs / #words
- 标题词占比(ratio of title words):#title words / #words,其中,title words指的是首字母为大写的词
- 平均词长(mean length of words):sum(length of each word in a text) / #words,即一份影评中,平均每个词的长度(平均每个词有多少个字母)
- 文本字符长度(count of characters):len(text)
- 积极词占比(ratio of positive words):#positive words / #words,其中,积极词词库出自这里
2. Features related to vectorizers
这部分的特征涉及以下几个概念:
- Tokenization:文本词条(tokens)化,如将句子 "I like this move." 分解为 "I", "like", "this", "movie"
- Count Vectorization:将一个语料库(corpus)转化为基于词频的矩阵,矩阵的每一行代表一个文本,每一列代表某个词条在该文本中出现的次数
- TF-IDF: Term Frequency-Inverse Document Frequency,用于评估一字词对于一个语料库中的其中一份文件的重要程度,字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
- TF-IDF Vctorization:将一个语料库转化为基于TF-IDF值的矩阵,矩阵的每一行代表一个文本,每一列代表某个词条在该文本下的TF-IDF值
- SVD:奇异值分解(Singular Value Decomposition),线性代数中的一种矩阵分解方法,在NLP中常与TF-IDF矩阵结合,进行特征降维(Feature Decomposition)
- Stacking:集成学习方法(Ensemble Learning)的一种,思路大致如下:先通过不同算法得出一组预测值,再将这组预测值作为输入特征进行学习,从而得到数据的最终预测值(这里有一篇来自kaggle的文章,很好地解释了staking的实现过程)
了解了上述概念后,现在再回过头来看之前没有提到的特征:
- 基于词频矩阵的NB预测值:通过count vectorization构建词频矩阵,将该矩阵作为输入特征,通过朴素贝叶斯(Naive Bayes,NB)构建目标变量预测值(is_pos = 1的概率),并将该预测值作为最终XGBoost模型的新特征
- 基于词频矩阵矩阵的LR预测值:通过count vectorization构建词频矩阵,将该矩阵作为输入特征,通过逻辑回归(Logistic Regression,LR)构建目标变量预测值(is_pos = 1的概率),并将该预测值作为最终XGBoost模型的新特征
- SVD features of TF-IDF metrix:通过TF-IDF vectorization构建TF-IDF矩阵,通过SVD将该矩阵降至30维,并将这30维向量作为最终XGBoost模型的新特征
结果分析
我在不同阶段使用不同特征组合、算法进行了建模,下面我将分别给出这些不同的建模结果,并在最后对这些结果进行综合对比,以期对特征工程、Stacking的作用管中窥豹。
1. XGBoost with features related to texts only
我首先用11个和文本自身相关的特征(词数、名词占比等)进行建模。使用的算法为XGBoost。
验证集(test set)预测效果:
- LogLoss:0.59
- AUC of ROC curve:0.75
可以看到,预测效果还是不错的,说明这些特征在一定程度上捕捉到了有用信息。
2. XGBoost with SVD features of TF-IDF metrix
接着用XGboost对那30个SVD特征建模。
验证集(test set)预测效果:
- LogLoss:0.55
- AUC of ROC curve:0.79
这些特征同样也捕捉到了有用信息,并且预测效果比之前那11个特征要稍好。注意,这里并不是说用了这30个SVD特征后,前面那11个特征就不需要了,二者捕捉到的可能是不同方面的信息。
3. NB using count matrix
接着用NB对词频矩阵构建分类预测模型。
验证集(test set)预测效果:
- LogLoss:0.56
- AUC of ROC curve:0.86
logloss和之前的结果相似,但auc提升了近9%,说明词频包含了相当一部分的有用信息,是很好的预测特征。
4. LR using count matrix
接下来用LR对词频矩阵构建分类预测模型。
验证集(test set)预测效果:
- LogLoss:0.49
- AUC of ROC curve:0.87
auc和之前相似,但logloss下降了12%,这同样表明词频是不错的预测特征。此外,LR和NB可能捕捉到了不同方面的信息,换句话说,把这两个模型下的预测值进行综合(stacking),也许能够得到更准确的预测结果。
5. XGBoost using all features
最后是使用全部43个特征进行建模的结果。学习算法为XGBoost。
验证集(test set)预测效果:
- LogLoss:0.43
- AUC of ROC curve:0.88
logloss低于之前的任何一个模型,auc高于之前的任何一个模型。
对比一下上述各个模型的ROC曲线:
可以看到,仅使用count matrix进行预测的NB和LR模型,预测结果已经很接近最终模型的预测结果,说明词频包含了大部分预测所需信息,且NB和LR对于这组数据而言,是有效的算法。
另一方面,最终模型的ROC曲线整体而言稍优于NB和LR模型,说明NB和LR在训练时,得到了数据不同方面的信息,而基于TF-IDF的SVD特征和其他基于文本本身的特征,进一步捕捉到了NB、LR无法获取的其他信息。通过对所有特征进行建模,最终的XGBoost模型综合了以上所有信息,从而提高了预测准确率。
再来看一下特征重要程度排名:
前4位分别是积极词占比(pos_word_ratio)、NB预测值(nb_ctv_pred)、LR预测值(lr_ctv_pred)和平均词长(mean_word_len)。其他特征也都在不同程度上发挥了作用。这进一步说明了特征工程和Stacking的重要性。
现在,看一下最终预测结果在测试集上的confusion matrix。
因为预测结果为is_pos = 1的概率值,所以我们需要先确定一个阈值,当概率值大于该阈值时,判定该影评为正面的(predictive is_pos = 1),否则为负面的(predictive is_pos = 0)。
基于ROC曲线返回的结果,选择True Positive Rate大于0.75下的第一个Threshold为该阈值,则当预测结果大于0.56时,判定该影评为正面的,否则为负面的。
由此得到的confusion matrix如下:
预测集中共有300份影评,其中正面影评共有161份,负面影评共139份。在这些正面的影评中,有76.4%份影评被正确识别;在这些负面的影评中,有82.7%份影评被正确识别。整体而言,当拿到一份影评的时候,这个模型有0.793的概率能够正确辨别出这个观众喜欢还是不喜欢这部TA所点评的电影。(注意,不同的阈值返回不同的confusion matrix,以上只是其中一个阈值对应的结果。)
最后我们随机抽取出测试集上的几条影评来具体看一下模型给出的预测结果,从而对这个分类器有一个更直观的感受:
"This is an extraordinary film."
正面影评概率:0.724
"I couldn't take them seriously."
正面影评概率:0.098
"The plot doesn't hang together at all, and the acting is absolutely appalling."
正面影评概率:0.064
"My rating: just 3 out of 10."
正面影评概率:0.192
"I've seen soap operas more intelligent than this movie."
正面影评概率:0.504
"And I really did find them funny."
正面影评概率:0.563
"The story unfolds in 18th century Jutland and the use of period music played on period instruments is just one more fine touch."
正面影评概率:0.373
这个分类器给出的大多数概率值都比较合理,一个可能的原因是这个数据集本身就是经过筛选的,数据集里的影评基本上都“憎恶分明”,使得信息的提取相对容易。注意最后一条影评,我个人觉得这个影评是中性偏肯定的(is_pos也确实是1),但分类器只给出了0.373的概率,说明这个分类器的性能还有待提升。
结语
上面谈到的数据处理方法,灵感主要来源于Kaggle的某个基于NLP的分类竞赛(见前言中的“Tutorial来源”),比如构建词数、非重复词占比、平均词长这样的特征,比如将NB的预测值作为新特征后再二次建模。
这次练习的结果印证了Kaggle里两条百试不爽的法则:
- 一个数据挖掘任务的成功与否,很大程度上看特征工程做得好不好
- 能否进一步提升模型预测能力,stacking是一大秘诀
另外,有几个点我在这次的练习中没有涉及,但对于NLP来说非常值得一试,我在这里标出来供日后查缺补漏:
- Stemming and Lemmatization (related tutorial)
- Word Vectors and Deep Learning (related tutorial)
以上。