【NLP】保姆级教程:手把手带你CNN文本分类(附代码)
分享一篇老文章,文本分類的原理和代碼詳解,非常適合NLP入門!
寫在前面
本文是對經典論文《Convolutional Neural Networks for Sentence Classification[1]》的詳細復現,(應該是)基于TensorFlow 1.1以及python3.6。從數據預處理、模型搭建、模型訓練預測以及可視化一條龍講解,旨在為剛接觸該領域不知道如何下手搭建網絡的同學提供一個參考。廢話不說直接進入主題吧
NLP中的CNN
論文中是使用的CNN框架來實現對句子的分類,積極或者消極。當然這里我們首先必須對CNN有個大概的了解,可以參考我之前的這篇【Deep learning】卷積神經網絡CNN結構。目前主流來看,CNN主要是應用在computer vision領域,并且可以說由于CNN的出現,使得CV的研究與應用都有了質的飛躍。
目前對NLP的研究分析應用最多的就是RNN系列的框架,比如RNN,GRU,LSTM等等,再加上Attention,基本可以認為是NLP的標配套餐了。但是在文本分類問題上,相比于RNN,CNN的構建和訓練更為簡單和快速,并且效果也不差,所以仍然會有一些研究。
那么,CNN到底是怎么應用到NLP上的呢?
不同于CV輸入的圖像像素,NLP的輸入是一個個句子或者文檔。句子或文檔在輸入時經過embedding(word2vec或者Glove)會被表示成向量矩陣,其中每一行表示一個詞語,行的總數是句子的長度,列的總數就是維度。例如一個包含十個詞語的句子,使用了100維的embedding,最后我們就有一個輸入為10x100的矩陣。
在CV中,filters是以一個patch(任意長度x任意寬度)的形式滑過遍歷整個圖像,但是在NLP中,filters會覆蓋到所有的維度,也就是形狀為 [filter_size, embed_size]。更為具體地理解可以看下圖,輸入為一個7x5的矩陣,filters的高度分別為2,3,4,寬度和輸入矩陣一樣為5。每個filter對輸入矩陣進行卷積操作得到中間特征,然后通過pooling提取最大值,最終得到一個包含6個值的特征向量。
弄清楚了CNN的結構,下面就可以開始實現文本分類任務了。
數據預處理
原論文中使用了好幾個數據集,這里我們只選擇其中的一個——Movie Review Data from Rotten Tomatoes[2]。該數據集包括了10662個評論,其中一半positive一半negative。
在數據處理階段,主要包括以下幾個部分:
1、load file
def load_data_and_labels(positive_file, negative_file):#load data from filespositive_examples = list(open(positive_file, "r", encoding='utf-8').readlines())positive_examples = [s.strip() for s in positive_examples]negative_examples = list(open(negative_file, "r", encoding='utf-8').readlines())negative_examples = [s.strip() for s in negative_examples]# Split by wordsx_text = positive_examples + negative_examplesx_text = [clean_str(sent) for sent in x_text]# Generate labelspositive_labels = [[0, 1] for _ in positive_examples]negative_labels = [[1, 0] for _ in negative_examples]y = np.concatenate([positive_labels, negative_labels], 0)return [x_text, y]2、clean sentences
def clean_str(string):string = re.sub(r"[^A-Za-z0-9(),!?\'\`]", " ", string)string = re.sub(r"\'s", " \'s", string)string = re.sub(r"\'ve", " \'ve", string)string = re.sub(r"n\'t", " n\'t", string)string = re.sub(r"\'re", " \'re", string)string = re.sub(r"\'d", " \'d", string)string = re.sub(r"\'ll", " \'ll", string)string = re.sub(r",", " , ", string)string = re.sub(r"!", " ! ", string)string = re.sub(r"\(", " \( ", string)string = re.sub(r"\)", " \) ", string)string = re.sub(r"\?", " \? ", string)string = re.sub(r"\s{2,}", " ", string)return string.strip().lower()模型實現
論文中使用的模型如下所示其中第一層為embedding layer,用于把單詞映射到一組向量表示。接下去是一層卷積層,使用了多個filters,這里有3,4,5個單詞一次遍歷。接著是一層max-pooling layer得到了一列長特征向量,然后在dropout 之后使用softmax得出每一類的概率。
在一個CNN類中實現上述模型
class TextCNN(object):"""A CNN class for sentence classificationWith a embedding layer + a convolutional, max-pooling and softmax layer"""def __init__(self, sequence_length, num_classes, vocab_size,embedding_size, filter_sizes, num_filters, l2_reg_lambda=0.0):""":param sequence_length: The length of our sentences:param num_classes: Number of classes in the output layer(pos and neg):param vocab_size: The size of our vocabulary:param embedding_size: The dimensionality of our embeddings.:param filter_sizes: The number of words we want our convolutional filters to cover:param num_filters: The number of filters per filter size:param l2_reg_lambda: optional這里再注釋一下filter_sizes和num_filters。filters_sizes是指filter每次處理幾個單詞,num_filters是指每個尺寸的處理包含幾個filter。
1. Input placeholder
tf.placeholder是tensorflow的一種占位符,與feeed_dict同時使用。在訓練或者測試模型階段,我們可以通過feed_dict來喂入輸入變量。
# set placeholders for variables self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name='input_x') self.input_y = tf.placeholder(tf.float32, [None, num_classes], name='input_y') self.dropout_keep_prob = tf.placeholder(tf.float32, name='dropout_keep_prob')tf.placeholder函數第一個參數是變量類型,第二個參數是變量shape,其中None表示sample的個數,第三個name參數用于指定名字。
dropout_keep_prob變量是在dropout階段使用的,我們在訓練的時候選取50%的dropout,在測試時不使用dropout。
2. Embedding layer
我們需要定義的第一個層是embedding layer,用于將詞語轉變成為一組向量表示。
# embedding layerwith tf.name_scope('embedding'):self.W = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0), name='weight')self.embedded_chars = tf.nn.embedding_lookup(self.W, self.input_x)# TensorFlow’s convolutional conv2d operation expects a 4-dimensional tensor# with dimensions corresponding to batch, width, height and channel.self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)W 是在訓練過程中學習到的參數矩陣,然后通過tf.nn.embedding_lookup來查找到與input_x相對應的向量表示。tf.nn.embedding_lookup返回的結果是一個三維向量,[None, sequence_length, embedding_size]。但是后一層的卷積層要求輸入為四維向量(batch, width,height,channel)。所以我們要將結果擴展一個維度,才能符合下一層的輸入。
3. Convolution and Max-Pooling Layers
在卷積層中最重要的就是filter?;仡櫛疚牡牡谝粡垐D,我們一共有三種類型的filter,每種類型有兩個。我們需要迭代每個filter去處理輸入矩陣,將最終得到的所有結果合并為一個大的特征向量。
# conv + max-pooling for each filter pooled_outputs = [] for i, filter_size in enumerate(filter_sizes):with tf.name_scope('conv-maxpool-%s' % filter_size):# conv layerfilter_shape = [filter_size, embedding_size, 1, num_filters]W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name='W')b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name='b')conv = tf.nn.conv2d(self.embedded_chars_expanded, W, strides=[1,1,1,1],padding='VALID', name='conv')# activationh = tf.nn.relu(tf.nn.bias_add(conv, b), name='relu')# max poolingpooled = tf.nn.max_pool(h, ksize=[1, sequence_length-filter_size + 1, 1, 1],strides=[1,1,1,1], padding='VALID', name='pool')pooled_outputs.append(pooled)# combine all the pooled fratures num_filters_total = num_filters * len(filter_sizes) self.h_pool = tf.concat(pooled_outputs, 3) # why 3? self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])這里W 就是filter矩陣,?tf.nn.conv2d是tensorflow的卷積操作函數,其中幾個參數包括
strides表示每一次filter滑動的距離,它總是一個四維向量,而且首位和末尾必定要是1,[1, width, height, 1]。
padding有兩種取值:VALID和SAME。
VALID是指不在輸入矩陣周圍填充0,最后得到的output的尺寸小于input;
SAME是指在輸入矩陣周圍填充0,最后得到output的尺寸和input一樣;
這里我們使用的是‘VALID’,所以output的尺寸為[1, sequence_length - filter_size + 1, 1, 1]。
接下去是一層max-pooling,pooling比較好理解,就是選出其中最大的一個。經過這一層的output尺寸為?[batch_size, 1, 1, num_filters]。
4. Dropout layer
這個比較好理解,就是為了防止模型的過擬合,設置了一個神經元激活的概率。每次在dropout層設置一定概率使部分神經元失效, 每次失效的神經元都不一樣,所以也可以認為是一種bagging的效果。
# dropout with tf.name_scope('dropout'):self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)5. Scores and Predictions
我們可以通過對上述得到的特征進行運算得到每個分類的分數score,并且可以通過softmax將score轉化成概率分布,選取其中概率最大的一個作為最后的prediction
#score and prediction with tf.name_scope("output"):W = tf.get_variable('W', shape=[num_filters_total, num_classes],initializer = tf.contrib.layers.xavier_initializer())b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name='b')l2_loss += tf.nn.l2_loss(W)l2_loss += tf.nn.l2_loss(b)self.score = tf.nn.xw_plus_b(self.h_drop, W, b, name='scores')self.prediction = tf.argmax(self.score, 1, name='prediction')6. Loss and Accuracy
通過score我們可以計算得出模型的loss,而我們訓練的目的就是最小化這個loss。對于分類問題,最常用的損失函數是cross-entropy 損失
# mean cross-entropy loss with tf.name_scope('loss'):losses = tf.nn.softmax_cross_entropy_with_logits(logits=self.score, labels=self.input_y)self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss為了在訓練過程中實時觀測訓練情況,我們可以定義一個準確率
# accuracy with tf.name_scope('accuracy'):correct_predictions = tf.equal(self.prediction, tf.argmax(self.input_y, 1))self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, 'float'), name='accuracy')到目前為止,我們的模型框架已經搭建完成,可以使用Tensorboardd來瞧一瞧到底是個啥樣
模型訓練
接下去我們就要開始使用影評數據來訓練網絡啦。
創建圖和session
對于Tensorflow有兩個重要的概念:Graph和Session。
Session會話可以理解為一個計算的環境,所有的operation只有在session中才能返回結果;
Graph圖就可以理解為上面那個圖片,在圖里面包含了所有要用到的操作operations和張量tensors。
PS:在一個項目中可以使用多個graph,不過我們一般習慣只用一個就行。同時,在一個graph中可以有多個session,但是在一個session中不能有多個graph。
with tf.Graph().as_default():session_conf = tf.ConfigProto(# allows TensorFlow to fall back on a device with a certain operation implementedallow_soft_placement= FLAGS.allow_soft_placement,# allows TensorFlow log on which devices (CPU or GPU) it places operationslog_device_placement=FLAGS.log_device_placement)sess = tf.Session(config=session_conf)Initialize CNN
cnn = TextCNN(sequence_length=x_train.shape[1],num_classes=y_train.shape[1],vocab_size= len(vocab_processor.vocabulary_),embedding_size=FLAGS.embedding_dim,filter_sizes= list(map(int, FLAGS.filter_sizes.split(','))),num_filters= FLAGS.num_filters,l2_reg_lambda= FLAGS.l2_reg_lambda) global_step = tf.Variable(0, name='global_step', trainable=False) optimizer = tf.train.AdamOptimizer(1e-3) grads_and_vars = optimizer.compute_gradients(cnn.loss) train_op = optimizer.apply_gradients(grads_and_vars, global_step=global_step)這里train_op的作用就是更新參數,每運行一次train_op,global_step都會增加1。
Summaries
Tensorflow有一個特別實用的操作,summary,它可以記錄訓練時參數或者其他變量的變化情況并可視化到tensorboard。使用tf.summary.FileWriter()函數可以將summaries寫入到硬盤保存到本地。
# visualise gradient grad_summaries = [] for g, v in grads_and_vars:if g is not None:grad_hist_summary = tf.summary.histogram('{}/grad/hist'.format(v.name),g)sparsity_summary = tf.summary.scalar('{}/grad/sparsity'.format(v.name), tf.nn.zero_fraction(g))grad_summaries.append(grad_hist_summary)grad_summaries.append(sparsity_summary) grad_summaries_merged = tf.summary.merge(grad_summaries)# output dir for models and summaries timestamp = str(time.time()) out_dir = os.path.abspath(os.path.join(os.path.curdir, 'run', timestamp)) print('Writing to {} \n'.format(out_dir))# summaries for loss and accuracy loss_summary = tf.summary.scalar('loss', cnn.loss) accuracy_summary = tf.summary.scalar('accuracy', cnn.accuracy)# train summaries train_summary_op = tf.summary.merge([loss_summary, accuracy_summary]) train_summary_dir = os.path.join(out_dir, 'summaries', 'train') train_summary_writer = tf.summary.FileWriter(train_summary_dir, sess.graph)# dev summaries dev_summary_op = tf.summary.merge([loss_summary, accuracy_summary]) dev_summary_dir = os.path.join(out_dir, 'summaries', 'dev') dev_summary_writer = tf.summary.FileWriter(dev_summary_dir, sess.graph)Checkpointing
checkpointing的作用就是可以保存每個階段訓練模型的參數,然后我們可以根據準確率來選取最好的一組參數。
checkpoint_dir = os.path.abspath(os.path.join(out_dir, 'checkpoints')) checkpoint_prefix = os.path.join(checkpoint_dir, 'model') if not os.path.exists(checkpoint_dir):os.makedirs(checkpoint_dir) saver = tf.train.Saver(tf.global_variables(), max_to_keep=FLAGS.num_checkpoints)Initializing the variables
在開始訓練之前,我們通常會需要初始化所有的變量。一般使用 tf.global_variables_initializer()就可以了。
Defining a single training step
我們可以定義一個單步訓練的函數,使用一個batch的數據來更新模型的參數
def train_step(x_batch, y_batch):"""A single training step:param x_batch::param y_batch::return:"""feed_dict = {cnn.input_x: x_batch,cnn.input_y: y_batch,cnn.dropout_keep_prob: FLAGS.dropout_keep_prob}_, step, summaries, loss, accuracy = sess.run([train_op, global_step, train_summary_op, cnn.loss, cnn.accuracy],feed_dict=feed_dict)time_str = datetime.datetime.now().isoformat()print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))train_summary_writer.add_summary(summaries, step)這里的feed_dict就是我們前面提到的同placeholder一起使用的。必須在feed_dict中給出所有placeholder節點的值,否則程序就會報錯。
接著使用sess.run()運行前面定義的操作,最終可以得到每一步的損失、準確率這些信息。
類似地我們定義一個函數在驗證集數據上看看模型的準確率等
def dev_step(x_batch, y_batch, writer=None):"""Evaluate model on a dev setDisable dropout:param x_batch::param y_batch::param writer::return:"""feed_dict = {cnn.input_x: x_batch,cnn.input_y: y_batch,cnn.dropout_keep_prob: 1.0}step, summaries, loss, accuracy = sess.run([global_step, dev_summary_op, cnn.loss, cnn.accuracy],feed_dict=feed_dict)time_str = datetime.datetime.now().isoformat()print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))if writer:writer.add_summary(summaries, step)Training loop
前面都定義好了以后就可以開始我們的訓練了。我們每次調用train_step函數批量的訓練數據并保存:
# generate batches batches = data_process.batch_iter(list(zip(x_train, y_train)), FLAGS.batch_size, FLAGS.num_epochs) # training loop for batch in batches:x_batch, y_batch = zip(*batch)train_step(x_batch, y_batch)current_step = tf.train.global_step(sess, global_step)if current_step % FLAGS.evaluate_every == 0:print('\n Evaluation:')dev_step(x_dev, y_dev, writer=dev_summary_writer)print('')if current_step % FLAGS.checkpoint_every == 0:path = saver.save(sess, checkpoint_prefix, global_step=current_step)print('Save model checkpoint to {} \n'.format(path))最后輸出的效果大概是這樣的
Visualizing Results
我們可以在代碼目錄下打開終端輸入以下代碼來啟動瀏覽器的tensorboard:
tensorboard --logdir /runs/xxxxxx/summaries小結
當然這只是一個利用CNN進行NLP分類任務(文本分類,情感分析等)的baseline,可以看出準確率并不是很高,后續還有很多可以優化的地方,包括使用pre-trained的Word2vec向量、加上L2正則化等等。
完整代碼:
https://github.com/KaiyuanGao/text_claasification/tree/master/cnn_classification
往期精彩回顧適合初學者入門人工智能的路線及資料下載機器學習及深度學習筆記等資料打印機器學習在線手冊深度學習筆記專輯《統計學習方法》的代碼復現專輯 AI基礎下載機器學習的數學基礎專輯黃海廣老師《機器學習課程》視頻課本站qq群851320808,加入微信群請掃碼:
總結
以上是生活随笔為你收集整理的【NLP】保姆级教程:手把手带你CNN文本分类(附代码)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【机器学习】微软出品!FLAML:一款可
- 下一篇: SVN创建不了资源库位置 解决方案