前言
本模块将使用IMDB情绪分析数据集,数据集包含100,000条正面和负面的多段电影评论。利用NLP技术来预测电影评论是正向情绪还是负面情绪。
什么是NLP?
NLP(自然语言处理)是一种用于解决文本问题的技术。本模块将会用word2vec进行加载和清理IMDB电影评论,用模型预测是赞成还是反对。
模块分为三部分:
- 基本自然语言处理:Bag of Words面向初学者,介绍了基本自然语言处理技术。
- 用于文本理解的深度学习:在第2部分和第3部分中,深入研究如何使用Word2Vec训练模型以及如何将所得的词向量用于情感分析。
01
第一部分
读取数据集
import pandas as pd
import numpy as np
from bs4 import BeautifulSoup
import re
# 载入数据集
train = pd.read_csv('labeledTrainData.tsv',
header=0,
delimiter="\t",
quoting=3)
调用get_text()得到没有标签或标记的评论文本。
BeautifulSoup是一个非常强大的库。这里用正则表达式删除标记并不是很好,最好还是使用BeautifulSoup之类的包。
处理标点,数字和停用词:NLTK和正则表达式
在考虑如何清除文本时,应该考虑我们要解决的是什么数据问题。对于许多问题,删除标点符号是有意义的。但我们正在解决情感分析问题,并且可能“ !!!” 或“ :-(”可能带有情感,应将其视为单词。在本模块中,为简单起见,我完全删除了标点符号,但是您可以自己使用它。
# 定义方法来清理文本数据:去掉停用词、标点符号
# 把句子转成单词列表
def review_to_words(raw_review):
# 去除html标签
review_text = BeautifulSoup(raw_review,
'html.parser').get_text()
# 剔除非单词字符,并用空格替换
letters_only = re.sub("[^a-zA-Z]", " " , review_text)
# 转为为小写字母,并切分成单词
words = letters_only.lower().split()
# 剔除停用词
stops = set(stopwords.words("english"))
meaningful_words = [w for w in words if not w in stops]
return " ".join(meaningful_words)
注释,将停用词列表转换为set是为了提高速度;由于我们将要调用该函数数万次,因此它需要快速运行,并且在Python中搜索集合比搜索列表要快得多。
OK,准备工作都做完了,现在可以循环清理所有训练集:
num_reviews = train['review'].size
clean_train_reviews = []
for i in range(num_reviews):
if (i + 1) % 1000 == 0:
print("Review %d of %d\n" % (i+1, num_reviews))
clean_train_reviews.append(review_to_words(train['review'][i]))
使用Bag of Words构造特征
两个句子:
Sentence 1: "The cat sat on the hat"
Sentence 2: "The dog ate the cat and the hat"
根据这两句话可以得到词集合如下:
{ the, cat, sat, on, hat, dog, ate, and }
bags of words:计算每个单词在句子中出现的次数。
Sentence 1, "the" 出现两次, "cat", "sat", "on", "hat" 各出现一次, 所以第一句话的特征词:
{ the, cat, sat, on, hat, dog, ate, and }
对应的特征向量是:
- Sentence 1: { 2, 1, 1, 1, 1, 0, 0, 0 }
同样的,可以得到:
- Sentence 1: { 3, 1, 0, 0, 1, 1, 1, 1}
下面用python实现上面的功能:「scikit-learn」
# Bag of Words构造特征
from sklearn.feature_extraction.text import CountVectorizer
# 初始化CountVectorizer对象
vectorizer = CountVectorizer(analyzer='word',
tokenizer=None,
preprocessor=None,
stop_words=None,
max_features=5000)
train_data_feature = vectorizer.fit_transform(clean_train_reviews)
train_data_feature = train_data_feature.toarray()
CountVectorizer可以自动执行预处理,标记化和停止单词删除-对于这些选项中的每一个,我们可以使用内置方法或指定要使用的函数来代替指定“ None”。有关更多详细信息,请参见功能文档。但是,在本教程我自己实现的数据清理功能,增加对这个过程的理解。
# 查看词汇表
vocab = vectorizer.get_feature_names()
dist = np.sum(train_data_feature, axis=0)
# 打印每个单词及其在训练集中出现的次数
for tag, count in zip(vocab, dist):
print(count, tag)
# 开始训练模型
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report
X_train,x_test,y_train,y_test = train_test_split(train_data_feature,
train.sentiment,
test_size=0.2,
random_state=0)
# 初始化100棵树
forest = RandomForestClassifier(n_estimators=100)
# 开始训练
forest = forest.fit(X_train, y_train)
# 在验证集上看预测看效果
y_pred = forest.predict(x_test)
# 查看混淆矩阵
conf_matrix = confusion_matrix(y_test, y_pred)
target_names = ['class 0', 'class 1']
class_report = classification_report(y_test,
y_pred,
target_names=target_names)
查看分类效果
02
第二部分
这一部分将重点介绍如何使用由Word2Vec算法创建的分布式单词向量。
Word2vec
由Google在2013年发布一种神经网络实现,用于学习单词的「分布式表示形式」。在此之前,还提出了其他深度或递归神经网络体系结构来学习单词表示,但是这些方法的主要问题是训练模型需要很长时间。Word2vec相对于其他模型可以快速学习。
词的向量化
- 词集法(one-hot):统计文档总词数建立长度为N的字典,将单词表示为一个N维高度稀疏的向量,词对应位置元素值为1,其他全为0;
- 词袋法bag of words:统计文档总词数N,将单词表示为一个N维高度稀疏的向量,词对应位置元素为词在该篇文档中的词频,其他位置元素值为0;
- 词的分布式表示法:将词表示为低维度、稠密的向量,主要是通过神经网络训练语言模型得到,如word2vec、glove、BERT等。
Word2Vec不需要标签即可创建有意义的表示形式。这很有用,因为现实世界中的大多数数据都没有标签。如果网络获得了足够的训练数据(数百亿个单词),它将产生具有价值的特征的单词向量。具有相似含义的单词出现在群集中,并且群集之间的间距使得可以使用矢量数学来再现某些单词关系(例如类推)。著名的例子是,在训练有素的单词向量中,“国王-男人+女人=女王”。
python中使用Word2vec
使用gensim软件包中word2vec,gensim需要自行安装。
# 加载数据集
train = pd.read_csv('labeledTrainData.tsv',
header=0,
delimiter="\t",
quoting=3)
unlabeled_train = pd.read_csv('unlabeledTrainData.tsv',
header=0,
delimiter="\t",
quoting=3)
attention
与第1部分不同,我新增了数据集unlabeledTrain.tsv,其中包含50,000条没有标签的附加评论。 当我们在第1部分中构建“语言袋”模型时,额外的未标记培训评论没有用。但是,由于Word2Vec可以从未标记的数据中学习,因此现在可以使用这些额外的50,000条评论。
def review_to_wordlist(review, remove_stopwords=False):
review_text = BeautifulSoup(review, 'html.parser').get_text()
review_text = re.sub('[^a-zA-z]', ' ', review_text)
words = review_text.lower().split()
if remove_stopwords:
words = [w for w in words if not w in stops]
return words
attention
训练Word2Vec最好不要删除停用词,因为该算法依赖于句子的更广泛上下文来生成高质量的词向量。因此,我们将在以下功能中将停用词删除功能设为可选。
上面函数功能
输入sentence:
['Adrian Pasdar is excellent is this film.']
函数review_to_wordlist处理后->
['adrian', 'pasdar', 'is', 'excellent', 'is', 'this', 'film']
Word2Vec要求使用单个句子,每个句子作为单词列表。换句话说,输入格式是列表的列表。
如何将段落拆分成句子
自然语言有各种各样的陷阱。英文句子可以以“?”,“!”,“”或“。”结尾,并且凭借空格和大小写也不可靠,因此,我们将使用NLTK的punkt标记器进行句子拆分。您将需要安装NLTK并使用nltk.download()下载有关punkt的相关文件。
import nltk.data
# 加载nltk的tokenizer
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
# 将评论分解成已解析的句子,返回句子列表
# 每个句子都是有单词构成的列表
def review_to_sentences(review, tokenizer, remove_stopwords=False):
# 使用NLTK分词器,把段子分成句子
raw_sentences = tokenizer.tokenize(review.strip())
sentences = []
for raw_sentence in raw_sentences:
# 如果句子是空串,跳过不处理
if len(raw_sentence) > 0:
# 调用上面的方法得到单词列表
sentences.append(review_to_wordlist(raw_sentence,
remove_stopwords))
return sentences
函数功能
sentence:
["Adrian Pasdar is excellent is this film. He makes a fascinating woman."]
函数review_to_wordlist处理后-->
- 第一步用tokenzier将段子分成句子
[["Adrian Pasdar is excellent is this film."],
["He makes a fascinating woman."]]
- 第二部调用review_to_wordlist将句子分成单词列表
[['adrian','pasdar','is','excellent','is','this','film'],
['he', 'makes', 'a', 'fascinating', 'woman']]
# 构造Word2Vec输入数据
sentences = []
# 解析train set
for review in train.review:
sentences += review_to_sentences(review, tokenizer)
# 解析unlabeled set
for review in unlabeled_train.review:
sentences += review_to_sentences(review, tokenizer)
开始训练和保存模型
# 训练和保存模型
from gensim.models import word2vec
import logging
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',
level=logging.INFO)
# 设置模型参数
num_features = 300
min_word_count = 40
num_workers = 4
context = 10
downsampling = 1e3
model = word2vec.Word2Vec(sentences,
workers=num_workers,
size=num_features,
min_count=min_word_count,
window=context,
sample=downsampling)
model.init_sims(replace=True)
# 模型保存
Word2Vec.load()
model_name = "300features_40minwords_10context"
model.save(model_nameme)
参数解释
- Architecture:Architecture选项是跳过语法(默认值)或连续的单词袋。我们发现,skip-gram的速度稍慢一些,但产生了更好的结果。
- Training algorithm:Hierarchical softmax (默认) 或 negative sampling。本实例中,默认设置效果很好。
- 常用词的下采样[downsampling]:Google文档建议使用.00001和.001之间的值。对于我们来说,更接近0.001的值似乎可以提高最终模型的准确性。
- 字向量维数[num_feature]:更多功能会导致更长的运行时间,并且通常(但并非总是)会导致更好的模型。合理的值可以在几十到几百之间。我们用了300。
- 上下文/窗口大小[Context/window size]:训练算法应考虑多少个上下文词?10对于分层softmax似乎很好用(越多越好,直到一定程度)。
- 辅助线程[num_workers]:要运行的并行进程数。这是特定于计算机的,但是在大多数系统上应该在4到6之间工作。
- 最小单词数[min_word_count]:这有助于将词汇量限制为有意义的单词。在所有文档中至少出现多次的任何单词都将被忽略。合理的值可以在10到100之间。在这种情况下,由于每部电影出现30次,因此我们将最小字数设置为40,以避免对单个电影标题过于重视。这样一来,整个词汇量约为15,000个单词。较高的值也有助于限制运行时间。
可以检查模型运行时,机器的使用效率,如果有4个工作线程,则列表中的第一个进程应该是Python,它应该显示280-400%的CPU使用率。
「top -o cpu」
OK,训练好了模型,我们来看看效果
# “ doesnt_match”函数将尝试推断集合中哪个词与其他词最不相似:
model.doesnt_match("man woman child kitchen".split())
output: kitchen
# 模型能够区分含义上的差异!它知道男人,女人和孩子彼此之间的相似程度远胜于厨房。
# 更多的探索表明,该模型对更细微的含义差异(例如国家和城市之间的差异)敏感:
model.doesnt_match("france england germany berlin".split())
output:berlin
# 还可以使用“ most_similar”功能来深入了解模型的词簇
# 比如和情绪相关的
model.most_similar("awful")
output:[('terrible', 0.7796876430511475),
('atrocious', 0.7326613664627075),
('horrible', 0.7166414260864258),
('abysmal', 0.7077516317367554),
('dreadful', 0.7073409557342529),
('horrendous', 0.6726270318031311),
('appalling', 0.6665079593658447),
('horrid', 0.6659281253814697),
('lousy', 0.6465609669685364),
('laughable', 0.609215259552002)]
但是,我们如何使用这些分布式词向量进行监督学习呢?第三部分将对此进行介绍。
03
第三部分
第2部分中训练的Word2Vec模型由词汇表中每个单词的特征向量组成, 存储在numpy 称为“ syn0”的数组中.
from gensim.models import Word2Vec
# 加载上面训练好的模型
model = Word2Vec.load("300features_40minwords_10context")
# type(model.wv.syn0) # numpy.ndarray
# print(model.wv.syn0.shape) # (16612, 300)
syn0中的行数是模型词汇量中的单词数,列数与特征向量的大小相对应; 这是我们在第2部分中设置的。将最小单词数设置为40可得到的总词汇量为16,612个单词,每个单词具有300个特征。可以通过以下方式查看各个单词向量:
model['flower'] # 返回一个1x300 numpy数组
IMDB数据集的挑战之一是可变长度评论。我们需要找到一种方法来获取单个单词向量并将其转换为每次评论都具有相同长度的特征集。
向量平均法——把不定长的句子转成定长的词向量
由于每个词都是300维空间中的向量,因此我们可以使用向量运算来组合每个评论中的词。我们尝试的一种方法是简单地对给定评论中的单词向量进行平均(这里我删除了停用词,这只会增加噪音)。
以下代码以第2部分中的代码为基础对特征向量进行平均。
def makeFeatureVec(words, model, num_features):
# 把一句话产生所有词向量做平均
# 初始化空数组
featureVec = np.zeros((num_features,), dtype='float32')
nwords = 0
# Index2word 是模型所有词的名字列表
# 转为set,搜索速度更快
index2word_set = set(model.wv.index2word)
for word in words:
if word in index2word_set:
nwords += 1
featureVec = np.add(featureVec, model[word])
return np.divide(featureVec, nwords)
def getAvgFeatureVecs(reviews, model, num_features):
counter = 0
reviewFeatureVecs = np.zeros((len(reviews),num_features),
dtype='float32')
for review in reviews:
if counter % 1000 == 0:
print('Review %d of %d' % (counter, len(reviews)))
reviewFeatureVecs[counter] = makeFeatureVec(review,
model,
num_features)
counter += 1
return reviewFeatureVecs
处理训练集数据,将文本转成定长向量
clean_train_reviews = []
for review in train.review:
clean_train_reviews.append(review_to_wordlist(review,
remove_stopwords=True))
开始训练模型
trainDataVecs = getAvgFeatureVecs(clean_train_reviews,
model, num_features)
X_train,x_test,y_train,y_test = train_test_split(trainDataVecs,
train.sentiment,
test_size=0.2,
random_state=0)
# 初始化100棵RF分类器
forest = RandomForestClassifier(n_estimators=100)
# 开始训练
forest = forest.fit(X_train, y_train)
# 在验证集上看预测看效果
y_pred = forest.predict(x_test)
# 查看混淆矩阵
conf_matrix = confusion_matrix(y_test, y_pred)
target_names = ['class 0', 'class 1']
class_report = classification_report(y_test,
y_pred,
target_names=target_names)
模型效果
可以看到word2vec的效果和bag of words差不多。。。。那看来取向量元素平均均值并不是好的办法,单词向量进行加权的标准方法是应用“ tf-idf ”权重,该权重用于衡量给定单词在给定文档集中的重要性。
tf-idf
- term frequency–inverse document frequency
- 在Python中提取tf-idf权重的一种方法是使用scikit-learn的TfidfVectorizer.
- tf-idf是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
该方法后续会补充,这里不再做扩展。。。。
流程总结
参考材料:
[1]“信号和信息处理的深度学习”,李登和董瑜(来自Microsoft)
[2]“深度学习教程”(Yann LeCun和Marc'Aurelio Ranzato的2013年演讲)
[3]"gensim官方文档"