本小节讲述通过imdb数据集,使用第六章提到的词集法、词袋法和TF-IDF法,以及新的Word2Vec和DocVec法进行特征提取,分别构建朴素贝叶斯(NB)、支持向量机(SVM)、随机森林(RF)、深度学习算法(DNN,CNN,RNN)模型来识别负面评论。
测试数据来自互联网电影资料库(Internet Movie Database,IMDB),IMDB是一个关于电影演员、电影、电视节目、电视明星和电影制作的在线数据库。IMDB另一受欢迎的特色是其对应每个数据库条目,有47个主要板块的留言板系统。注册用户可以在这些留言板上分享和讨论关于电影,演员,导演的消息。至今已有超过600万注册用户使用过留言板。我们使用标注为正面评论和负面评论的留言板数据。整个数据集一共10万条记录,5万做了标记,5万没有做标记。5万做了标记的数据集合被随机分配成了训练数据集和测试数据集。
加载训练数据集目录下的正面评论和负面评论目录下全部文件,同时进行标记,正面评论为0,负面评论为1
def load_all_files():
x_train=[]
y_train=[]
x_test=[]
y_test=[]
path="../data/review/aclImdb/train/pos/"
print ("Load %s" % path)
x_train=load_files_from_dir(path)
y_train=[0]*len(x_train)
path="../data/review/aclImdb/train/neg/"
print ("Load %s" % path)
tmp=load_files_from_dir(path)
y_train+=[1]*len(tmp)
x_train+=tmp
path="../data/review/aclImdb/test/pos/"
print ("Load %s" % path)
x_test=load_files_from_dir(path)
y_test=[0]*len(x_test)
path="../data/review/aclImdb/test/neg/"
print ("Load %s" % path)
tmp=load_files_from_dir(path)
y_test+=[1]*len(tmp)
x_test+=tmp
return x_train, x_test, y_train, y_test
三、特征提取
(一)词集模型(词汇表模型)
def get_features_by_tf():
global max_document_length
x_train, x_test, y_train, y_test=load_all_files()
vp=tflearn.data_utils.VocabularyProcessor(max_document_length=max_document_length,
min_frequency=0,
vocabulary=None,
tokenizer_fn=None)
x_train=vp.fit_transform(x_train, unused_y=None)
x_train=np.array(list(x_train))
x_test=vp.transform(x_test)
x_test=np.array(list(x_test))
return x_train, x_test, y_train, y_test
(二)词袋模型
通过对训练数据集使用Scikit-Learn的CountVectorizer对象进行词袋化处理,同时将抽取的词汇表保存,用于词袋化测试数据集。
def get_features_by_wordbag():
global max_features
x_train, x_test, y_train, y_test=load_all_files()
vectorizer = CountVectorizer(
decode_error='ignore',
strip_accents='ascii',
max_features=max_features,
stop_words='english',
max_df=1.0,
min_df=1 )
print (vectorizer)
x_train=vectorizer.fit_transform(x_train)
x_train=x_train.toarray()
vocabulary=vectorizer.vocabulary_
vectorizer = CountVectorizer(
decode_error='ignore',
strip_accents='ascii',
vocabulary=vocabulary,
stop_words='english',
max_df=1.0,
min_df=1 )
print (vectorizer)
x_test=vectorizer.fit_transform(x_test)
x_test=x_test.toarray()
return x_train, x_test, y_train, y_test
(三)TF-IDF模型
def get_features_by_wordbag_tfidf():
global max_features
x_train, x_test, y_train, y_test=load_all_files()
vectorizer = CountVectorizer(
decode_error='ignore',
strip_accents='ascii',
max_features=max_features,
stop_words='english',
max_df=1.0,
min_df=1,
binary=True)
print (vectorizer)
x_train=vectorizer.fit_transform(x_train)
x_train=x_train.toarray()
vocabulary=vectorizer.vocabulary_
vectorizer = CountVectorizer(
decode_error='ignore',
strip_accents='ascii',
vocabulary=vocabulary,
stop_words='english',
max_df=1.0,binary=True,
min_df=1 )
print(vectorizer)
x_test=vectorizer.fit_transform(x_test)
x_test=x_test.toarray()
transformer = TfidfTransformer(smooth_idf=False)
x_train=transformer.fit_transform(x_train)
x_train=x_train.toarray()
x_test=transformer.transform(x_test)
x_test=x_test.toarray()
return x_train, x_test, y_train, y_test
(四)Word2Vec模型
Word2Vec是Google在2013年开源的一款将词表征为实数值向量的高效工具,采用的模型有连续词袋(Continuous Bag-Of-Words,CBOW)模型和Skip-Gram两种,原理图如下。
CBOW模型能够根据输入周围n-1个词来预测出这个词本身,而Skip-gram模型能够根据词本身来预测周围有哪些词。也就是说,CBOW模型的输入是某个词A周围的n个单词的词向量之和,输出是词A本身的词向量;而Skip-gram模型的输入是词A本身,输出是词A周围的n个单词的词向量。
Word2Vec通过训练,可以把对文本内容的处理简化为K维向量空间中的向量运算,而向量空间上的相似度可以用来表示文本语义上的相似度。因此,Word2Vec输出的词向量可以被用来做很多NLP相关的工作,比如聚类、找同义词、词性分析等等。
Word2Vec最常用的开源实现之一就是gensim。gensim的使用非常简洁,加载数据和训练数据可以合并,训练好模型后就可以按照单词获取对应的向量表示:
sentences = [['first', 'sentence'], ['second', 'sentence']]
model = gensim.models.Word2Vec(sentences, min_count=1)
print(model['first'])
其中,Word2Vec有很多可以影响训练速度和质量的参数。参数min_count可以对字典做截断,出现少于min_count次数的单词会被丢弃掉,默认值为5:
model = Word2Vec(sentences, min_count=10)
另外一个是神经网络的隐藏层的单元数,推荐值为几十到几百。事实上Word2Vec参数的个数也与神经网络的隐藏层的单元数相同,比如size=200,那么训练得到的Word2Vec参数个数也是200:
model = Word2Vec(sentences, size=200)
创建字典并开始训练获取Word2Vec。gensim的官方文档中强调增加训练次数可以提高生成的Word2Vec的质量,可以通过设置epochs参数来提高训练次数,默认的训练次数为5:
x=x_train+x_test
model.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)
以处理IMDB数据集为例,初始化Word2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度也与神经网络的隐藏层的单元数相同。设置处理的窗口大小为5个单词,出现少于10次的单词会被丢弃掉,迭代计算次数为10次:
def get_features_by_word2vec():
global max_features
x_train, x_test, y_train, y_test=load_all_files()
x_train=cleanText(x_train)
x_test=cleanText(x_test)
x=x_train+x_test
cores=multiprocessing.cpu_count()
if os.path.exists(word2ver_bin):
print ("Find cache file %s" % word2ver_bin)
model=gensim.models.Word2Vec.load(word2ver_bin)
else:
model=gensim.models.Word2Vec(size=max_features, window=5, min_count=10, iter=10, workers=1)
model.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)
model.save(word2ver_bin)
x_train= getVecsByWord2Vec(model,x_train,max_features)
x_test = getVecsByWord2Vec(model, x_test, max_features)
return x_train, x_test, y_train, y_test
经过训练后,Word2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取单词“love”的Word2Vec就可以使用如下形式:
model["love"]
而本例中,获取训练集和测试集的vec方法如下所示:
x_train= getVecsByWord2Vec(model,x_train,max_features)
x_test = getVecsByWord2Vec(model, x_test, max_features)
通过遍历一段英文,逐次获取每个单词对应的Word2Vec,连接起来就可以获得该英文段落对应的Word2Vec:
def getVecsByWord2Vec(model, corpus, size):
global max_document_length
x=[]
for text in corpus:
xx = []
for i, vv in enumerate(text):
try:
xx.append(model[vv].reshape((1,size)))
except KeyError:
continue
x = np.concatenate(xx)
x=np.array(x, dtype='float')
return x
(五)DocVec模型
基于上述(四)提到的Word2Vec的方法,Quoc Le和Tomas Mikolov又给出了Doc2Vec的训练方法。如下图所示,其原理与Word2Vec相同,分为分布式存储(Distributed Memory,DM)和分布式词袋(Distributed Bag of Words,DBOW)。
以处理IMDB数据集为例,初始化Doc2Vec对象,设置神经网络的隐藏层的单元数为200,生成的词向量的维度与神经网络的隐藏层的单元数相同。设置处理的窗口大小为8个单词,出现少于10次的单词会被丢弃掉,迭代计算次数为10次, 源码如下
model=Doc2Vec(dm=0, dbow_words=1, size=max_features, window=8, min_count=10, iter=10, workers=cores)
其中,需要强调的是,dm为使用的算法,默认为1,表明使用DM算法;设置为0,表明使用DBOW算法,通常使用默认配置即可,比如:
model = gensim.models.Doc2Vec.Doc2Vec(size=50, min_count=2, iter=10)
与Word2Vec不同的地方是,Doc2Vec函数的定义如下所示:
gensim.models.doc2vec.Doc2Vec(
documents=None,
size=300,
alpha=0.025,
window=8,
min_count=5,
max_vocab_size=None,
sample=0,
seed=1,
workers=1,
min_alpha=0.0001,
dm=1,
hs=1,
negative=0,
dbow_words=0,
dm_mean=0,
dm_concat=0,
dm_tag_count=1,
docvecs=None,
docvecs_mapfile=None,
comment=None,
trim_rule=None,
**kwargs)
参数的含义如下所示
size 是特征向量的纬度。
window 是要预测的词和文档中用来预测的上下文词之间的最大距离。
alpha 是初始化的学习速率,会随着训练过程线性下降。
seed 是随机数生成器。.需要注意的是,对于一个完全明确的重复运行(fully deterministically-reproducible run),你必须同时限制模型单线程工作以消除操作系统线程调度中的有序抖动。(在python3中,解释器启动的再现要求使用PYTHONHASHSEED环境变量来控制散列随机化)
min_count 忽略总频数小于此的所有的词。
max_vocab_size 在词汇累积的时候限制内存。如果有很多独特的词多于此,则将频率低的删去。每一千万词类大概需要1G的内存,设为None以不限制(默认)。
sample 高频词被随机地降低采样的阈值。默认为0(不降低采样),较为常用的事1e-5。
dm 定义了训练的算法。默认是dm=1,使用 ‘distributed memory’ (PV-DM),否则 distributed bag of words (PV-DBOW)。
workers 使用多少现成来训练模型(越快的训练需要越多核的机器)。
iter 语料库的迭代次数。从Word2Vec中继承得到的默认是5,但在已经发布的‘Paragraph Vector’中,设为10或者20是很正常的。
hs 如果为1 (默认),分层采样将被用于模型训练(否则设为0)。
negative 如果 > 0,将使用负采样,它的值决定干扰词的个数(通常为5-20)。
dm_mean 如果为0(默认),使用上下文词向量的和;如果为1,使用均值。(仅在dm被用在非拼接模型时使用)
dm_concat 如果为1,使用上下文词向量的拼接,默认是0。注意,拼接的结果是一个更大的模型,输入的大小不再是一个词向量(采样或算术结合),而是标签和上下文中所有词结合在一起的大小。
dm_tag_count 每个文件期望的文本标签数,在使用dm_concat模式时默认为1。
dbow_words 如果设为1,训练word-vectors (in skip-gram fashion) 的同时训练 DBOW doc-vector。默认是0 (仅训练doc-vectors时更快)。
trim_rule 词汇表修建规则,用来指定某个词是否要被留下来。被删去或者作默认处理 (如果词的频数< min_count则删去)。可以设为None (将使用min_count),或者是随时可调参 (word, count, min_count) 并返回util.RULE_DISCARD,util.RULE_KEEP ,util.RULE_DEFAULT之一。注意:这个规则只是在build_vocab()中用来修剪词汇表,而且没被保存。
本文的doc2vec逻辑如下所示:
def get_features_by_doc2vec():
global max_features
x_train, x_test, y_train, y_test=load_all_files()
x_train=cleanText(x_train)
x_test=cleanText(x_test)
x_train = labelizeReviews(x_train, 'TRAIN')
x_test = labelizeReviews(x_test, 'TEST')
x=x_train+x_test
cores=multiprocessing.cpu_count()
if os.path.exists(doc2ver_bin):
print ("Find cache file %s" % doc2ver_bin)
model=Doc2Vec.load(doc2ver_bin)
else:
model=Doc2Vec(dm=0, vector_size=max_features, negative=5,
hs=0, min_count=2, workers=1, iter=15)
model.build_vocab(x)
model.train(x, total_examples=model.corpus_count, epochs=model.iter)
model.save(doc2ver_bin)
x_test=getVecs(model,x_test,max_features)
x_train=getVecs(model,x_train,max_features)
return x_train, x_test, y_train, y_test
经过训练后,Doc2Vec会以字典的形式保存在model对象中,可以使用类似字典的方式直接访问获取,比如获取段落“I love tensorflow”的Doc2Vec就可以使用如下形式:
model.docvecs[”I love tensorflow”]
本例中,Doc2Vec的维度与之前设置的神经网络的隐藏层的单元数相同,为200,即一个长度为200的一维向量。以英文段落为单位,通过遍历训练数据集和测试数据集,逐次获取每个英文 段落对应的Doc2Vec,这里的英文段落就可以理解为数据集中针对电影的一段评价。获取训练集和测试集向量的方式如下
def getVecs(model, corpus, size):
vecs = [np.array(model.docvecs[z.tags[0]]).reshape((1, size)) for z in corpus]
return np.array(np.concatenate(vecs),dtype='float')
训练Word2Vec和Doc2Vec是非常费时费力的过程,调试阶段会频繁更换分类算法以及修改分类算法参数调优。为了提高效率,可以把之前训练得到的Word2Vec和Doc2Vec模型保存成文件形式,以Doc2Vec为例,使用model.save函数把训练后的结果保存在本地硬盘上,运行程序时,在初始化Doc2Vec对象之前,可以先判断本地硬盘是否存在模型文件,如果存在就直接读取模型文件并初始化Doc2Vec对象,反之则需要训练数据:
if os.path.exists(doc2ver_bin):
print("Find cache file %s" % doc2ver_bin)
model=Doc2Vec.load(doc2ver_bin)
else:
model=Doc2Vec(size=max_features, window=5, min_count=2, workers=cores, iter=40)
model.build_vocab(x))
model.train(x, total_examples=model.corpus_count, epochs=model.iter)
model.save(doc2ver_bin)
(一)NB
作者的代码实际上注释较少,这部分写了两个函数,实际上必要性不大
def do_nb_wordbag(x_train, x_test, y_train, y_test):
print ("NB and wordbag")
gnb = GaussianNB()
gnb.fit(x_train,y_train)
y_pred=gnb.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
def do_nb_doc2vec(x_train, x_test, y_train, y_test):
print ("NB and doc2vec")
gnb = GaussianNB()
gnb.fit(x_train,y_train)
y_pred=gnb.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
(二)SVM
作者的代码实际上注释较少,这部分写了两个函数,实际上必要性不大
def do_svm_wordbag(x_train, x_test, y_train, y_test):
print ("SVM and wordbag")
clf = svm.SVC()
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
def do_svm_doc2vec(x_train, x_test, y_train, y_test):
print ("SVM and doc2vec")
clf = svm.SVC()
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
(三)随机森林
def do_rf_doc2vec(x_train, x_test, y_train, y_test):
print ("rf and doc2vec")
clf = RandomForestClassifier(n_estimators=10)
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
(四)DNN
作者的代码实际上注释较少,这部分写了两个函数,实际上必要性不大
def do_dnn_wordbag(x_train, x_test, y_train, y_test):
print ("MLP and wordbag")
# Building deep neural network
clf = MLPClassifier(solver='lbfgs',
alpha=1e-5,
hidden_layer_sizes = (5, 2),
random_state = 1)
print ( clf)
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
def do_dnn_doc2vec(x_train, x_test, y_train, y_test):
print ("MLP and doc2vec")
global max_features
# Building deep neural network
clf = MLPClassifier(solver='lbfgs',
alpha=1e-5,
hidden_layer_sizes = (5, 2),
random_state = 1)
print ( clf)
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
print (metrics.accuracy_score(y_test, y_pred))
print (metrics.confusion_matrix(y_test, y_pred))
(五)CNN
词袋模型
def do_cnn_wordbag(trainX, testX, trainY, testY):
global max_document_length
print ("CNN and tf")
trainX = pad_sequences(trainX, maxlen=max_document_length, value=0.)
testX = pad_sequences(testX, maxlen=max_document_length, value=0.)
# Converting labels to binary vectors
trainY = to_categorical(trainY, nb_classes=2)
testY = to_categorical(testY, nb_classes=2)
# Building convolutional network
network = input_data(shape=[None,max_document_length], name='input')
network = tflearn.embedding(network, input_dim=1000000, output_dim=128)
branch1 = conv_1d(network, 128, 3, padding='valid', activation='relu', regularizer="L2")
branch2 = conv_1d(network, 128, 4, padding='valid', activation='relu', regularizer="L2")
branch3 = conv_1d(network, 128, 5, padding='valid', activation='relu', regularizer="L2")
network = merge([branch1, branch2, branch3], mode='concat', axis=1)
network = tf.expand_dims(network, 2)
network = global_max_pool(network)
network = dropout(network, 0.8)
network = fully_connected(network, 2, activation='softmax')
network = regression(network, optimizer='adam', learning_rate=0.001,
loss='categorical_crossentropy', name='target')
# Training
model = tflearn.DNN(network, tensorboard_verbose=0)
model.fit(trainX, trainY,
n_epoch=5, shuffle=True, validation_set=(testX, testY),
show_metric=True, batch_size=100,run_id="review")
doc2vec, 相对于word2vec只是不需要对其处理
def do_cnn_doc2vec(trainX, testX, trainY, testY):
global max_features
print ("CNN and doc2vec")
#trainX = pad_sequences(trainX, maxlen=max_features, value=0.)
#testX = pad_sequences(testX, maxlen=max_features, value=0.)
# Converting labels to binary vectors
trainY = to_categorical(trainY, nb_classes=2)
testY = to_categorical(testY, nb_classes=2)
# Building convolutional network
network = input_data(shape=[None,max_features], name='input')
network = tflearn.embedding(network, input_dim=1000000, output_dim=128,validate_indices=False)
branch1 = conv_1d(network, 128, 3, padding='valid', activation='relu', regularizer="L2")
branch2 = conv_1d(network, 128, 4, padding='valid', activation='relu', regularizer="L2")
branch3 = conv_1d(network, 128, 5, padding='valid', activation='relu', regularizer="L2")
network = merge([branch1, branch2, branch3], mode='concat', axis=1)
network = tf.expand_dims(network, 2)
network = global_max_pool(network)
network = dropout(network, 0.8)
network = fully_connected(network, 2, activation='softmax')
network = regression(network, optimizer='adam', learning_rate=0.001,
loss='categorical_crossentropy', name='target')
# Training
model = tflearn.DNN(network, tensorboard_verbose=0)
model.fit(trainX, trainY,
n_epoch=5, shuffle=True, validation_set=(testX, testY),
show_metric=True, batch_size=100,run_id="review")
doc2vec_2d,这部分相对于doc2vec的区别主要是卷积核不同
具体源码如下所示
def do_cnn_doc2vec_2d(trainX, testX, trainY, testY):
print("CNN and doc2vec 2d")
trainX = trainX.reshape([-1, max_features, max_document_length, 1])
testX = testX.reshape([-1, max_features, max_document_length, 1])
# Building convolutional network
network = input_data(shape=[None, max_features, max_document_length, 1], name='input')
network = conv_2d(network, 16, 3, activation='relu', regularizer="L2")
network = max_pool_2d(network, 2)
network = local_response_normalization(network)
network = conv_2d(network, 32, 3, activation='relu', regularizer="L2")
network = max_pool_2d(network, 2)
network = local_response_normalization(network)
network = fully_connected(network, 128, activation='tanh')
network = dropout(network, 0.8)
network = fully_connected(network, 256, activation='tanh')
network = dropout(network, 0.8)
network = fully_connected(network, 10, activation='softmax')
network = regression(network, optimizer='adam', learning_rate=0.01,
loss='categorical_crossentropy', name='target')
# Training
model = tflearn.DNN(network, tensorboard_verbose=0)
model.fit({'input': trainX}, {'target': trainY}, n_epoch=20,
validation_set=({'input': testX}, {'target': testY}),
snapshot_step=100, show_metric=True, run_id='review')
(六)RNN
def do_rnn_wordbag(trainX, testX, trainY, testY):
global max_document_length
print ("RNN and wordbag")
trainX = pad_sequences(trainX, maxlen=max_document_length, value=0.)
testX = pad_sequences(testX, maxlen=max_document_length, value=0.)
# Converting labels to binary vectors
trainY = to_categorical(trainY, nb_classes=2)
testY = to_categorical(testY, nb_classes=2)
# Network building
net = tflearn.input_data([None, max_document_length])
net = tflearn.embedding(net, input_dim=10240000, output_dim=128)
net = tflearn.lstm(net, 128, dropout=0.8)
net = tflearn.fully_connected(net, 2, activation='softmax')
net = tflearn.regression(net, optimizer='adam', learning_rate=0.001,
loss='categorical_crossentropy')
# Training
model = tflearn.DNN(net, tensorboard_verbose=0)
model.fit(trainX, trainY, validation_set=(testX, testY), show_metric=True,
batch_size=10,run_id="review",n_epoch=5)
六、总结
本小节相对于上节内容,主要是讲解了word2vec和doc2vec这两种方法提取特征向量,模型构建部分则是增加了随机森林。事实上构造word2vec和doc2vec这两个向量模型过于耗时,直接使用作者的源码测试性能并没有如此好,甚至有点差得离谱,我觉得应该是参数仍有较大调优的空间。