数据集

Yelp Reviews是Yelp为了学习目的而发布的一个开源数据集。它包含了由数百万用户评论,商业属性和来自多个大都市地区的超过20万张照片。这是一个常用的全球NLP挑战数据集,包含5,200,000条评论,174,000条商业属性。 数据集下载地址为:

  1. https://www.yelp.com/dataset/download

Yelp Reviews格式分为JSON和SQL两种,以JSON格式为例,其中最重要的review.json,包含评论数据。Yelp Reviews数据量巨大,非常适合验证CNN模型。

特征提取

特征提取使用词袋序列模型和词向量。

词袋序列模型

词袋序列模型是在词袋模型的基础上发展而来的,相对于词袋模型,词袋序列模型可以反映出单词在句子中的前后关系。keras中通过Tokenizer类实现了词袋序列模型,这个类用来对文本中的词进行统计计数,生成文档词典,以支持基于词典位序生成文本的向量表示,创建该类时,需要设置词典的最大值。

  1. tokenizer = Tokenizer(num_words=None)

Tokenizer类的示例代码如下:

  1. from keras.preprocessing.text import Tokenizer
  2. text1='some thing to eat'
  3. text2='some thing to drink'
  4. texts=[text1,text2]
  5. tokenizer = Tokenizer(num_words=None)
  6. #num_words:None或整数,处理的最大单词数量。少于此数的单词丢掉
  7. tokenizer.fit_on_texts(texts)
  8. # num_words=多少会影响下面的结果,行数=num_words
  9. print( tokenizer.texts_to_sequences(texts))
  10. #得到词索引[[1, 2, 3, 4], [1, 2, 3, 5]]
  11. print( tokenizer.texts_to_matrix(texts))
  12. # 矩阵化=one_hot
  13. [[ 0., 1., 1., 1., 1., 0., 0., 0., 0., 0.],
  14. [ 0., 1., 1., 1., 0., 1., 0., 0., 0., 0.]]

在处理Yelp数据集时,把每条评论看成一个词袋序列,且长度固定。超过固定长度的截断,不足的使用0补齐。

  1. #转换成词袋序列,max_document_length为序列的最大长度
  2. max_document_length=200
  3. #设置分词最大个数 即词袋的单词个数
  4. tokenizer = Tokenizer(num_words=max_features)
  5. tokenizer.fit_on_texts(text)
  6. sequences = tokenizer.texts_to_sequences(text)
  7. #截断补齐
  8. x=pad_sequences(sequences, maxlen=max_document_length)

词向量模型

词向量模型常见实现形式有word2Vec,fastText和GloVe,本章使用最基础的word2Vec,基于gensim库实现。为了提高性能,使用了预训练好的词向量,即使用Google News dataset数据集训练出的词向量。加载方式为:

  1. model =
  2. gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin',binary=True)

GloVe预训练好的词向量可以从下列地址下载:

  1. http://nlp.stanford.edu/projects/glove/

衡量指标

使用5折交叉验证,并且考核F1的值,训练轮数为10轮,批处理大小为128。

  1. clf = KerasClassifier(build_fn=baseline_model, epochs=10, batch_size=128, verbose=0)
  2. #使用5折交叉验证
  3. scores = cross_val_score(clf, x, encoded_y, cv=5, scoring='f1_micro')
  4. # print scores
  5. print("f1_micro: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))

单层CNN模型

我们尝试使用单层CNN结构来处理Yelp的分类问题。首先通过一个Embedding层,降维成为50位的向量,然后使用一个核数为250,步长为1的一维CNN层进行处理,接着连接一个池化层。为了防止过拟合,CNN层和全连接层之间随机丢失20%的数据进行训练。需要特别指出的是,Embedding层相当于是临时进行了词向量的计算,把原始的词序列转换成了指定维数的词向量序列。

  1. #CNN参数
  2. embedding_dims = 50
  3. filters = 250
  4. kernel_size = 3
  5. hidden_dims = 250
  6. model = Sequential()
  7. model.add(Embedding(max_features, embedding_dims))
  8. model.add(Conv1D(filters,
  9. kernel_size,
  10. padding='valid',
  11. activation='relu',
  12. strides=1))
  13. #池化
  14. model.add(GlobalMaxPooling1D())
  15. model.add(Dense(2, activation='softmax'))
  16. model.compile(loss='categorical_crossentropy',
  17. optimizer='adam',
  18. metrics=['accuracy'])

损失函数使用交叉熵categorical_crossentropy,优化算法使用adsm,可视化结果如下。

使用CNN进行文档分类-图1.png

打印CNN的结构。

  1. model.summary()

输出的结果如下所示,除了显示模型的结构,还可以显示需要训练的参数信息。

  1. _________________________________________________________________
  2. Layer (type) Output Shape Param #
  3. =================================================================
  4. embedding_1 (Embedding) (None, None, 50) 250000
  5. _________________________________________________________________
  6. conv1d_1 (Conv1D) (None, None, 250) 37750
  7. _________________________________________________________________
  8. global_max_pooling1d_1 (Glob (None, 250) 0
  9. _________________________________________________________________
  10. dense_1 (Dense) (None, 2) 502
  11. =================================================================
  12. Total params: 288,252
  13. Trainable params: 288,252
  14. Non-trainable params: 0
  15. _________________________________________________________________

当特征提取使用词袋序列,特征数取5000的前提下,结果如下。

数据量 F1值
1w 0.86
10w 0.92

单层CNN+MLP模型

在单层CNN的基础上增加一个隐藏层,便于更好的分析CNN层提取的高级特征,该隐藏层结点数为250。

  1. #CNN参数
  2. embedding_dims = 50
  3. filters = 250
  4. kernel_size = 3
  5. hidden_dims = 250
  6. model = Sequential()
  7. model.add(Embedding(max_features, embedding_dims))
  8. model.add(Conv1D(filters,
  9. kernel_size,
  10. padding='valid',
  11. activation='relu',
  12. strides=1))
  13. #池化
  14. model.add(GlobalMaxPooling1D())
  15. #增加一个隐藏层
  16. model.add(Dense(hidden_dims))
  17. model.add(Dropout(0.2))
  18. model.add(Activation('relu'))
  19. #输出层
  20. model.add(Dense(2, activation='softmax'))
  21. model.compile(loss='categorical_crossentropy',
  22. optimizer='adam',
  23. metrics=['accuracy'])

可视化结果如下。

使用CNN进行文档分类-图2.png

打印CNN的结构。

  1. model.summary()

输出的结果如下所示,除了显示模型的结构,还可以显示需要训练的参数信息。

  1. _________________________________________________________________
  2. Layer (type) Output Shape Param #
  3. =================================================================
  4. embedding_1 (Embedding) (None, None, 50) 250000
  5. _________________________________________________________________
  6. conv1d_1 (Conv1D) (None, None, 250) 37750
  7. _________________________________________________________________
  8. global_max_pooling1d_1 (Glob (None, 250) 0
  9. _________________________________________________________________
  10. dense_1 (Dense) (None, 250) 62750
  11. _________________________________________________________________
  12. dropout_1 (Dropout) (None, 250) 0
  13. _________________________________________________________________
  14. activation_1 (Activation) (None, 250) 0
  15. _________________________________________________________________
  16. dense_2 (Dense) (None, 2) 502
  17. =================================================================
  18. Total params: 351,002
  19. Trainable params: 351,002
  20. Non-trainable params: 0
  21. _________________________________________________________________

当特征提取使用词袋序列,特征数取5000的前提下,结果如下,可以增加数据量可以提升性能。在数据量相同的情况下,比单层CNN效果略好。

数据量 F1值
1w 0.87
10w 0.93

TextCNN

TextCNN是利用卷积神经网络对文本进行分类的算法,由Yoon Kim中提出,本质上分别使用大小为3,4和5的一维卷积处理文本数据。这里的文本数据可以是定长的词袋序列模型,也可以使用词向量。

使用CNN进行文档分类-图3

TextCNN的一种实现方式,就是分别使用大小为3,4和5的一维卷积处理输入,然后使用MaxPooling1D进行池化处理,并将处理的结果使用Flatten层压平并展开。把三个卷积层的结果合并,作为下一个隐藏层的输入,为了防止过拟合,丢失50%的数据进行训练。

  1. #CNN参数
  2. embedding_dims = 50
  3. filters = 100
  4. # Inputs
  5. input = Input(shape=[max_document_length])
  6. # Embeddings layers
  7. x = Embedding(max_features, embedding_dims)(input)
  8. # conv layers
  9. convs = []
  10. for filter_size in [3,4,5]:
  11. l_conv = Conv1D(filters=filters, kernel_size=filter_size, activation='relu')(x)
  12. l_pool = MaxPooling1D()(l_conv)
  13. l_pool = Flatten()(l_pool)
  14. convs.append(l_pool)
  15. merge = concatenate(convs, axis=1)
  16. out = Dropout(0.5)(merge)
  17. output = Dense(32, activation='relu')(out)
  18. output = Dense(units=2, activation='softmax')(output)
  19. #输出层
  20. model = Model([input], output)
  21. model.compile(loss='categorical_crossentropy',
  22. optimizer='adam',
  23. metrics=['accuracy'])

可视化结果如下。

使用CNN进行文档分类-图4

打印CNN的结构。

  1. model.summary()

输出的结果如下所示,除了显示模型的结构,还可以显示需要训练的参数信息。

  1. __________________________________________________________________________________________________
  2. Layer (type) Output Shape Param # Connected to
  3. ==================================================================================================
  4. input_1 (InputLayer) (None, 200) 0
  5. __________________________________________________________________________________________________
  6. embedding_1 (Embedding) (None, 200, 50) 250000 input_1[0][0]
  7. __________________________________________________________________________________________________
  8. conv1d_1 (Conv1D) (None, 198, 100) 15100 embedding_1[0][0]
  9. __________________________________________________________________________________________________
  10. conv1d_2 (Conv1D) (None, 197, 100) 20100 embedding_1[0][0]
  11. __________________________________________________________________________________________________
  12. conv1d_3 (Conv1D) (None, 196, 100) 25100 embedding_1[0][0]
  13. __________________________________________________________________________________________________
  14. max_pooling1d_1 (MaxPooling1D) (None, 99, 100) 0 conv1d_1[0][0]
  15. __________________________________________________________________________________________________
  16. max_pooling1d_2 (MaxPooling1D) (None, 98, 100) 0 conv1d_2[0][0]
  17. __________________________________________________________________________________________________
  18. max_pooling1d_3 (MaxPooling1D) (None, 98, 100) 0 conv1d_3[0][0]
  19. __________________________________________________________________________________________________
  20. flatten_1 (Flatten) (None, 9900) 0 max_pooling1d_1[0][0]
  21. __________________________________________________________________________________________________
  22. flatten_2 (Flatten) (None, 9800) 0 max_pooling1d_2[0][0]
  23. __________________________________________________________________________________________________
  24. flatten_3 (Flatten) (None, 9800) 0 max_pooling1d_3[0][0]
  25. __________________________________________________________________________________________________
  26. concatenate_1 (Concatenate) (None, 29500) 0 flatten_1[0][0]
  27. flatten_2[0][0]
  28. flatten_3[0][0]
  29. __________________________________________________________________________________________________
  30. dropout_1 (Dropout) (None, 29500) 0 concatenate_1[0][0]
  31. __________________________________________________________________________________________________
  32. dense_1 (Dense) (None, 32) 944032 dropout_1[0][0]
  33. __________________________________________________________________________________________________
  34. dense_2 (Dense) (None, 2) 66 dense_1[0][0]
  35. ==================================================================================================
  36. Total params: 1,254,398
  37. Trainable params: 1,254,398
  38. Non-trainable params: 0
  39. __________________________________________________________________________________________________

当特征提取使用词袋序列,特征数取5000的前提下,结果如下,可以看出增加数据量可以提升性能。在数据量相同的情况下,比单层CNN效果略好,与CNN+MLP效果相当。

数据量 F1值
1w 0.88
10w 0.92

TextCNN变种

  • CNN-rand:设计好 embedding_size 这个超参数后,对不同单词的向量作随机初始化, 后续BP的时候作调整.
  • static:拿 pre-trained vectors from word2vec,FastText or GloVe 直接用, 训练过程中不再调整词向量. 这也算是迁移学习的一种思想.
  • non-static:pre-trained vectors + fine tuning,即拿word2vec训练好的词向量初始化, 训练过程中再对它们微调.
  • multiple channel:类比于图像中的RGB通道, 这里也可以用static与non-static 搭两个通道来搞.

static版本TextCNN

可以通过使用预先训练的词向量,训练过程中不再调整词向量。最简单的一种实现方式就是使用Word2Vec训练好的词向量。在gensim库中,在Google News dataset数据集训练出的词向量。

  1. model = KeyedVectors.load_word2vec_format(word2vec_file, binary=True)
  2. print model['word'].shape

通过打印某个单词的词向量形状获取预先训练的词向量的维数,本例中为300。设置需要处理的单词的最大个数max_features,然后获取对应单词序列。

  1. #设置分词最大个数 即词袋的单词个数
  2. tokenizer = Tokenizer(num_words=max_features,lower=True)
  3. tokenizer.fit_on_texts(text)
  4. sequences = tokenizer.texts_to_sequences(text)
  5. x=pad_sequences(sequences, maxlen=max_document_length)

通过tokenizer对象获取word到对应数字编号的映射关系表。

  1. #获取word到对应数字编号的映射关系
  2. word_index = tokenizer.word_index
  3. print('Found %s unique tokens.' % len(word_index))

枚举映射关系表,生成嵌入层的参数矩阵。虽然是在很大数据集上进行了训练,理论上还是存在单词无法查找到对应的词向量的可能,所以需要捕捉这种异常情况。

  1. #获取词向量的映射矩阵
  2. embedding_matrix = np.zeros((max_features + 1, embedding_dims))
  3. for word, i in word_index.items():
  4. #编号大于max_features的忽略 该字典是按照字典顺序 所以对应的id不一定是顺序的
  5. if i > max_features:
  6. continue
  7. try:
  8. embedding_matrix[i] = model[word].reshape(embedding_dims)
  9. except:
  10. print "%s not found!" % (word)

生成了嵌入层的参数矩阵矩阵后,可以使用该矩阵创建对应的嵌入层。在本例中,预先训练的词向量,训练过程中不再调整词向量,所以需要把trainable设置为False。

  1. # Inputs
  2. input = Input(shape=[max_document_length])
  3. # 词向量层,本文使用了预训练word2vec词向量,把trainable设为False
  4. x = Embedding(max_features + 1,
  5. embedding_dims,
  6. weights=[embedding_matrix],
  7. trainable=False)(input)

打印CNN的结构。

  1. model.summary()

输出的结果如下所示,除了显示模型的结构,还可以显示需要训练的参数信息。其中1,500,300个参数不可训练,这里指的就是固定的词向量。

  1. _____________________
  2. Layer (type) Output Shape Param # Connected to
  3. ==================================================================================================
  4. input_5 (InputLayer) (None, 200) 0
  5. __________________________________________________________________________________________________
  6. embedding_5 (Embedding) (None, 200, 300) 1500300 input_5[0][0]
  7. __________________________________________________________________________________________________
  8. conv1d_13 (Conv1D) (None, 198, 200) 180200 embedding_5[0][0]
  9. __________________________________________________________________________________________________
  10. conv1d_14 (Conv1D) (None, 197, 200) 240200 embedding_5[0][0]
  11. __________________________________________________________________________________________________
  12. conv1d_15 (Conv1D) (None, 196, 200) 300200 embedding_5[0][0]
  13. __________________________________________________________________________________________________
  14. max_pooling1d_13 (MaxPooling1D) (None, 99, 200) 0 conv1d_13[0][0]
  15. __________________________________________________________________________________________________
  16. max_pooling1d_14 (MaxPooling1D) (None, 98, 200) 0 conv1d_14[0][0]
  17. __________________________________________________________________________________________________
  18. max_pooling1d_15 (MaxPooling1D) (None, 98, 200) 0 conv1d_15[0][0]
  19. __________________________________________________________________________________________________
  20. flatten_13 (Flatten) (None, 19800) 0 max_pooling1d_13[0][0]
  21. __________________________________________________________________________________________________
  22. flatten_14 (Flatten) (None, 19600) 0 max_pooling1d_14[0][0]
  23. __________________________________________________________________________________________________
  24. flatten_15 (Flatten) (None, 19600) 0 max_pooling1d_15[0][0]
  25. __________________________________________________________________________________________________
  26. concatenate_5 (Concatenate) (None, 59000) 0 flatten_13[0][0]
  27. flatten_14[0][0]
  28. flatten_15[0][0]
  29. __________________________________________________________________________________________________
  30. dropout_5 (Dropout) (None, 59000) 0 concatenate_5[0][0]
  31. __________________________________________________________________________________________________
  32. dense_9 (Dense) (None, 32) 1888032 dropout_5[0][0]
  33. __________________________________________________________________________________________________
  34. dense_10 (Dense) (None, 2) 66 dense_9[0][0]
  35. ==================================================================================================
  36. Total params: 4,108,998
  37. Trainable params: 2,608,698
  38. Non-trainable params: 1,500,300
  39. __________________________________________________________________________________________________

当特征提取使用词向量,且使用预训练好的词向量,特征数取5000的前提下,训练过程中词向量相关参数不可改变,结果如下,可以看出增加数据量可以提升性能。在数据量相同的情况下,比单层CNN效果略好,与CNN+MLP效果相当。

数据量 F1值
1w 0.88
10w 0.91

fine tuning版本的TextCNN

fine tuning版本的TextCNN的最大特点是使用预先训练的词向量,训练过程中词向量的参数参与整个反向传递过程,接受训练和调整。具体实现时设置trainable为True即可。

  1. # 词向量层,本文使用了预训练word2vec词向量并接受调整,把trainable设为True
  2. x = Embedding(max_features + 1,
  3. embedding_dims,
  4. weights=[embedding_matrix],
  5. trainable=True)(input)

查看需要训练的参数信息,发现所有参数均可以参与训练过程。

  1. ==================================================================================================
  2. Total params: 4,108,998
  3. Trainable params: 4,108,998
  4. Non-trainable params: 0
  5. __________________________________________________________________________________________________

当特征提取使用词向量,且使用预训练好的词向量,特征数取5000的前提下,训练过程中词向量相关参数可改变,结果如下,可以看出增加数据量可以提升性能。在数据量相同的情况下,比单层CNN效果略好,与CNN+MLP效果相当,比使用静态词向量的效果略好。

数据量 F1值
1w 0.89
10w 0.92

参考文献