简介
这次的任务是输入一串评价的字符串,得出其情感是正向的还是否定的。数据来源于Kaggle,搜索SI650就可以找到。
先看一下数据长什么样:
1 I loved the Da Vinci Code, but now I want something better and different!..
1 i thought da vinci code was great, same with kite runner.
1 The Da Vinci Code is actually a good movie...
1 I thought the Da Vinci Code was a pretty good book.
0 Brokeback Mountain is fucking horrible..
0 Then snuck into Brokeback Mountain, which is the most depressing movie I have ever seen..
0 , she helped me bobbypin my insanely cool hat to my head, and she laughed at my stupid brokeback mountain cowboy jokes..
标记为0的即为负面评价,否则为正面评价。思路比较简单,将单词的ID转换为更具有信息量的Vector,然后再用RNN来学习每个单词之间的关系,这个关系代表了句子的信息,最后用全连接层来输出对情感的预测。
预处理
这次相比于上次的字符级别文本生成来说,需要对单词进行处理,并需要统计一句话的最大长度,来决定RNN网络的Input形状;统计出现的单词数量来决定输入到embedding
层中向量的序号范围。
代码中用到了nltk.word_tokenize
来把句子拆成单词,用collections.Counter
来统计每个单词出现的次数,len(word_freqs)
即为出现过的单词数量。
import nltk
import collections
maxlen=0
word_freqs=collections.Counter()
num_recs=0
with open("train.txt",'r') as f:
for line in f:
label,sentence=line.strip().split('\t')
words=nltk.word_tokenize(sentence.lower())
if(len(words)>maxlen):
maxlen=len(words)
for word in words:
word_freqs[word]+=1
# 用于统计train有多少条数据
num_recs+=1
> print(maxlen)
42
> print(len(word_freqs))
2328
Embedding
由预处理结果,我们将单词个数限制在2000个,RNN网络输入的序列长度定为40,对于测试集中未达到该长度的句子进行填充,所以还需要一个伪单词PAD
;超出2000个单词范围之外的单词设为伪单词UNK(Unknown)
# input word转为int
MAX_FEATURES=2000
MAX_SEN_LEN=40
vocab_size=MAX_FEATURES+2
# most_common返回形式为[{'a':3},{'b':4}]
# x[0]为取出对应的字符
word2index={x[0]:i+2 for i,x in
enumerate(word_freqs.most_common(MAX_FEATURES))}
word2index['PAD']=0
word2index['UNK']=1
index2word={v:k for k,v in word2index.items()}
下面的代码用到了keras.preprocessing.sequence
中的pad_sequences
,可以很方便的将序列补0,成为想要的长度。
import keras
from keras.layers.core import Activation,Dense,Dropout,SpatialDropout1D
from keras.layers.embeddings import Embedding
from keras.layers import LSTM
from keras.models import Sequential
from sklearn.model_selection import train_test_split
from keras.preprocessing import sequence
from keras.activations import sigmoid
from keras.losses import binary_crossentropy
from keras.optimizers import Adam
import numpy as np
X=np.empty((num_recs,),dtype=list)
y=np.zeros((num_recs,))
with open("train.txt",'r') as f:
i=0
for line in f:
label,sentence=line.strip().split('\t')
words=nltk.word_tokenize(sentence.lower())
seqs=[]
for w in words:
if (w in word2index.keys()):
seqs.append(word2index[w])
else:
seqs.append(word2index['UNK'])
X[i]=seqs
y[i]=int(label)
i+=1
# 用于统计train有多少条数据
X=sequence.pad_sequences(X,maxlen=MAX_SEN_LEN)
训练
注意新加了一层Embedding
层,用于将单词的ID转为某一向量,可以参考搜索Word2Vec。单词ID的范围最大值即vocab_size
,该向量的长度即为EMBEDDING_SIZE
。因为句子中单词具有相关性,进行普通的针对单个神经元连接的Dropout
不合适,采用SpatialDropout1D
用于整体进行Dropout。
Xtrain,Xtest,ytrain,ytest=train_test_split(X,y,test_size=0.2)
EMBEDDING_SIZE=128
HIDDEN_LAYER_SIZE=64
BATCH_SIZE=32
NUM_EPOCHS=10
model=Sequential()
model.add(Embedding(vocab_size,EMBEDDING_SIZE,
input_length=MAX_SEN_LEN))
model.add(SpatialDropout1D(0.2))
model.add(LSTM(HIDDEN_LAYER_SIZE,dropout=0.2,recurrent_dropout=0.2))
model.add(Dense(1))
# Softmax用于多分类,Sigmoid用于二分类
model.add(Activation(sigmoid))
model.compile(loss=binary_crossentropy,optimizer=Adam(),metrics=['accuracy'])
另外需要注意因为是多对一的RNN模型,最后一层全连接仅加在最后一个时间步上。如下图,连接在最后一个时间步上的64个LSTM Cell上。如果是多对多的模型,则需要另外类型的全连接层,参考下一篇文章。
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, 40, 128) 256256
_________________________________________________________________
spatial_dropout1d_1 (Spatial (None, 40, 128) 0
_________________________________________________________________
lstm_1 (LSTM) (None, 64) 49408
_________________________________________________________________
dense_1 (Dense) (None, 1) 65
_________________________________________________________________
activation_1 (Activation) (None, 1) 0
=================================================================
训练
history=model.fit(Xtrain,ytrain,batch_size=BATCH_SIZE,epochs=NUM_EPOCHS,
validation_data=(Xtest,ytest))
结果如下
Epoch 9/10
5668/5668 [==============================] - 11s 2ms/step - loss: 0.0026 - acc: 0.9991 - val_loss: 0.0333 - val_acc: 0.9901
Epoch 10/10
5668/5668 [==============================] - 11s 2ms/step - loss: 2.0604e-04 - acc: 1.0000 - val_loss: 0.0341 - val_acc: 0.9901
验证集达到了99%的准确率。
应用测试
我们来自己测试一下效果,
Xtest=np.empty((4,),dtype=list)
sentences=['this is a good movie','what a bad shit','abcdefg','give me five']
for i,s in enumerate(sentences):
words=nltk.word_tokenize(s.lower())
seqs=[]
for w in words:
if (w in word2index.keys()):
seqs.append(word2index[w])
else:
seqs.append(word2index['UNK'])
Xtest[i]=seqs
Xtest=sequence.pad_sequences(Xtest,maxlen=MAX_SEN_LEN)
> model.predict(Xtest)
array([[0.8109143 ],
[0.0062162 ],
[0.00602398],
[0.5252423 ]], dtype=float32)
可以看出对this is a good movie
评价很好,对于what a bad shit
评价就属于负面了,而模棱两可的give me five
评分则处于中间的0.5,未出现的单词UNK则判定在0左右,效果还算不错。
改进方案
1、训练好的Embedding层
第一个问题就是这个情感语料库里面的文本可能不太够,导致Embedding层对单词的理解不够透彻。所以我们可以选择Google基于维基百科训练好的Word2Vec模型再来调整。这里需要做的事情就是把训练好的模型的形状适应到我们的任务上来。通过gensim加载模型之后设置Embedding层的权重为该模型即可。
2、双向LSTM
当我们说一句话的时候后面的词也会影响前面的词,比如那边的那只鸟
里面的只
,,所以可以采用双向的模型将评论后续的文本信息加入考虑。
思考
思考网络结构的时候可以在脑海中产生一幅RNN网络按时间步展开的图像,比如在这篇文章里面的多对一模型,就仅仅是在最后一个时间步加上全连接层用做预测。
这里用到了LSTM作为基本神经元,其结构如下:
(图片来自http://speech.ee.ntu.edu.tw/~tlkagk/courses_MLDS17.html)
为了解决梯度消失的问题设计了类似残差网络的“高速通道”,使神经元对长程输入的记忆能力增强,且由于增加了遗忘门、输入门、输出门等机制,使模型适应训练集的能力得到了增强。