记录一次半监督的情感分析
By 苏剑林 | 2017-05-04 | 52749位读者 |本文是一次不怎么成功的半监督学习的尝试:在IMDB的数据集上,用随机抽取的1000个标注样本训练一个文本情感分类模型,并且在余下的49000个测试样本中,测试准确率为73.48%。
思路 #
本文的思路来源于OpenAI的这篇文章:
《OpenAI新研究发现无监督情感神经元:可直接调控生成文本的情感》
文章里边介绍了一种无监督(实际上是半监督)做情感分类的模型的方法,并且实验效果很好。然而文章里边的实验很庞大,对于个人来说几乎不可能重现(在4块Pascal GPU花了1个月时间训练)。不过,文章里边的思想是很简单的,根据里边的思想,我们可以做个“山寨版”的。思路如下:
我们一般用深度学习做情感分类,比较常规的思路就是Embedding层+LSTM层+Dense层(Sigmoid激活),我们常说的词向量,相当于预训练了Embedding层(这一层的参数量最大,最容易过拟合),而OpenAI的思想就是,为啥不连LSTM层一并预训练了呢?预训练的方法也是用语言模型来训练。当然,为了使得预训练的结果不至于丢失情感信息,LSTM的隐藏层节点要大一些。
如果连LSTM层预训练了,那么剩下的Dense层参数就不多了,因此可以用少量标注样本就能够训练完备了。这就是整个半监督学习的思路了。至少OpenAI文章说的什么情感神经元,那不过是形象的描述罢了。
当然,从情感分析这个任务上来看,本文的73.48%准确率实在难登大雅之台,随便一个“词典+规则”的方案,都有80%以上的准确率。我只是验证了这种实验方案的可行性。我相信,如果规模能做到OpenAI那么大,效果应该会更好的。而且,本文想描述的是一种建模策略,并非局限于情感分析,同样的思想可以用于任意的二分类甚至多分类问题。
过程 #
首先,加载数据集并且重新划分训练集和测试集:
from keras.preprocessing import sequence
from keras.models import Model
from keras.layers import Input, Embedding, LSTM, Dense, Dropout
from keras.datasets import imdb
from keras import backend as K
import numpy as np
max_features = 10000 #保留前max_features个词
maxlen = 100 #填充/阶段到100词
batch_size = 1000
nb_grams = 10 #训练一个10-gram的语言模型
nb_train = 1000 #训练样本数
#加载内置的IMDB数据集
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_lm_ = np.append(x_train, x_test)
#构造用来训练语言模型的数据
#这里只用了已有数据,实际环境中,可以补充其他数据使得训练更加充分
x_lm = []
y_lm = []
for x in x_lm_:
for i in range(len(x)):
x_lm.append([0]*(nb_grams - i + max(0,i-nb_grams))+x[max(0,i-nb_grams):i])
y_lm.append([x[i]])
x_lm = np.array(x_lm)
y_lm = np.array(y_lm)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
x = np.vstack([x_train, x_test])
y = np.hstack([y_train, y_test])
#重新划分训练集和测试集
#合并原来的训练集和测试集,随机挑选1000个样本,作为新的训练集,剩下为测试集
idx = range(len(x))
np.random.shuffle(idx)
x_train = x[idx[:nb_train]]
y_train = y[idx[:nb_train]]
x_test = x[idx[nb_train:]]
y_test = y[idx[nb_train:]]
然后搭建模型
embedded_size = 100 #词向量维度
hidden_size = 1000 #LSTM的维度,可以理解为编码后的句向量维度。
#encoder部分
inputs = Input(shape=(None,), dtype='int32')
embedded = Embedding(max_features, embedded_size)(inputs)
lstm = LSTM(hidden_size)(embedded)
encoder = Model(inputs=inputs, outputs=lstm)
#完全用ngram模型训练encode部分
input_grams = Input(shape=(nb_grams,), dtype='int32')
encoded_grams = encoder(input_grams)
softmax = Dense(max_features, activation='softmax')(encoded_grams)
lm = Model(inputs=input_grams, outputs=softmax)
lm.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
#用sparse交叉熵,可以不用事先将类别转换为one hot形式。
#情感分析部分
#固定encoder,后面接一个简单的Dense层(相当于逻辑回归)
#这时候训练的只有hidden_size+1=1001个参数
#因此理论上来说,少量标注样本就可以训练充分
for layer in encoder.layers:
layer.trainable=False
sentence = Input(shape=(maxlen,), dtype='int32')
encoded_sentence = encoder(sentence)
sigmoid = Dense(10, activation='relu')(encoded_sentence)
sigmoid = Dropout(0.5)(sigmoid)
sigmoid = Dense(1, activation='sigmoid')(sigmoid)
model = Model(inputs=sentence, outputs=sigmoid)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
好了,训练语言模型,这部分工作比较耗时:
#训练语言模型,比较耗时,一般迭代两三次就好
lm.fit(x_lm, y_lm,
batch_size=batch_size,
epochs=3)
语言模型的训练结果为
Epoch 1/3
11737946/11737946 [==============================] - 2400s - loss: 5.0376
Epoch 2/3
11737946/11737946 [==============================] - 2404s - loss: 4.5587
Epoch 3/3
11737946/11737946 [==============================] - 2404s - loss: 4.3968
接着,开始用1000个样本训练情感分析模型。由于前面已经预训练好,因此这里需要训练的参数不多,配合Dropout的话,因此1000个样本也不会导致严重过拟合。
#训练情感分析模型
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=200)
训练结果为
Epoch 198/200
1000/1000 [==============================] - 0s - loss: 0.2481 - acc: 0.9250
Epoch 199/200
1000/1000 [==============================] - 0s - loss: 0.2376 - acc: 0.9330
Epoch 200/200
1000/1000 [==============================] - 0s - loss: 0.2386 - acc: 0.9350
接着来评估一下模型:
#评估一下模型的效果
model.evaluate(x_test, y_test, verbose=True, batch_size=batch_size)
准确率73.04%,一般般~~试试迁移学习,把训练集连同测试集的预测结果一起进行训练:
#把训练集连同测试集的预测结果(即可能包含有误的数据),重新训练模型
y_pred = model.predict(x_test, verbose=True, batch_size=batch_size)
y_pred = (y_pred.reshape(-1) > 0.5).astype(int)
xt = np.vstack([x_train, x_test])
yt = np.hstack([y_train, y_pred])
model.fit(xt, yt,
batch_size=batch_size,
epochs=10)
#评估一下模型的效果
model.evaluate(x_test, y_test, verbose=True, batch_size=batch_size)
训练结果为
Epoch 8/10
50000/50000 [==============================] - 27s - loss: 0.1455 - acc: 0.9561
Epoch 9/10
50000/50000 [==============================] - 27s - loss: 0.1390 - acc: 0.9590
Epoch 10/10
50000/50000 [==============================] - 27s - loss: 0.1349 - acc: 0.9600
这次我们得到了73.33%的准确率。不难发现,事实上这个过程可以重复迭代,再重复一次,得到73.33%的准确率,重复第二次,得到73.47%...可以预料,这会趋于一个稳定值,我再重复了5次,稳定在73.48%。
从刚开始的73.04%,到迁移学习后的73.48%,约有0.44%的提升,看上去不大,但是如果对于做比赛或者写论文的同学来说,0.44%的提升,可以小书一笔了~
点评 #
文章开头已经说了,这次是个不大成功的尝试,毕竟是“山寨版”的,因此大家不要太纠结准确率不高的问题。而从本文的实验结果来看,这种方案是靠谱的。通过大量的、混合情感的语料训练语言模型,确实能够很好地提取文本的特征,这类似于图像的自编码过程。
本文做得很简单,没有细微调过超参。改进的思路大概有那么几个:增大语言模型的规模、增加情感语料(只需要是情感评论的,不需要标签)、训练细节上的优化。这个就暂时不做了~
转载到请包括本文地址:https://kexue.fm/archives/4374
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (May. 04, 2017). 《记录一次半监督的情感分析 》[Blog post]. Retrieved from https://kexue.fm/archives/4374
@online{kexuefm-4374,
title={记录一次半监督的情感分析},
author={苏剑林},
year={2017},
month={May},
url={\url{https://kexue.fm/archives/4374}},
}
January 3rd, 2018
那个准确率是怎么计算的
就是普通的准确率,预测正确的样本数除以总样本数。预测大于0.5就视为正样本,否则负样本
January 8th, 2018
是loss小于0.5的就是正样本吗?最近在学习深度学习的文本情感分析,对这些不是很清楚。
y_pred = model.predict(x_test, verbose=True, batch_size=batch_size)
y_pred = (y_pred.reshape(-1) > 0.5).astype(int)
model.predict输出的就是概率值(小数),(y_pred.reshape(-1) > 0.5).astype(int)就是对它进行整数化处理,大于0.5就视为正样本,否则负样本
其实你把代码调试成功跑通了,自然就知道输出是怎样的啦~(◎_◎;)
June 15th, 2018
训练语言模型的意义何在呢?
并且训练是用前10个单词预测第11个单词,LSTM的输入就代表了描述前10个单词的一个向量。
为什么不是输入不是所有的单词呢?即w1,w2,......wn预测w2,w3,.....wn? 更符合情感分类的输入呢?
LSTM输出的这个向量能适用于各种场景?如情感分类,文本分类等等,LSTM输入的向量是不是比其它论文提取的各种网络结构提取的句子向量效果弱一些?只是在这种场景下(标注数据少)而采取的一种折中办法? 先用语言模型这个普适办法提取句子向量,然后就可以对接各种场景了?