第四节 文本情绪分析(1 / 1)

基于前面讲述的分词及词向量技术,可以进行非常有用也更复杂的文本处理工作。这一节将讲述如何利用这些技术来实现文本情绪的识别。很多语句、新闻、台词、评论都带有某种情绪,如果能够理解文本中的这种情绪,不但是有趣的事情,而且也有重要的应用价值。例如,在电商购物网站上,有很多的评论数据,如果能够自动识别这些评论数据的情绪,对于顾客选择商品、商家推荐商品都是很有帮助的。

这是一个相对复杂的工作,作为本教材最后一个实际案例,将采用模块化的方式实现,包括用于训练情绪识别网络的训练模块和使用训练好的网络识别文本情绪的识别模块。这种模块化的编程方式把复杂的任务分解成几个相对简单的任务,流程和可读性更好,在实际工作中十分常见。

本节所使用的训练数据是从电商网站的评论区抓取的商品评论,可从教材资源平台下载。这些文本的情绪已经做了人工标注,被分成了正面评论和负面评论两类。首先通过深度学习,去学习这些评论中正面评论、负面评论的特征。学习完毕后就可以使用训练好的网络判断新的文本表达的情绪是正面还是负面。标注了情绪的数据包括10 679条正面评论和10 428条负面评论,以Excel表格文件分别存储,文件名分别是pos.xls和neg.xls。表9-2是从中截取的一部分样本。

表9-2

在训练模块中,首先需要使用训练好的词向量把训练数据(具有正负面情绪的文本)向量化,然后使用深度神经网络提取文本中的情绪特征,所用的网络结构中包括在自然语言理解中常用的长短时记忆结构(Long-Short-Term Memory,LSTM)。经过训练后的网络连接权值可以抽取出文本的情绪特征,使用它就可以识别新文本中的情绪了。

需要说明的是,由于训练适用于本任务的向量化词表需要使用内容相符并且规模较大的语料库,所以在教材资源平台提供了训练好的词向量模型,下载后可以直接使用。感兴趣的读者可以自己抓取文本,建立合适的语料库训练自己的词向量。虽然上一节已经介绍过具体的训练方法,为方便起见,本节最后也提供了适用于这个案例的词向量训练模块。

首先给出训练模块的程序,代码的含义请阅读代码中的注释。

#导入所需的模块

import sys

#用来分隔训练集和检测集

from sklearn.cross_validation import train_test_split

#使用多个进程进行计算

import multiprocessing

import numpy as np

#使用词向量

from gensim.models.word2vec import Word2Vec

from gensim.corpora.dictionary import Dictionary

#使用序贯模型

from keras.preprocessing import sequence

#将要用到的网络类型

from keras.models import Sequential

from keras.layers.embeddings import Embedding

from keras.layers.recurrent import LSTM

from keras.layers.core import Dense,Dropout,Activation

import jieba

import pandas as pd

np.random.seed(1337)#指定随机种子,保证结果可复制

sys.setrecursionlimit(1000000)

#设置参数

vocab_dim=100 #词向量的维数

maxlen=100

n_iterations=1

n_exposures=10 #出现频率低于10次的词会被忽略

window_size=10 #词向量考虑的上下文最大长度

batch_size=30

n_epoch=10

input_length=100

cpu_count=multiprocessing.cpu_count()

#并行cpu的数量可设置为cpu核心数

#定义加载训练文件并对数据格式进行处理的函数,以备使用

def loadfile():

pos=pd.read_excel('pos.xls',header=None,index=None)

neg=pd.read_excel('neg.xls',header=None,index=None)

combined=np.concatenate((pos[0],neg[0]))

y=np.concatenate((np.ones(len(pos),dtype=int),

np.zeros(len(neg),dtype=int)))

return combined,y

#定义函数,对句子进行分词,并去掉换行符

def tokenizer(text):

text=[jieba.lcut(document.replace('\n',''))for document

in text]

return text

#创建词典,并返回每个词语的索引、词向量以及每个句子所对应的词语索引

def create_dictionaries(model=None,

combined=None):

if(combined is not None)and(model is not None):

}bl

} gensim_dict=Dictionary()

gensim_dict.doc2bow(model.wv.vocab.keys(),allow_updat

e=True)

#所有频数超过10的词语的索引

w2indx={v:k+1 for k,v in gensim_dict.items()}

#所有频数超过10的词语的词向量

w2vec={word:model[word] for word in w2indx.keys()}

def parse_dataset(combined):

data=[]

for sentence in combined:

new_txt=[]

for word in sentence:

try:

new_txt.append(w2indx[word])

except:

new_txt.append(0)

data.append(new_txt)

return data

combined=parse_dataset(combined)

#每个句子所含词语对应的索引

combined=sequence.pad_sequences(combined,maxlen=maxlen)

return w2indx,w2vec,combined

else:

print("No data provided...")

#定义词向量函数,读取训练好的词向量模型。调用创建词典的函数

#返回每个词语的索引、词向量以及每个句子所对应的词语索引

def word2vec_train(combined):

model=Word2Vec.load('word2vec_model.pkl')

index_dict,word_vectors,combined=

create_dictionaries(model=model,combined=combined)

return index_dict,word_vectors,combined

def get_data(index_dict,word_vectors,combined,y):

#所有单词的索引数,频数小于10的词语索引为0,所以加1

n_symbols=len(index_dict)+1

#索引为0的词语,词向量全为0

embedding_weights=np.zeros((n_symbols,vocab_dim))

#从索引为1的词语开始循环,每个词语对应到它的词向量

for word,index in index_dict.items():

embedding_weights[index,:]=word_vectors[word]

x_train,x_test,y_train,y_test=train_test_split(combined,y,

test_size=0.2)

print(x_train.shape,y_train.shape)

return n_symbols,embedding_weights,x_train,y_train,x_test,y_test

#定义网络结构

def train_lstm(n_symbols,embedding_weights,

x_train,y_train,x_test,y_test):

print("Defining a Simple Keras Model...")

model=Sequential()#使用序贯模型

model.add(Embedding(output_dim=vocab_dim,

input_dim=n_symbols,

mask_zero=True,

weights=[embedding_weights],

input_length=input_length))

model.add(LSTM(recurrent_activation="hard_sigmoid",

activation="sigmoid",units=50))

model.add(Dropout(0.5))

model.add(Dense(1))

model.add(Activation('sigmoid'))

print("Compiling the Model...")

model.compile(loss='binary_crossentropy',

optimizer='adam',metrics=['accuracy'])

print("Train...")

model.fit(x_train,y_train,batch_size=batch_size,epochs=n_epoch,

verbose=1,validation_data=(x_test,y_test))

#对模型进行评价并打印显示评价结果

print("Evaluate...")

loss,accuracy=model.evaluate(x_test,y_test,batch_size=batch_

size)

#把模型保存到lstm.h5文件中,并打印最终训练结果的损失和精度

model.save('lstm.h5',overwrite=True)

print("\nLoss:%.2f,Accuracy:%.2f%%"%(loss,accuracy*100))

#定义函数调用train_lstm用来训练网络并保存训练结果

def train():

print("Loading Data...")

combined,y=loadfile()

print(len(combined),len(y))

print("Tokenising...")

combined=tokenizer(combined)

print("Training a Word2vec model...")

index_dict,word_vectors,combined=word2vec_train(combined)

print("Setting up Arrays for Keras Embedding Layer...")

n_symbols,embedding_weights,x_train,y_train,x_test,y_test

=get_data(index_dict,word_vectors,combined,y)

print(x_train.shape,y_train.shape)

train_lstm(n_symbols,embedding_weights,x_train,y_train,x_test,

y_test)

#定义函数,对句子进行分词并调用词向量转换成向量格式

#这个函数用于训练完毕后的测试,在测试模块中调用此函数

#定义在训练模块是因为需要使用此模块中的创建词典函数

def input_transform(string):

words=jieba.lcut(string)

}bl words=np.array(words).reshape(1,-1)

model=Word2Vec.load('word2vec_model.pkl')

_,_,combined=create_dictionaries(model,words)

return combined

#运行训练函数(train())

if __name__=='__main__':

train()

将训练模块以文件名lstm.py保存,在IPython控制台输入如下命令运行训练模块,并保存训练好的情绪识别网络。

In[1]:run lstm.py

因为不同计算机的环境不同,所以读者的输出结果也许会稍有不同。输出结果如下所示。

Using TensorFlow backend.

Loading Data...

Building prefix dict from the default dictionary ...

21105 21105

Tokenising...

Dumping model to file cache

Loading model cost 0.922 seconds.

Prefix dict has been built succesfully.

Training a Word2vec model...

w2vec={word:model[word] for word in w2indx.keys()}

Setting up Arrays for Keras Embedding Layer...

(16884,100)(16884,)

(16884,100)(16884,)

Defining a Simple Keras Model...

Compiling the Model...

Train...

Train on 16884 samples,validate on 4221 samples

Epoch 1/10

16884/16884[==============================]-44s 3ms/step-loss:0.6541-acc:0.6020-val_loss:0.4702-val_acc:0.7894

Epoch 2/10

16884/16884[==============================]-42s 2ms/step-loss:0.2957-acc:0.8888-val_loss:0.2601-val_acc:0.9029

Epoch 3/10

16884/16884[==============================]-42s 2ms/step-loss:0.1669-acc:0.9450-val_loss:0.2454-val_acc:0.9157

Epoch 4/10

16884/16884[==============================]-42s 3ms/step-loss:0.1224-acc:0.9640-val_loss:0.2627-val_acc:0.9154

Epoch 5/10

16884/16884[==============================]-42s 3ms/step-loss:0.0955-acc:0.9728-val_loss:0.2901-val_acc:0.9083

Epoch 6/10

16884/16884[==============================]-42s 2ms/step-loss:0.0749-acc:0.9801-val_loss:0.3316-val_acc:0.9078

Epoch 7/10

16884/16884[==============================]-43s 3ms/step-loss:0.0687-acc:0.9814-val_loss:0.3266-val_acc:0.9048

Epoch 8/10

16884/16884[==============================]-42s 3ms/step-loss:0.0493-acc:0.9877-val_loss:0.3791-val_acc:0.9119

Epoch 9/10

16884/16884[==============================]-42s 2ms/step-loss:0.0435-acc:0.9887-val_loss:0.4097-val_acc:0.9083

Epoch 10/10

16884/16884[==============================]-42s 2ms/step-loss:0.0378-acc:0.9911-val_loss:0.4326-val_acc:0.9029

Evaluate...

4221/4221[==============================]-3s 636us/step

Loss:0.43,Accuracy:90.29%

可以看到,训练过程中会显示每个Epoch所花费的时间、损失、精度等信息。经过10个Epoch的训练,识别网络在测试集上的精度超过90%。

需要提醒的是,并不是训练的Epoch越多越好。虽然精度也许会不断提升,但是检验精度也许会下降,这就说明网络在训练集上已经过拟合了,此时应该停止训练。准确率与很多因素有关,调整网络结构、网络参数、增加标注数据、训练更好的词向量等方式都可以有效地提高识别准确率。本教材所使用的模型并未经过仔细调整,读者可以尝试各种调整方式以取得更好的识别效果。

接下来介绍识别模块,代码如下。

#导入需要使用的库并设定参数

import sys

from keras.models import load_model

import numpy as np

np.random.seed(1337)

from lstm import input_transform

import multiprocessing

vocab_dim=100

maxlen=100

n_iterations=1

n_exposures=10

window_size=10

batch_size=30

n_epoch=10

input_length=100

cpu_count=multiprocessing.cpu_count()

argvs_length=len(sys.argv)

argvs=sys.argv

#test_sentence表示需要识别情绪的句子,在识别过程中通过读取交互控制台

#输入的命令中的最后一个参数获得

test_sentence=argvs[-1]

#载入训练好的模型

print("loading model......")

model=load_model('lstm.h5')

model.compile(loss='binary_crossentropy',

optimizer='adam',metrics=['accuracy'])

#调用lstm模块中的转换函数,把识别对象转换成与模型的输入数据相符的格式

print('当前文本为:',test_sentence)

data=input_transform(test_sentence)

data.reshape(1,-1)

#调用训练好的情绪识别网络进行预测,并打印预测结果

result=model.predict_classes(data)

if result[0][0]==1:

print("测试文本为正面情绪")

else:

print("测试文本为负面情绪")

将上述模块保存为“useit.py”,在IPython控制台输入下述命令,使用训练好的网络进行新文本的情绪判别。下面的示例中输出的判别结果是令人满意的。读者可尝试输入其他评论进行情绪识别,但是需要说明,因为这个模型并未精细**,所以出现识别错误是正常的,读者对深度学习掌握得更熟练后可尝试新的方法训练更好的情绪识别网络。

In[2]:run useit.py "好好好"

loading model......

当前文本为:好好好

测试文本为正面情绪

In[3]:run useit.py "太差了"

loading model......

当前文本为:太差了

测试文本为负面情绪

为了帮助读者训练效果更好的词向量,本节最后提供一个适用于本案例的词向量训练模块供读者参考。在这个模块中以“语料.txt”代表用来训练词向量的语料文件,读者需要通过自己收集数据建立这个语料文件。

import sys

import os

import codecs

import multiprocessing

import numpy as np

from gensim.models.word2vec import Word2Vec

from gensim.corpora.dictionary import Dictionary

from gensim.models.word2vec import LineSentence

from keras.preprocessing import sequence

np.random.seed(1337)#固定随机数种子,保证结果可复制

import jieba

import logging

sys.setrecursionlimit(1000000)

#设置网络参数

vocab_dim=100 #训练完毕后词向量的维数

maxlen=100

'''

下述参数表示对每个输入词向量训练函数的句子迭代的次数。这可以理解为用

来向训练函数中输入数据的迭代器的迭代次数,通常情况下,训练函数第一

次接收数据用来收集单词并计算词频,第二次及以后,用来做神经网络训练。

因为会迭代iterations+1次,所以此参数至少为1。也可以更大,用以增加对

每个输入的训练次数,但训练速度会更慢。现在模块的训练函数中,指明了

build_vocab和train操作,所以就是训练一次。这样做,而不是直接用

gensim.models.Word2Vec(corpus)是为了可以处理输入数据不能重复的情

况,扩展性更好。

'''

n_iterations=1

n_exposures=10 #训练中,出现频率低于10次的词会被忽略

window_size=10 #训练中考虑的上下文的最大长度

batch_size=32

n_epoch=10

input_length=100

#并行cpu的数量,可设置为cpu的核心数量

cpu_count=multiprocessing.cpu_count()

#定义加载语料库函数

def loadcorpus():

#读取语料库,文件格式txt,编码utf-8

corpus=codecs.open('语料_sep.txt','w','utf-8')

source=codecs.open("语料.txt",encoding='utf-8')

line=source.readline()

#分词

while line!="":

line=line.rstrip('\n')

seg_list=jieba.cut(line,cut_all=False)#精确模式

output=' '.join(list(seg_list))#空格拼接

corpus.write(output+' ')#空格取代换行'\r\n'

line=source.readline()

else:

corpus.write('\r\n')

source.close()

corpus.close()

return corpus

#词典创建函数,返回词索引、词向量以及每个句子所对应的词语索引

def corpus_dict(model=None,

corpus=None):#不限制模型和语料库数据格式

#如果模型和语料库正确输入,返回相应的值,否则显示无输入数据

if(corpus is not None)and(model is not None):

gensim_dict=Dictionary()

gensim_dict.doc2bow(model.wv.vocab.key(),

allow_update=True)

#计算在文档中,每个关键词出现的频率并用稀疏矩阵的方式返回结果。例如(0,1)

#(1,1)...表达的意思是此文档中,出现了词典中的第0个词1次,出现了第

#1个词1次,依此类推。另外,允许增加新的文档来更新这个稀疏矩阵

w2indx={v:k+1 for k,v in gensim_dict.items()}

#所有频数超过10的词的索引k和v代表key和value,遍历词典中所有元素

#w2vec={word:model[word] for word in w2indx.keys()}所有

#频数超过10的词的词向量

def rebuild_corpus(corpus):

#用词的频率和索引重新描述语料库

data=[]

for sentence in corpus:

new_txt=[]

for word in sentence:

try:

new_txt.append(w2indx[word])

except:

new_txt.append(0)

data.append(new_txt)

return data

corpus=rebuild_corpus(corpus)

#对语料的句子进行处理,每个句子允许的最大长度为maxlen,

#超过这个值的句子会被截断,短于这个值的句子会用0填充

#可用参数控制截断和填充从头开始还是从尾进行

corpus=sequence.pad_sequences(corpus,maxlen=maxlen)

return w2indx,w2vec,corpus

else:

print("No data provided...")

#定义词向量训练函数

#创建词典,返回词索引、词向量以及每个句子所对应的词语索引

def word2vec_train(corpus):

program=os.path.basename(sys.argv[0])

logger=logging.getLogger(program)

logging.basicConfig(format='%(asctime)s:%(levelname)

s:%(message)s')

}bl  logging.root.setLevel(level=logging.INFO)

logger.info("running%s"%' '.join(sys.argv))

model=Word2Vec(LineSentence(corpus),

size=vocab_dim,

min_count=n_exposures,

window=window_size,

workers=cpu_count,

iter=n_iterations)

#保存词向量模型

model.save('word2vec_model.pkl')

#生成词向量并保存

def corpus_wv():

print("Loading corpus...")

corpus=codecs.open("语料_sep.txt",encoding='utf-8')

print(len(corpus))#载入语料库并显示语料库长度

#调用词向量训练函数,训练词向量

print("Training a Word2vec model of corpus...")

word2vec_train(corpus)

print("Vectors of corpus are built successfully.")

#定义主函数

if __name__=='__main__':

corpus_wv()