1 簡述
????在沒有監(jiān)督數(shù)據(jù)的時候,采用無監(jiān)督算法的方式可以計算兩句話的相似度,即通過一些因子,比如語序、詞性、共現(xiàn)詞比例等等進行打分,最后通過加權計算的方式得到最終的相似分值,最終結果主要依賴因子即特征的提取和加權公式的設計,相關項目可以參考Kaggle Quora比賽華人第一名的解決方案,里面有一些優(yōu)秀的可借鑒特征。
????但是最終想要更好的效果必然要使用到有監(jiān)督的算法,而現(xiàn)有較好的技術便是TextCNN。本文主要是整理捋順一下TextCNN的整個過程以及背后的一些原理,包括自己踩坑的點,還有Tensorflow的一些框架問題。
2 項目
????CNN做文本分類的項目及代碼可以參考: TextCNN
3 論文
????CNN做文本分類的論文可以參看: Convolutional Neural Networks for Sentence Classification

????上圖是論文中給出的模型結構,但是這張圖不是很清楚,下圖可以更清楚的看清TextCNN的結構。

????算法的主要流程是通過使用不同kernel_sizes的卷積核對文本embedding二維向量進行卷積操作,每一種kernel_sizes的卷積核有多個,這樣就可以獲得類似n-gram的句法特征,最后經(jīng)過max_pooling,在進行一次拼接,即可以得到文檔向量,經(jīng)過全連接和softmax即可進行分類。
????由上圖所示,文檔二維embedding向量為(7x5),卷積核的kernel_sizes有三種,分別為2,3,4,每種卷積核有2個,一共有2x3=6個卷積核。kernel_sizes=4的卷積核經(jīng)過卷積之后得到(7-4+1)x1即4x1的向量,通過max_pooling之后得到一個數(shù)值,同理其余的5個卷積核分別進行卷積和池化操作,這樣可以得到6個數(shù)值,然后將這6個數(shù)值拼接一起,注意拼接時候的維度,既可以得到文本新的向量。然后可以接全連接再接softmax即可以分類。
????論文中另一case是針對算法的輸入即embedding向量進行優(yōu)化,可以使用Pre-trained的詞向量代替隨機初始化的embedding向量,分別對比隨機初始化、靜態(tài)預訓練向量(不參與訓練)、微調(diào)預訓練向量(參與訓練)和通過設置通道channels輸入不同向量四種方式。最終效果個人感覺只能做一個參考,具體還要視業(yè)務場景而定。比如針對業(yè)務垂直領域很強的文本領域,使用基于大數(shù)據(jù)集的預訓練向量可能不會有很好的效果,比如針對數(shù)碼領域,如果隨機初始化embedding向量,在通過train的方式更新,那么類似“蘋果”之類的詞語更會傾向“蘋果手機”,而使用大數(shù)據(jù)集預訓練的詞向量,“蘋果”這個詞則可能更會傾向水果,這種引入預訓練的方式可能會打破原有數(shù)據(jù)的結構平衡,當然如果對領域多元化的業(yè)務可能引入預訓練效果會更好。另一個引入預訓練詞向量的好處是可以加快訓練速度,這樣即使效果得不到提升,但是速度得到提升也是蠻不錯的。
4 代碼
????下面是一個標準的TextCNN的代碼:
# coding: utf-8
import tensorflow as tf
import warnings
warnings.filterwarnings("ignore")
class TCNNConfig(object):
embedding_dim = 64 # 詞向量維度
seq_length = 600 # 序列長度
num_classes = 10 # 類別數(shù)
num_filters = 256 # 卷積核數(shù)目
# kernel_sizes = [3, 4, 5] # 卷積核尺寸
kernel_sizes = 5 # 卷積核尺寸
vocab_size = 5000 # 詞匯表達小
hidden_dim = 128 # 全連接層神經(jīng)元
dropout_keep_prob = 0.5 # dropout保留比例
learning_rate = 1e-3 # 學習率
batch_size = 64 # 每批訓練大小
num_epochs = 10 # 總迭代輪次
print_per_batch = 100 # 每多少輪輸出一次結果
save_per_batch = 10 # 每多少輪存入tensorboard
class TextCNN(object):
def __init__(self, config):
self.config = config
# 三個待輸入的數(shù)據(jù)
self.input_x = tf.placeholder(tf.int32, [None, self.config.seq_length], name='input_x')
self.input_y = tf.placeholder(tf.float32, [None, self.config.num_classes], name='input_y')
self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')
self.cnn()
def cnn(self):
# 詞向量映射
with tf.device('/cpu:0'):
embedding = tf.get_variable('embedding', [self.config.vocab_size, self.config.embedding_dim])
embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x)
with tf.name_scope("cnn"):
# CNN layer
conv = tf.layers.conv1d(embedding_inputs, self.config.num_filters, self.config.kernel_size, name='conv')
# global max pooling layer
gmp = tf.reduce_max(conv, reduction_indices=[1], name='gmp')
'''
for kernel_size in self.config.kernel_sizes:
gmps = []
with tf.name_scope("cnn-%s" % kernel_size):
# CNN layer
conv = tf.layers.conv1d(embedding_inputs, self.config.num_filters, kernel_size)
# global max pooling layer
gmp = tf.reduce_max(conv, reduction_indices=[1])
gmps.append(gmp)
gmp = tf.concat(values=gmps, name='last_pool_layer', axis=3)
'''
with tf.name_scope("score"):
# 全連接層,后面接dropout以及relu激活
fc1 = tf.layers.dense(gmp, self.config.hidden_dim, name='fc1')
fc2 = tf.contrib.layers.dropout(fc1, self.keep_prob)
fc = tf.nn.relu(fc2)
# 分類器
self.logits = tf.layers.dense(fc, self.config.num_classes, name='fc2')
self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1) # 預測類別
with tf.name_scope("optimize"):
# 損失函數(shù),交叉熵
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.input_y)
self.loss = tf.reduce_mean(cross_entropy)
# 優(yōu)化器
self.optim = tf.train.AdamOptimizer(learning_rate=self.config.learning_rate).minimize(self.loss)
with tf.name_scope("accuracy"):
# 準確率
correct_pred = tf.equal(tf.argmax(self.input_y, 1), self.y_pred_cls)
self.acc = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
????代碼中的TextCNN并沒有設置可變kernel_sizes,統(tǒng)一設置kernel_sizes=5。通過修改注釋代碼即可以實現(xiàn)可變kernel_sizes。實驗之后發(fā)現(xiàn)加入可變的kernel_sizes可以在實驗的那個數(shù)據(jù)集上略微提升一點效果,但是提升不大,因為固定kernel_sizes=5效果就已經(jīng)很好了,具體對比可以看GitHub的對比,所以在新的數(shù)據(jù)集上可以實驗對比兩種效果,kernel_sizes一般選2,3,4,5。
????另一個困擾我比較久的是數(shù)據(jù)傳輸問題,數(shù)據(jù)在傳送過程中的維度變化?,F(xiàn)在就基于以上代碼(tensorflow)捋順一下數(shù)據(jù)傳送過程中的維度變化。batch_size=64,seq_length=600,可得input_x (64, 600);vocab_size=5000,embedding_dim =64,可得embedding (5000, 64);embedding_inputs (64, 600, 64);num_filters=256,kernel_size=5,可得conv (64, 596, 256);gmp (64, 256);fc1 (64, 128);fc2(64, 128),fc1到fc2只經(jīng)過一個dropout所以維度并沒有變化;fc(64, 128),fu2到fc只經(jīng)過一個relu所以維度沒有變化。之后就是進行softmax操作了。
5 踩坑
???? - python2 對漢字真是太不友好了,各種編碼問題
???? - 盡量不要并聯(lián)太多and,不然速度會很慢,比如分詞的時候可以先通過詞長過濾掉一部分,然后全部收集,最后再遍歷,篩掉停用詞。如果分詞的時候直接篩停用詞,速度超級慢。
???? - 字符級和詞語級從當前公開數(shù)據(jù)集上對比,效果相當,詞語級速度更快,但是需要預先分詞
???? - 目前還沒有實驗預訓練詞向量,未完待續(xù)。。。