0%

情感分析大作业笔记

前言

这次的信息内容安全课程期末大作业选题定为了微博热点爬取+情感分析。整合起来就有点像一个舆论分析玩具。具体要涉及到的东西也蛮多,这里我负责情感分析的部分,所以将用到的东西整理到这里。

主要参考到的是一个 GitHub 的 Repo : https://github.com/BUPTLdy/Sentiment-Analysis 。作者做了 SVM 和 LSTM 的情感极性分析。而我们的大作业选题中就包括使用 LSTM 进行情感分析,所以这里我主要以他的代码作为参考。

环境要求

  • Unix/Linux系统
  • python 2.7
  • python包安装: keras,sklearn,gensim,jieba,h5py,numpy,pandas

然后作者使用的是 python 2.7 + Linux 。看来搞这些还是 Linux 稳啊,但是因为打算现在 Windows 上试试,(最后搞不出来就得不要脸地和实验室借 Linux 服务器了),所以现在自己的 Windows 主机上搞一搞。

Windows

环境准备

首先就是安各种库了,这里我用的是前不久安得 Python 3.8.2,已经安装了部分的包,基本就是无脑 pip 即可。

值得一提的是 Keras , 去官网简单看了看貌似被集成在了 TensorFlow,于是又去先安 TensorFlow。 TensorFlow 也是蛮大的包,最新的是 2.2,然后默认就是 GPU 版本,我的电脑上安装的是 NVIDIA 的 GeForce 1050,应该是要比 CPU 好使的,所以就研究了一下 GPU 的使用。

之前由于使用 PyTorch 已经安装了最新的 CUDA 10.2,结果发现最新的 TensorFlow 适配的 CUDA 居然是 10.1 。太难过了,又要 C 盘再安几个 G。而且看了看貌似 Keras 和 TensorFlow 官网上声明支持的python版本最高也是 3.6, 3.7 。用 3.8 可能还会有各种风险,太难了。不过先往后做吧,出问题再说。

安装完各种东西以后测试一下 TensorFlow 对于 GPU 的支持性。

1
2
3
import tensorflow as tf

tf.test.gpu_device_name()

如果遇到了 W 开头的输出,也就是 Warning,就要自己复制到百度搜一搜解决方案了。

代码阅读

好像安了大部分了,但是还有一些等一会儿出问题了再安,先过一遍代码,看看能不能屡清楚思路,(当然肯定是不能的),然后遇到哪里,学哪里。(时间有限迫不得已的办法,有时间的童鞋还是应该系统学习。)

1
2
3
import yaml
import sys
reload(sys)

要注意一点, yaml 要使用 pip install pyyaml 进行安装。

然后有一个 reload(sys) 的操作报错了。参考这篇文章:https://blog.csdn.net/mighty13/article/details/98670394。这里我干脆先注释掉了,应该就是 Python2 编码相关的问题。https://www.cnblogs.com/bestween/p/11186988.html

1
from sklearn.cross_validation import train_test_split

这条语句继续报错,改成下面的代码即可。

1
from sklearn.model_selection import train_test_split

https://www.cnblogs.com/Yanjy-OnlyOne/p/11288098.html 是 train_test_split 的教程。大致上是给定数据集划分测试集与训练集的工具。

1
2
3
4
import multiprocessing
import numpy as np
from gensim.models.word2vec import Word2Vec
from gensim.corpora.dictionary import Dictionary

这几句 import 不再解释, gensim 是 NLP 的库。

1
2
3
4
5
6
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
from keras.models import model_from_yaml

这一段就是 import 了 很多 keras 的库,这里 PyCharm 智能地提醒了我这里需要用 Keras 1.1.1,而我的 Keras 是2.3.1。先无视吧,有兼容性问题再上网找。

1
2
3
4
5
np.random.seed(1337)  # For Reproducibility
import jieba
import pandas as pd
import sys
sys.setrecursionlimit(1000000)

然后为了可以复现,设定随机数种子 1337 。引入了结巴分词,pandas。设定了递归上限。

1
2
3
4
5
6
7
8
9
10
# set parameters:
vocab_dim = 100
maxlen = 100
n_iterations = 1 # ideally more..
n_exposures = 10
window_size = 7
batch_size = 32
n_epoch = 4
input_length = 100
cpu_count = multiprocessing.cpu_count()

这段是一些函数参数变量的声明,在后面用到了再回头分析。

1
2
3
4
5
6
7
8
9
#加载训练文件
def loadfile():
neg=pd.read_excel('data/neg.xls',header=None,index=None)
pos=pd.read_excel('data/pos.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

首先加载训练文件,作者的数据集都是 Excel 存储的,已经存储在了 Github 的 Repo 里。然后将积极的与消极的评论连接起来,并且整合出一个 y ,所有积极的对应 1, 消极的对应 0 。

1
2
3
4
5
6
7
8
#对句子经行分词,并去掉换行符
def tokenizer(text):
''' Simple Parser converting each document to lower-case, then
removing the breaks for new lines and finally splitting on the
whitespace
'''
text = [jieba.lcut(document.replace('\n', '')) for document in text]
return text

这里就是调用结巴分词的API了。遍历 text 中的 每个 document ,这里把所有的换行换成空格。然后使用 lcut,jieba.lcut 直接生成的就是一个list。

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
26
27
28
29
30
31
32
33
34
#创建词语字典,并返回每个词语的索引,词向量,以及每个句子所对应的词语索引
def create_dictionaries(model=None,
combined=None):
''' Function does are number of Jobs:
1- Creates a word to index mapping
2- Creates a word to vector mapping
3- Transforms the Training and Testing Dictionaries

'''
if (combined is not None) and (model is not None):
gensim_dict = Dictionary()
gensim_dict.doc2bow(model.vocab.keys(),
allow_update=True)
w2indx = {v: k+1 for k, v in gensim_dict.items()}#所有频数超过10的词语的索引
w2vec = {word: model[word] for word in w2indx.keys()}#所有频数超过10的词语的词向量

def parse_dataset(combined):
''' Words become integers
'''
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)#每个句子所含词语对应的索引,所以句子中含有频数小于10的词语,索引为0
return w2indx, w2vec,combined
else:
print ('No data provided...')

这里最后一行的 print 改成了 Python3 的语法,后面不再重复。首先做了个条件判断看看有没有输入。然后使用了 gensim 库 的 Dictionary 类。这里扔上来 Dictionary 类的 官方API 。doc2bow 函数用来将 文档转换为 BOW即 Bag of words ,词袋。词袋的内容就是一个 dict ,key 是 token 的 id,value 是 token 的count。

至于 model 传入的是什么类我们还不清楚,往后看。 gensim_dict.items 就像是 python自己的dict,返回的是键值对。但是还看不懂哪里做了注释中提到的筛选,说是只有频数超过10的词语。pad_sequences 函数用来将序列补全,一般补0, https://blog.csdn.net/wcy23580/article/details/84957471。

1
2
3
4
5
6
7
8
9
10
11
12
13
#创建词语字典,并返回每个词语的索引,词向量,以及每个句子所对应的词语索引
def word2vec_train(combined):

model = Word2Vec(size=vocab_dim,
min_count=n_exposures,
window=window_size,
workers=cpu_count,
iter=n_iterations)
model.build_vocab(combined)
model.train(combined)
model.save('lstm_data/Word2vec_model.pkl')
index_dict, word_vectors,combined = create_dictionaries(model=model,combined=combined)
return index_dict, word_vectors,combined

不过还好作者似乎在不同函数中的局部变量都使用的是一样的名字。这里我们就看到了 model 是使用 Word2Vec 类实例化的变量。 Word2Vec是一个 gensim的库,找一找 API。https://radimrehurek.com/gensim/models/word2vec.html#gensim.models.word2vec.Word2Vec,参数的 min_count = n_exposures ,也就是 10,意思表示出现频率低于 10 的词会被忽视,怪不得上一个函数的注释说道只有频数超过10的词语。然后进行训练。最后将模型保存起来,然后调用前面的函数。

1
2
3
4
5
6
7
8
9
def get_data(index_dict,word_vectors,combined,y):

n_symbols = len(index_dict) + 1 # 所有单词的索引数,频数小于10的词语索引为0,所以加1
embedding_weights = np.zeros((n_symbols, vocab_dim))#索引为0的词语,词向量全为0
for word, index in index_dict.items():#从索引为1的词语开始,对每个词语对应其词向量
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

这里就是获取数据的函数(顾名思义了hhh),然后这里的代码好像也没有特别的API需要查,大致就是构建了一个词向量的矩阵,然后分割了训练集与测试集,最后返回了所有单词的索引加上0,词向量的矩阵,以及训练集、测试集。

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
26
27
28
29
30
##定义网络结构
def train_lstm(n_symbols,embedding_weights,x_train,y_train,x_test,y_test):
print ('Defining a Simple Keras Model...')
model = Sequential() # or Graph or whatever
model.add(Embedding(output_dim=vocab_dim,
input_dim=n_symbols,
mask_zero=True,
weights=[embedding_weights],
input_length=input_length)) # Adding Input Length
model.add(LSTM(output_dim=50, activation='sigmoid', inner_activation='hard_sigmoid'))
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, nb_epoch=n_epoch,verbose=1, validation_data=(x_test, y_test),show_accuracy=True)

print ("Evaluate...")
score = model.evaluate(x_test, y_test,
batch_size=batch_size)

yaml_string = model.to_yaml()
with open('lstm_data/lstm.yml', 'w') as outfile:
outfile.write( yaml.dump(yaml_string, default_flow_style=True) )
model.save_weights('lstm_data/lstm.h5')
print ('Test score:', score)

这里就到了定义神经网络的部分了,感觉要开始硬核了。什么理论知识都没得,只好看见什么查什么了。首先model是一个序列模型。然后是 Embedding。

https://keras.io/zh/layers/embeddings/

https://arxiv.org/pdf/1512.05287.pdf

https://blog.csdn.net/jiangpeng59/article/details/77533309 看这篇

https://github.com/MoyanZitto/keras-cn/blob/master/docs/legacy/blog/word_embedding.md

理解了一下大概就是,这个层只可以做输入层,然后可以用来获取词向量。把输入的词的索引输出成词向量。那个mask_zero 参数的意思可以参照文档,我暂时还理解的比较模糊就不乱说了……

然后下一层 LSTM,太硬了,一会儿回头再理解吧,大概就是核心了。

然后下一层 Dropout。

http://www.jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf Dropout 的 paper

这个 Dropout 有种陌生又熟悉的感觉……可能之前查文献查到过这个,大致的思想就是一个神经元的输入可以随机扔掉几个输入,按照比例,然后这样可以防止过拟合。(太感人了,和最近在刷的吴恩达老师的机器学习联系上了,过拟合一般的原因就是参数过多,样本较少,较好的解决办法是正则化)

然后 Dense 层就是全连接层。

Activation 是激活函数,这里用的 sigmoid 函数。

然后编译模型,目标函数用了 交叉熵,又遇到了盲点。

https://blog.csdn.net/Julialove102123/article/details/80236180 损失函数:sigmoid与binary_crossentropy,以及softmax与categorical_crossentropy的关系

https://blog.csdn.net/wtq1993/article/details/51741471 交叉熵代价函数(cross-entropy cost function)

具体的交叉熵函数如下,其中 x 是 输入, y 是期望输出, a是 实际输出。

img

关于优化方法,这里使用的是 Adam,再次盲点,扔连接。似乎常用的都是 Adam。

https://www.cnblogs.com/GeekDanny/p/9655597.html 介绍各类优化方法

https://keras.io/api/optimizers/adam/

关于 fit 和 evaluate,参见官方文档。https://keras.io/zh/models/sequential/。

最后将模型输出为了 yaml,然后输出了一下 evaluate 的分数。将模型的参数(权重)保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
#训练模型,并保存
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)

这个函数应该就是一个整合函数了,调用一次就可以完成所有的准备以及训练。

首先加载 xls 表格的数据,然后进行 Tokenize 分词,然后将分词送入 word2vec,训练词向量模型。然后将词向量整理成矩阵,然后分割测试集训练集,送入 lstm 模型训练。

1
2
3
4
5
6
def input_transform(string):
words=jieba.lcut(string)
words=np.array(words).reshape(1,-1)
model=Word2Vec.load('lstm_data/Word2vec_model.pkl')
_,_,combined=create_dictionaries(model,words)
return combined

这里应该是一个实际应用时的 输入转化,用来将输入的字符串分词,然后转化成向量,然后加载 Word2Vec 模型,最后用模型获取输入的字符串的向量表示(可以送入lstm的向量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def lstm_predict(string):
print ('loading model......')
with open('lstm_data/lstm.yml', 'r') as f:
yaml_string = yaml.load(f)
model = model_from_yaml(yaml_string)

print ('loading weights......')
model.load_weights('lstm_data/lstm.h5')
model.compile(loss='binary_crossentropy',
optimizer='adam',metrics=['accuracy'])
data=input_transform(string)
data.reshape(1,-1)
#print data
result=model.predict_classes(data)
if result[0][0]==1:
print (string,' positive')
else:
print (string,' negative')

然后这里就是加载模型及其参数,然后将输入的字符串用上一个函数转化一下,然后送入模型。判断结果是1或者0.

运行测试

终于分析完了,太感人了,虽然LSTM最关键的地方没有分析,但是对于整个代码的结构已经有了一个概念。赶紧跑一下这个代码试试,不过由于他的代码环境和我的完全不一样,不敢保证出现其他的问题。

一测试就出了 Bug。

1
ImportError: Missing optional dependency 'xlrd'. Install xlrd >= 1.0.0 for Excel support Use pip or conda to install xlrd.

还算好,能直接告诉怎么解决。

1
pip install xlrd

继续下一个 bug。

1
2
3
4
5
6
7
8
9
10
11
12
    index_dict, word_vectors,combined=word2vec_train(combined)
File "F:\InfoContentSecurityProject\Sentiment-Analysis\Sentiment_lstm.py", line 101, in word2vec_train
model.train(combined)
File "***\Python\Python38\lib\site-packages\gensim\models\word2vec.py", line 907, in train
return super(Word2Vec, self).train(
File "***\Python\Python38\lib\site-packages\gensim\models\base_any2vec.py", line 1077, in train
return super(BaseWordEmbeddingsModel, self).train(
File "***\Python\Python38\lib\site-packages\gensim\models\base_any2vec.py", line 533, in train
self._check_training_sanity(
File "***\Python\Python38\lib\site-packages\gensim\models\base_any2vec.py", line 1199, in _check_training_sanity
raise ValueError(
ValueError: You must specify either total_examples or total_words, for proper job parameters updationand progress calculations. The usual value is total_examples=model.corpus_count.

按照提示,将 简单的 train 改成如下代码。

1
model.train(combined, total_examples=model.corpus_count)

再次报错

1
ValueError: You must specify an explict epochs count. The usual value is epochs=model.epochs.

https://www.cnblogs.com/chenlove/p/9911762.html : 在 model 后面加上 mv 就好了。

1
gensim_dict.doc2bow(model.mv.vocab.keys(), allow_update = True)

再次报错

1
2
3
4
5
6
  File "F:\InfoContentSecurityProject\Sentiment-Analysis\Sentiment_lstm.py", line 135, in train_lstm
model.fit(x_train, y_train, batch_size=batch_size, nb_epoch=n_epoch,verbose=1, validation_data=(x_test, y_test),show_accuracy=True)
File "***\Python\Python38\lib\site-packages\keras\engine\training.py", line 1118, in fit
raise TypeError('Unrecognized keyword arguments: ' + str(kwargs))
TypeError: Unrecognized keyword arguments: {'show_accuracy': True}

https://stackoverflow.com/questions/46115403/typeerror-unrecognized-keyword-arguments-show-accuracy-true-yelp-challen 大致就是老版本的 Keras 可以用. Accuracy 应该放在 compile 的参数里。用 metrics,这里前面的代码已经放了这个参数,所以我的解决方案就是先上吊 show_accuracy 试试。

插播一个吐槽,这个测试要十几秒才能跑到下一个 bug ,还是相对有一些麻烦的。

哦豁,跑起来了,debug到这里居然跑起来了,我都不知道现在是跑的哪一个的训练,预计时间貌似还走的挺准的,估计了4分钟多一点。要是跑完了再出 bug 我就太难过了。我滴个鬼鬼,一个 epoch 要跑4分钟,设置了 epoch 是 4,总共要 16分钟…… 这要是一会儿出 bug 了就得改代码直接读 model 了,训练属实搞不起啊。

终于跑完了,后面居然没有bug,太感人了。就 pyyaml 库报了一个 warning。

https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation

1
yaml_string = yaml.load(f, Loader=yaml.FullLoader)

上面的 load 函数如果没有指定 Loader 就会报 warning 的,不过不影响,看了一下上面的网站,大概就是直接用 FullLoader还是安全的。就OK了,主要是担心加载 yaml 文件是恶意的。

MISC

基本上流程跑完一遍了,也将 GitHub 上的代码的兼容性问题解决。这里记录一些乱七八糟的。

Word2Vec Model

Word2Vec 看了 Stanford 的 CS224n,当看到用词向量做类推时震惊了,国王减男人加女人等于皇后。居然能用向量存储这些隐含信息,太有意思了。

然后由于语料和算力有限,玩了玩人家公开的model就没再看,今天做起来这个作业想起来这个模型,上网搜了一下中文的model。 https://www.jianshu.com/p/ae5b45e96dbf,结果看到了这个,文中有下载链接可以玩。

现在还在下,百度云120KB的速度属实地道。

Pandas 合并2个 Dataframe

在整理数据时,需要将多个数据来源的数据整理起来用来训练。

搜了一下有蛮多函数可以干这个事情,最后使用 pandas.concat 函数来做这个事情。

但是合并以后输出的 Dataframe是两列,原来都是1列,现在变成2列。

最后发现是因为读取 Dataframe 的时候,第一行数据被当做了 Header,两个 Dataframe 的 Header 不一样,所以变成了两个列,第一个 Dataframe 的第二个列补零,第二个 Dataframe 的第一个列补零。

只需要在读取 Dataframe 时,使用 Header = None 即可。

1
2
3
4
5
6
7
8
import pandas as pd

pos_original =pd.read_excel('data/pos.xls', header=None, index=None)
pos_steam = pd.read_table('data/pos_steam.txt',sep='\n', header = None)
pos_60000 = pd.read_table('data/pos60000.txt',sep='\n', header = None)


pos = pd.concat([pos_original, pos_steam, pos_60000], ignore_index= True)

Keras 模型可视化

使用 pydot 库,样例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# encoding=utf-8

from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation
from keras.layers.embeddings import Embedding

from keras.utils import plot_model

model = Sequential()
model.add(Embedding(input_dim=1024, output_dim=256, input_length=50))
model.add(Dropout(0.5))
model.add(Dense(16))
model.add(Dense(1))
model.add(Activation('sigmoid'))

plot_model(model, to_file='lstm_data/model_test.png', show_shapes=True)

这时候几乎一定会报错,然后就是安装各种包。

1
2
3
pip install pydot-ng
pip install graphviz
pip install pydot

安装完后又要安装一个库,http://www.graphviz.org/download/。直接下载 msi 安装程序,安装。

安装完成后会提示你把安装目录的 bin 目录加入 PATH。然后这个时候你就会发现各种各样的问题。

这里拿出我的解决方案。不会改很多的源码。其实PATH也不用添加。

点击报错时的路径,比如 C:\Users\xxx\AppData\Local\Programs\Python\Python38\Lib\site-packages\pydot.py。然后修改这个文件,

1
2
3
4
5
6
7
8
9
    'plain-ext', 'png', 'ps', 'ps2',
'svg', 'svgz', 'vml', 'vmlz',
'vrml', 'vtx', 'wbmp', 'xdot', 'xlib']

self.prog = 'dot'

# Automatically creates all
# the methods enabling the creation
# of output in any of the supported formats.

找到类似上面的代码片段,将 self.prog 后面的赋值更改C:\Program Files (x86)\Graphviz2.38\bin\dot.exe。记得反斜杠要2个转义。

1
2
3
4
5
6
7
8
9
    'plain-ext', 'png', 'ps', 'ps2',
'svg', 'svgz', 'vml', 'vmlz',
'vrml', 'vtx', 'wbmp', 'xdot', 'xlib']

self.prog = 'C:\\Program Files (x86)\\Graphviz2.38\\bin\\dot.exe'

# Automatically creates all
# the methods enabling the creation
# of output in any of the supported formats.

改成这样就OK了。

PS:学会一个不重启更新 Windows 环境变量的方法,打开任务管理器,找到 Windows 资源管理器,选中,重启,就可以不重启更新环境变量了。