从Embedding层到Keras解决文本分类
最近,正在做CCF的一个与文本分类相关的竞赛。这也是我第一次接触和NLP相关的分类问题,所以成绩也并不是很理想,只是想分享下相关的经验,因为我觉得NLP问题的预处理阶段真的很劝退。。。
首先我们拿到的数据是tsv格式,也能当做txt格式来看。tsv内部的格式为:
因此我们可以吧一篇文章当做id \t title \t content \t label \n的格式
这里我们在NLP问题中需要将词\字转化为向量进行处理,那么就需要用分词和gensim来完成转换处理。不过这部分的word2vec处理不是我们这里说的重点,可以参考https://zhuanlan.zhihu.com/p/25928551。
假设词向量长度为100,那么“词组1”=[x1,x2,x3,…,x100]的向量。一篇文章假设有1000字,那么这篇文章就是个1000x100的矩阵,然后我们初期就傻乎乎的吧一共900000篇文章都转换成了1500x100的矩阵,规模就是900000x1500x100,用掉了450GB的空间。没错!450GB,这无疑是太愚蠢太不科学了。
并且,450GB的数据在带入深度网络中训练之前IO的开销非常非常的大,整体的性能也极差,可以说我这个外行人闹了个笑话。其实会有这个问题的最关键原因就是我并没有理解所谓的Embedding层的概念。这里极力推荐苏神分享的经验http://kexue.fm/archives/4122/,如果看过后还不能充分理解的话可以来这里结合具体实现的例子来看看。
在苏神的分享中已经给出了Embedding的定义,其本质就是一个特殊的全连接,只是输入都是0-1
再说得通俗一点,我们看上面的例子,假设最左边第一个矩阵为onehot矩阵X,中间的矩阵是Embedding层中存储的权重矩阵W,X的第一行(1,0,0,0,0,0)是1,第二行其实就是2。然后输出的分别是W矩阵的第一行和第二行。这里很多人可能会恍然大悟,窝草!这不就是个HashMap的key,value映射吗?其实是可以这么认为的。也就是X中的key=1对应W中value=第一行,key=2对应W矩阵value=第二行。看到这里大家应该就能理解了,这不就可以用来转换之前的word2vec吗?是的。。。就是这么搞的。
我们回到之前的工作,我们已经做好了分词,以及word2vec的转换,但是转换工作是基于非重复字典的。也就是说假设你900000篇文章里所有的词组去重后(一般还要去掉停用词即无意义的符号和词组)可能只有10000个独立词组,那么我转换后得到的就是一个10002x100的矩阵W,这里的100列即是我们之前所提到的100维词组向量。但是肯定又会奇怪,不是只有10000个独立词组,怎么又10002行呢?其实第一行是全是0用来补空缺(由于所有文章需要同等长度,然而这是不可能的。我是采用所有文章平均长度x2作为标准,那么更长的文章需要截断,更短的文章需要补0),而最后一行就是用来表示之前所说的停用词(无意义的词),值得一提的是停用词词向量的取值范围需要谨慎,而不是无脑0~1,因为要和你word2vec的范围保持一致,否则出现停用词的情况下网络难以收敛(例如我这里的范围是随机-0.25~0.25)。
然后我们怎么去处理原文章呢?还要不要可怕的900000x1500x100=450GB的存储空间了呢?答案当然是NO,我们可以仅仅通过词向量所处W矩阵的行号(index化)来表示原文。这不仅极大缩减了存储空间,还减少了训练时候的IO开销。
最终,W矩阵只需要存储1.7GB,而文章index化存储10GB,总共只需11.7GB就存储了之前450GB的东西。
训练的模型如下:
这里提供下Embedding层keras的代码,也是有一定的代表意义的:
这里的input_dim和output_dim分别是矩阵W的行数和列数(10002,100)。为什么输入会变成10002维呢?因为我们所输入的index(0~10001)在Embedding自动转换为onehot编码即10002维。所以这就是个10002—100的全连接,weights就是权重矩阵W,input_length是我们文章的长度上限,mask_zero是能够自动补0,然而我们W中第一行已经是全0向量了这里就不用了。不使用mask_zero的原因是他后面所有的layer都必须标记mask_zero,太麻烦了。。。至于trainable,由于我们的W是word2vec训练出来的,算作预训练模型,所以就无需训练了。如果Embedding层是trainable的,那么由onehot的输入可以认为每个样本只能更新矩阵W的某一行。
这个分享主要是针对面向NLP的新人(包括我),所以是非常基础的,请大家轻喷。