文本情感分类(三):分词 OR 不分词
By 苏剑林 | 2016-06-29 | 412759位读者 |去年泰迪杯竞赛过后,笔者写了一篇简要介绍深度学习在情感分析中的应用的博文《文本情感分类(二):深度学习模型》。虽然文章很粗糙,但还是得到了不少读者的反响,让我颇为意外。然而,那篇文章中在实现上有些不清楚的地方,这是因为:1、在那篇文章以后,keras已经做了比较大的改动,原来的代码不通用了;2、里边的代码可能经过我随手改动过,所以发出来的时候不是最适当的版本。因此,在近一年之后,我再重拾这个话题,并且完成一些之前没有完成的测试。
为什么要用深度学习模型?除了它更高精度等原因之外,还有一个重要原因,那就是它是目前唯一的能够实现“端到端”的模型。所谓“端到端”,就是能够直接将原始数据和标签输入,然后让模型自己完成一切过程——包括特征的提取、模型的学习。而回顾我们做中文情感分类的过程,一般都是“分词——词向量——句向量(LSTM)——分类”这么几个步骤。虽然很多时候这种模型已经达到了state of art的效果,但是有些疑问还是需要进一步测试解决的。对于中文来说,字才是最低粒度的文字单位,因此从“端到端”的角度来看,应该将直接将句子以字的方式进行输入,而不是先将句子分好词。那到底有没有分词的必要性呢?本文测试比较了字one hot、字向量、词向量三者之间的效果。
模型测试 #
本文测试了三个模型,或者说,是三套框架,具体代码在文末给出。这三套框架分别是:
1、one hot:以字为单位,不分词,将每个句子截断为200字(不够则补空字符串),然后将句子以“字-one hot”的矩阵形式输入到LSTM模型中进行学习分类;
2、one embedding:以字为单位,不分词,,将每个句子截断为200字(不够则补空字符串),然后将句子以“字-字向量(embedding)“的矩阵形式输入到LSTM模型中进行学习分类;
3、word embedding:以词为单位,分词,,将每个句子截断为100词(不够则补空字符串),然后将句子以“词-词向量(embedding)”的矩阵形式输入到LSTM模型中进行学习分类。
其中所用的LSTM模型结构是类似的。所用的语料还是《文本情感分类(二):深度学习模型》中的语料,以15000条进行训练,剩下的6000条左右做测试。意外的是,三个模型都取得了相近的结果。
$$\begin{array}{c|ccc}
\hline
&\text{one hot} & \text{one embedding} & \text{word embedding}\\
\hline
\text{迭代次数} & 90 & 30 & 30\\
\text{每轮用时} & 100s & 36s & 18s\\
\text{训练准确率} & 96.60\% & 95.95\% & 98.41\% \\
\text{测试准确率} & 89.21\% & 89.55\% & 89.03\% \\
\hline
\end{array}$$
可见,在准确率方面,三者是类似的,区分度不大。不管是用one hot、字向量还是词向量,结果都差不多。也许用《文本情感分类(二):深度学习模型》的方法来为每个模型选取适当的阈值,会使得测试准确率更高一些,但模型之间的相对准确率应该不会变化很大。
当然,测试本身可能存在一些不公平的情况,也许会导致测试结果不公平,而我也没有反复去测试。比如one hot的模型迭代了90次,其它两个模型是30次,因为one hot模型所构造的样本维度太大,需要经过更长时间才出现收敛现象,而且训练过程中,准确率是波动上升的,并非像其它两个模型那样稳定上升。事实上这是所有one hot模型的共同特点。
多扯一点 #
看上去,one hot模型的确存在维度灾难的问题,而且训练时间又长,效果又没有明显提升,那是否就说明没有研究one hot表示的必要了呢?
我觉得不是这样的。当初大家诟病one hot模型的原因,除了维度灾难之外,还有一个就是“语义鸿沟”,也就说任意两个词之间没有任何相关性(不管用欧式距离还是余弦相似度,任意两个词的计算结果是一样的)。可是,这一点假设用在词语中不成立,可是用在中文的“字”上面,不是很合理吗?汉字单独成词的例子不多,大多数是二字词,也就是说,任意两个字之间没有任何相关性,这个假设在汉字的“字”的层面上,是近似成立的!而后面我们用了LSTM,LSTM本身具有整合邻近数据的功能,因此,它暗含了将字整合为词的过程。
此外,one hot模型还有一个非常重要的特点——它没有任何信息损失——从one hot的编码结果中,我们反过来解码出原来那句话是哪些字词组成的,然而,我无法从一个词向量中确定原来的词是什么。这些观点都表明,在很多情况下,one hot模型都是很有价值的。
而我们为什么用词向量呢?词向量相当于做了一个假设:每个词具有比较确定的意思。这个假设在词语层面也是近似成立的,毕竟一词多义的词语相对来说也不多。正因为如此,我们才可以将词放到一个较低维度的实数空间里,用一个实数向量来表示一个词语,并且用它们之间的距离或者余弦相似度来表示词语之间的相似度。这也是词向量能够解决“一义多词”而没法解决“一词多义”的原因。
从这样看来,上面三个模型中,只有one hot和word embedding才是理论上说得过去的,而one embedding则看上去变得不伦不类了,因为字似乎不能说具有比较确定的意思。但为什么one embedding效果也还不错?我估计,这可能是因为二元分类问题本身是一个很粗糙的分类(0或1),如果更多元的分类,可能one embedding的方式效果就降下来了。不过,我也没有进行更多的测试了,因为太耗时间了。
当然,这只能算是我的主观臆测,还望大家指正。尤其是one embedding部分的评价,是值得商榷的。
代码来了 #
可能大家并不想看我胡扯一通,是直接来看代码的,现奉上三个模型的代码。最好有GPU加速,尤其是试验one hot模型,不然慢到哭了。
模型1:one hot
# -*- coding:utf-8 -*-
'''
one hot测试
在GTX960上,约100s一轮
经过90轮迭代,训练集准确率为96.60%,测试集准确率为89.21%
Dropout不能用太多,否则信息损失太严重
'''
import numpy as np
import pandas as pd
pos = pd.read_excel('pos.xls', header=None)
pos['label'] = 1
neg = pd.read_excel('neg.xls', header=None)
neg['label'] = 0
all_ = pos.append(neg, ignore_index=True)
maxlen = 200 #截断字数
min_count = 20 #出现次数少于该值的字扔掉。这是最简单的降维方法
content = ''.join(all_[0])
abc = pd.Series(list(content)).value_counts()
abc = abc[abc >= min_count]
abc[:] = list(range(len(abc)))
word_set = set(abc.index)
def doc2num(s, maxlen):
s = [i for i in s if i in word_set]
s = s[:maxlen]
return list(abc[s])
all_['doc2num'] = all_[0].apply(lambda s: doc2num(s, maxlen))
#手动打乱数据
#当然也可以把这部分加入到生成器中
idx = list(range(len(all_)))
np.random.shuffle(idx)
all_ = all_.loc[idx]
#按keras的输入要求来生成数据
x = np.array(list(all_['doc2num']))
y = np.array(list(all_['label']))
y = y.reshape((-1,1)) #调整标签形状
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.layers import LSTM
import sys
sys.setrecursionlimit(10000) #增大堆栈最大深度(递归深度),据说默认为1000,报错
#建立模型
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen,len(abc))))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer='rmsprop',
metrics=['accuracy'])
#单个one hot矩阵的大小是maxlen*len(abc)的,非常消耗内存
#为了方便低内存的PC进行测试,这里使用了生成器的方式来生成one hot矩阵
#仅在调用时才生成one hot矩阵
#可以通过减少batch_size来降低内存使用,但会相应地增加一定的训练时间
batch_size = 128
train_num = 15000
#不足则补全0行
gen_matrix = lambda z: np.vstack((np_utils.to_categorical(z, len(abc)), np.zeros((maxlen-len(z), len(abc)))))
def data_generator(data, labels, batch_size):
batches = [list(range(batch_size*i, min(len(data), batch_size*(i+1)))) for i in range(len(data)/batch_size+1)]
while True:
for i in batches:
xx = np.zeros((maxlen, len(abc)))
xx, yy = np.array(map(gen_matrix, data[i])), labels[i]
yield (xx, yy)
model.fit_generator(data_generator(x[:train_num], y[:train_num], batch_size), samples_per_epoch=train_num, nb_epoch=30)
model.evaluate_generator(data_generator(x[train_num:], y[train_num:], batch_size), val_samples=len(x[train_num:]))
def predict_one(s): #单个句子的预测函数
s = gen_matrix(doc2num(s, maxlen))
s = s.reshape((1, s.shape[0], s.shape[1]))
return model.predict_classes(s, verbose=0)[0][0]
模型2:one embedding
# -*- coding:utf-8 -*-
'''
one embedding测试
在GTX960上,36s一轮
经过30轮迭代,训练集准确率为95.95%,测试集准确率为89.55%
Dropout不能用太多,否则信息损失太严重
'''
import numpy as np
import pandas as pd
pos = pd.read_excel('pos.xls', header=None)
pos['label'] = 1
neg = pd.read_excel('neg.xls', header=None)
neg['label'] = 0
all_ = pos.append(neg, ignore_index=True)
maxlen = 200 #截断字数
min_count = 20 #出现次数少于该值的字扔掉。这是最简单的降维方法
content = ''.join(all_[0])
abc = pd.Series(list(content)).value_counts()
abc = abc[abc >= min_count]
abc[:] = list(range(1, len(abc)+1))
abc[''] = 0 #添加空字符串用来补全
word_set = set(abc.index)
def doc2num(s, maxlen):
s = [i for i in s if i in word_set]
s = s[:maxlen] + ['']*max(0, maxlen-len(s))
return list(abc[s])
all_['doc2num'] = all_[0].apply(lambda s: doc2num(s, maxlen))
#手动打乱数据
idx = list(range(len(all_)))
np.random.shuffle(idx)
all_ = all_.loc[idx]
#按keras的输入要求来生成数据
x = np.array(list(all_['doc2num']))
y = np.array(list(all_['label']))
y = y.reshape((-1,1)) #调整标签形状
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout, Embedding
from keras.layers import LSTM
#建立模型
model = Sequential()
model.add(Embedding(len(abc), 256, input_length=maxlen))
model.add(LSTM(128))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])
batch_size = 128
train_num = 15000
model.fit(x[:train_num], y[:train_num], batch_size = batch_size, nb_epoch=30)
model.evaluate(x[train_num:], y[train_num:], batch_size = batch_size)
def predict_one(s): #单个句子的预测函数
s = np.array(doc2num(s, maxlen))
s = s.reshape((1, s.shape[0]))
return model.predict_classes(s, verbose=0)[0][0]
模型3:word embedding
# -*- coding:utf-8 -*-
'''
word embedding测试
在GTX960上,18s一轮
经过30轮迭代,训练集准确率为98.41%,测试集准确率为89.03%
Dropout不能用太多,否则信息损失太严重
'''
import numpy as np
import pandas as pd
import jieba
pos = pd.read_excel('pos.xls', header=None)
pos['label'] = 1
neg = pd.read_excel('neg.xls', header=None)
neg['label'] = 0
all_ = pos.append(neg, ignore_index=True)
all_['words'] = all_[0].apply(lambda s: list(jieba.cut(s))) #调用结巴分词
maxlen = 100 #截断词数
min_count = 5 #出现次数少于该值的词扔掉。这是最简单的降维方法
content = []
for i in all_['words']:
content.extend(i)
abc = pd.Series(content).value_counts()
abc = abc[abc >= min_count]
abc[:] = list(range(1, len(abc)+1))
abc[''] = 0 #添加空字符串用来补全
word_set = set(abc.index)
def doc2num(s, maxlen):
s = [i for i in s if i in word_set]
s = s[:maxlen] + ['']*max(0, maxlen-len(s))
return list(abc[s])
all_['doc2num'] = all_['words'].apply(lambda s: doc2num(s, maxlen))
#手动打乱数据
idx = list(range(len(all_)))
np.random.shuffle(idx)
all_ = all_.loc[idx]
#按keras的输入要求来生成数据
x = np.array(list(all_['doc2num']))
y = np.array(list(all_['label']))
y = y.reshape((-1,1)) #调整标签形状
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout, Embedding
from keras.layers import LSTM
#建立模型
model = Sequential()
model.add(Embedding(len(abc), 256, input_length=maxlen))
model.add(LSTM(128))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])
batch_size = 128
train_num = 15000
model.fit(x[:train_num], y[:train_num], batch_size = batch_size, nb_epoch=30)
model.evaluate(x[train_num:], y[train_num:], batch_size = batch_size)
def predict_one(s): #单个句子的预测函数
s = np.array(doc2num(list(jieba.cut(s)), maxlen))
s = s.reshape((1, s.shape[0]))
return model.predict_classes(s, verbose=0)[0][0]
转载到请包括本文地址:https://kexue.fm/archives/3863
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jun. 29, 2016). 《文本情感分类(三):分词 OR 不分词 》[Blog post]. Retrieved from https://kexue.fm/archives/3863
@online{kexuefm-3863,
title={文本情感分类(三):分词 OR 不分词},
author={苏剑林},
year={2016},
month={Jun},
url={\url{https://kexue.fm/archives/3863}},
}
December 28th, 2016
博主您好,如果我想再word embedding层哪里再加一个lstm层改怎么做呢。主要代码如下:
model = Sequential()
model.add(Embedding(len(abc), 256, input_length=maxlen))
model.add(LSTM(128,return_sequences = True))
model.add(Dropout(0.5))
model.add(LSTM(128,return_sequences = True))
model.add(Dropout(0.5))
model.add(TimeDistributed(Dense(1)))
model.add(Activation('softmax'))
错误是Error when checking model target: expected activation_1 to have 3 dimensions, but got array with shape (14000L, 1L)
这和激活函数有什么关系。我应该怎么破?谢谢
第二句model.add(LSTM(128,return_sequences = True))改为model.add(LSTM(128))
January 4th, 2017
博主您好,我在运行您的代码时出现了下面的错误,您看一下是什么原因好吗?
Traceback (most recent call last):
File "keras_lstm.py", line 59, in
model.add(LSTM(128))
File "/usr/local/lib/python2.7/dist-packages/keras/models.py", line 327, in add
output_tensor = layer(self.outputs[0])
File "/usr/local/lib/python2.7/dist-packages/keras/engine/topology.py", line 569, in __call__
self.add_inbound_node(inbound_layers, node_indices, tensor_indices)
File "/usr/local/lib/python2.7/dist-packages/keras/engine/topology.py", line 632, in add_inbound_node
Node.create_node(self, inbound_layers, node_indices, tensor_indices)
File "/usr/local/lib/python2.7/dist-packages/keras/engine/topology.py", line 164, in create_node
output_tensors = to_list(outbound_layer.call(input_tensors[0], mask=input_masks[0]))
File "/usr/local/lib/python2.7/dist-packages/keras/layers/recurrent.py", line 227, in call
input_length=input_shape[1])
File "/usr/local/lib/python2.7/dist-packages/keras/backend/tensorflow_backend.py", line 1836, in rnn
axes = [1, 0] + list(range(2, len(outputs.get_shape())))
File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/framework/tensor_shape.py", line 462, in __len__
raise ValueError("Cannot take the length of Shape with unknown rank.")
ValueError: Cannot take the length of Shape with unknown rank.
哪一个脚本?完全没改出现的问题?
最后一个,换了一个机子跑没有问题了,我查一查依赖库或者keras版本,看是不是这方面的问题吧。
我也出现了这个问题 是因为安装keras包的时候依赖库没装好吗?
February 16th, 2017
博主你好,还是一个新手菜鸟,想试一下代码,但是提示下面错误应该怎么改呢?
Traceback (most recent call last):
File "E:/python app/deeplearningtextsentimentclassification.py", line 51, in
from keras.models import Sequential
File "C:\Python27\lib\site-packages\keras\__init__.py", line 2, in
from . import backend
File "C:\Python27\lib\site-packages\keras\backend\__init__.py", line 59, in
from .tensorflow_backend import *
File "C:\Python27\lib\site-packages\keras\backend\tensorflow_backend.py", line 1, in
import tensorflow as tf
ImportError: No module named tensorflow
博主 跑你这个代码必须用python3么,我用的是2.7.10版本的
就是用python 2.7跑的,你这个错误是没有装tensorflow,你连这个都不懂,你还是好好学python的,不要想着修改几个参数就完事。
March 7th, 2017
博主,你好.我用http://deeplearning.net/tutorial/lstm.html#lstm-networks-for-sentiment-analysis这个官方提供的lstm,跑了一下自己的中文数据,数据集分为训练,验证和测试集。最后运行结果得出了训练误差,验证误差和测试误差。我想问一下,可以用这个测试误差作为模型的准确率吗?还是要保存每条语料的预测值,然后计算准确率之类的呢?
一般来说,要有说服力地证明你的模型,需要对每条语料进行预测,然后计算准确率。
loss只是相对的,同一个准确率的不同模型,loss可以差别很大。而且loss本身选取也有很多种,mse、交叉熵,等等,结果都不一样的。所以loss只适合训练过程中的相对比较。
刚看到回复好激动,真的很感谢博主的回复。因为我没太看懂程序的里train_err,valid_err和test_err是怎样计算的,所以就以为最终的test_err就能说明模型的准确性。那博主,我需要自己再写测试程序来对每条语料进行预测吗?然后再计算准确率。。
对呀,这是必须的。包括你参加比赛,也是以这个准确率为唯一标准。
博主,我用的是官方http://deeplearning.net/tutorial/lstm.html#lstm-networks-for-sentiment-analysis提供的这个lstm的程序。程序当中def pred_probs(),这个方法我没太看明白,是用来进行预测的吗?
像你说的对每条语料进行预测,是不是就类似你代码里的
def predict_one(s)?好惭愧刚开始接触python程序,希望博主指导一下
,真的谢谢。我的qq是 137820449
程序当中肯定对每一条测试语料进行预测了吧?要不然怎么会得出test_err呢。是不是没有保存预测结果呢?
你的理解是对的,就是预测概率。建议你,刚入门就直接用keras之类的库好了,里边搭建LSTM也很容易,theano属于比较低层的库,要求对算法和模型有着比较清晰的认识。
博主,你的意思是这个def pred_probs()就是用来对每条语料进行预测的。但是输出的好像不是概率啊。那我需要怎样保存预测结果呢?
因为我是做医疗方向的,之前一直都没有合适的数据库,方向换了好几次,现在刚定下方向,老师马上又要出国了。所以最近刚把中文数据集跑通,想看看结果。您说的keras我也会找机会尝试的。
用keras你可能半小时就跑通了,代码清晰易懂,官方文档也全,中英文都有。对于情感分析来说,本文就已经很完整了,可以直接用。
def pred_probs()我简单看了一下,应该是输出概率呀,有什么问题吗?
博主很有耐心,真心感谢。我现在的问题就是,不知道怎样在现有的基础上来计算准确率呢?
March 10th, 2017
博主您好,我在运行您的word embedding代码时也出现了下面的错误,重新安装了Anaconda 包那些 还是有这样的问题?
Traceback (most recent call last):
File "keras_lstm.py", line 59, in
model.add(LSTM(128))
File "/usr/local/lib/python2.7/dist-packages/keras/models.py", line 327, in add
output_tensor = layer(self.outputs[0])
File "/usr/local/lib/python2.7/dist-packages/keras/engine/topology.py", line 569, in __call__
self.add_inbound_node(inbound_layers, node_indices, tensor_indices)
File "/usr/local/lib/python2.7/dist-packages/keras/engine/topology.py", line 632, in add_inbound_node
Node.create_node(self, inbound_layers, node_indices, tensor_indices)
File "/usr/local/lib/python2.7/dist-packages/keras/engine/topology.py", line 164, in create_node
output_tensors = to_list(outbound_layer.call(input_tensors[0], mask=input_masks[0]))
File "/usr/local/lib/python2.7/dist-packages/keras/layers/recurrent.py", line 227, in call
input_length=input_shape[1])
File "/usr/local/lib/python2.7/dist-packages/keras/backend/tensorflow_backend.py", line 1836, in rnn
axes = [1, 0] + list(range(2, len(outputs.get_shape())))
File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/framework/tensor_shape.py", line 462, in __len__
raise ValueError("Cannot take the length of Shape with unknown rank.")
ValueError: Cannot take the length of Shape with unknown rank.
你是用python3?我用的是python2。你试试将abc[:] = range(1, len(abc)+1)改为abc[:] = list(range(1, len(abc)+1))
还是不行 感觉是keras版本的问题 还没试其他的机子能不能跑
在我电脑全部库都是最新的,不过我写这个代码的时候,版本还是旧的。那说明,新旧通用的~
May 7th, 2017
问个问题,从结构上来说字确实是组成中文文本最基本的单位,但是词才是表征了语义最基本的单位,我们表达一个意思的时候也是相关的词语堆砌而成的句子吧。所以个人感觉字one-hot的立足点还是不足,但是lstm本身考虑了前面的信息,就是说得通的,那就可以不用分词。但是从另外一个角度来看,分词更是说得通的,当前词语整合前面的信息更明确了其语境意...糟了,我好像也不知道自己在说什么了。哈哈
May 25th, 2017
感谢博主的分享。有点问题请教:
用Embedding将词典下标转换成的低维词向量,是否和word2vec,GloVe训练获得的词向量一样,具有空间中一义多词距离相近的特性?
毕竟对单纯的整数序列Embedding也能将其转换为向量吧?Embedding层背后的实现原理是怎样的?用word2vec,GloVe是否会取得更好的效果?
http://kexue.fm/archives/4122/
May 26th, 2017
博主,请教一下,是否可以使用双向的LSTM的方法进行文本情感分析,可以的话要如何修改代码呢?
May 26th, 2017
你好,运行代码时出现问题
idx = range(len(all_))
np.random.shuffle(idx)
报错TypeError: 'range' object does not support item assignment
改为idx =[ i for i in range(len(all_))]即可
你用的是python3,我用的是python2
May 27th, 2017
您好博主:
我想请教下,model.fit()中是否应添加validation_data=(x[train_num:], y[train_num:])
这样加也可以,这样就是说每迭代一次都算一次准确率,看准确率的变化情况。
但有些时候迭代一次的时间,远小于算一次准确率的时间,这样就严重拖慢进度了。
总的来说,看情况吧~