文本情感分类(三):分词 OR 不分词
By 苏剑林 | 2016-06-29 | 416701位读者 |去年泰迪杯竞赛过后,笔者写了一篇简要介绍深度学习在情感分析中的应用的博文《文本情感分类(二):深度学习模型》。虽然文章很粗糙,但还是得到了不少读者的反响,让我颇为意外。然而,那篇文章中在实现上有些不清楚的地方,这是因为: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://spaces.ac.cn/archives/3863
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jun. 29, 2016). 《文本情感分类(三):分词 OR 不分词 》[Blog post]. Retrieved from https://spaces.ac.cn/archives/3863
@online{kexuefm-3863,
title={文本情感分类(三):分词 OR 不分词},
author={苏剑林},
year={2016},
month={Jun},
url={\url{https://spaces.ac.cn/archives/3863}},
}
July 1st, 2016
大神你好,请问 如何实现训练样本的10折交叉验证的准确率呢? 将预测结果保存到CSV文件中。本人菜鸟一个,请大神不吝赐教,最好能有代码直接实现~~~万分感激~~
交叉验证在sklearn里边有,不过交叉验证我感觉没啥必要,用随机打乱的方法就差不多了。
新手建议慢慢扎实基础,多读读官方文档,搞清楚每一步,不要立马就想要代码。
多谢指导~
目前,只能得到训练集的准确率,并不能得到测试集的准确率,如何能得到测试集的准确率呢? 另外,如何将测试集保存到EXCEL或者CSV中呢?谢谢
model.evaluate这一句就是测试集验证,输出两个参数,第二个参数就是测试准确率。至于保存结果,用DataFrame的to_csv方法即可,还是那句话,建议先打好基础,会事半功倍的,否则欲速则不达
July 2nd, 2016
博主,你好,
一直在读你的文章,写的很不错,但是现在想深入了解下RNN算法的原理,我想问问你是怎么入门LSTM的,初学这个算法感觉不太好懂,有没有好的建议和方法。
还有我想用GPU跑一些keras的文本情感分类实验,不知道选哪一种型号的比较好,性价比高的显卡?我现在处理的训练集数据量能有千万级别的,模型的话想先用keras教程的demo去跑。
话说RNN和LSTM的具体细节我也不是很熟,只是有个大概了解,数学细节什么的也没有看。不知道你要认识到什么程度?我一般是找一些博文来看的,我觉得定性认识比定量认识更重要。
显卡我目前用GTX960,只是够便宜而已,我觉得这方面没什么性价比可言的,只要在预算范围内,还是卖最贵的吧。
我目前也需要处理上亿文章,不过深度学习模型对我来说就仅仅是学术探究的,真的用于大数据分析的,还只是一些简单清晰的模型。
July 4th, 2016
请问一下博主,keras一些模块包你是怎么学习的,比如keras.utils等,你在写程序的时候怎么知道调用这个包用np_utils.to_categorical这个函数的,也即将单标签扩充为one-hot的矩阵。我查了keras.io的网站怎么检索不到这个包或者函数的文档说明
官方文档本来很详细啦,而且内容不多,可以一字一句读完整。此外,可以看github上的example,很有实用价值。
July 15th, 2016
请问一下博主,模型一one-hot运行出现如下错误,TypeError:sequence item 10432:expected string or Unicode,float found,是第22行的content=''.join(all_[0])出错了,该怎么改
原因:估计是因为文件存在空行,空行识别为float,所以会出错。
解决办法:用drop_na先把空行去掉。
忠告:先好好学一个月python和pandas等库,再去搞模型,不要想着一步到位。此外,这个问题的原因,需要多看数据才能看到。从来就没有傻瓜式的可以直接调用的代码。
July 15th, 2016
改为content=''.join(str(all_[0]))之后又出现TypeError:'float' object is not iterable
July 18th, 2016
博主您好,代码中有model.evaluate ,但是为何不显示 测试集的准确率? 按照keras网站中的关于model.evaluate的介绍,将模型3:word embedding 中的模型评估改成model.evaluate(x[train_num:], y[train_num:], batch_size = batch_size, verbose=1, sample_weight=None),仅能显示时间,不能显示loss和acc。该如何解决?3Q
model.evaluate就是用来进行测试的呀,它返回两个参数,第一个参数是score,我一般不怎么关注它,第二个参数就是acc呀。既然是测试,不是训练,不显示loss是正常的呀,loss一般是训练(fit)时才显示的。
July 18th, 2016
请问博主,我也遇到6楼的情况,虽然有model.evaluate ,但是无法显示测试集的loss和acc。 模型2:one embedding 模型3:word embedding 可以设置validation_split=0.1 ,显示10%得测试集的结果。但是模型1 :one-hot 则无法 通过validation_split=0.1来显示 测试集效果。请问博主该如何进行需改或调整 代码。
首先数据已经随机打乱了,然后选取前15000来训练,后面的来测试,这就是人工形式的split了呀。你们运行完model.evaluate后,都没有返回数据吗?我的会返回两个数字,第一个参数是score,第二个就是acc
我们运行的都是只有时间的,没有score和acc,请问后面苏老师还看了嘛?
先理解代码再去跑代码。
嗯呢 问题已经解决了 苏老师为什么注释掉手动打乱数据跑出来的精度会很低啊,为什么要手动打乱数据啊?
自己对比一下打乱/不打乱时的训练集和验证集就知道了。
请问你的问题解决了嘛?
July 20th, 2016
请问博主,word embedding 时使用的keras的词向量,那么如果我想使用其他的词向量文件,该怎么使用呢,感觉在word embeding中,词向量的质量应该还是会有一些影响的
自己预先训练好词向量,比如word2vec,然后去掉embedding层,自己生成所需要格式的训练样本。
July 22nd, 2016
博主你好,由于是随机产生的测试样本,因此3个模型的训练集和测试集中的样本应该不同,这样的话由于样本不同产生的结果说服力不是很强。 想要使得3个模型中随机产生的护具训练集和测试集一样,应该怎么做呢?谢谢
要想一样,那先完成其中一个模型的测试,不要退出,然后继续建立另外一个模型测试就行了,不要重新打算数据。具体来说,打乱数据的代码是:
idx = range(len(all_))
np.random.shuffle(idx)
all_ = all_.loc[idx]
因此,第二次建模的时候,不要重开窗口,不要重新打乱数据,那么数据就跟原来的保持一样了。当然,你可以将第一次的idx保存为文件,第二次再调用,也是一种方案。
不知在三个模型中都加入相同的随机种子能否使得三个模型训练样本和测试样本一致呢?例如都加入np.random.seed(1234). 请楼主解答一下。
September 8th, 2016
请问博主最后得到的list(all_['doc2num']其实是一个词频统计矩阵是吧?为什么要使用词频矩阵来训练成相应的词向量?不是应该把相应的句子或者词语转化为词向量么,就像word2vec那样?
不是词频矩阵,而是词语编号,比如“科学”、“空间”分别用1、2编号,那么“科学 空间”就对应着[1,2],embedding层会将编号1、2自动训练为词向量。