Tensorflow實現的CNN文字分類
翻譯自部落格:IMPLEMENTING A CNN FOR TEXT CLASSIFICATION IN TENSORFLOW
在這篇文章中,我們將實現一個類似於Kim Yoon的卷積神經網路語句分類的模型。 本文提出的模型在一系列文字分類任務(如情感分析)中實現了良好的分類效能,並已成為新的文字分類架構的標準基準。
本文假設你已經熟悉了應用於NLP的卷積神經網路的基礎知識。 如果沒有,建議先閱讀Understanding Convolutional Neural Networks for NLP 以獲得必要的背景。
1. 資料和預處理
我們將在這篇文章中使用的資料集是 Movie Review data from Rotten Tomatoes,也是原始文獻中使用的資料集之一。 資料集包含10,662個示例評論句子,正負向各佔一半。 資料集的大小約為20k。 請注意,由於這個資料集很小,我們很可能會使用強大的模型。 此外,資料集不附帶拆分的訓練/測試集,因此我們只需將10%的資料用作 dev set。 原始文獻展示了對資料進行10倍交叉驗證的結果。
這裡不討論資料預處理程式碼,程式碼可以在 Github 上獲得,並執行以下操作:
- 從原始資料檔案中載入正負向情感的句子。
- 使用與原始文獻相同的程式碼清理文字資料。
- 將每個句子加到最大句子長度(59)。我們向所有其他句子新增特殊的操作,使其成為59個字。填充句子相同的長度是有用的,因為這樣就允許我們有效地批量我們的資料,因為批處理中的每個示例必須具有相同的長度。
- 構建詞彙索引,並將每個單詞對映到0到18,765之間的整數(詞庫大小)。 每個句子都成為一個整數向量。
2. 模型
原始文獻的網路結構如下圖:
第一層將單詞嵌入到低維向量中。 下一層使用多個過濾器大小對嵌入的字向量執行卷積。 例如,一次滑過3,4或5個字。 接下來,我們將卷積層的max_pooling結果作為一個長的特徵向量,新增dropout正則,並使用softmax層對結果進行分類。
因為這是一篇部落格,所以對於原始文獻的模型進行一下簡化:
- 我們不會對我們的詞嵌入使用預先訓練的word2vec向量。 相反,我們從頭開始學習嵌入。
- 我們不會對權重向量執行L2規範約束。 A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification 發現約束對最終結果幾乎沒有影響。
- 原始實驗用兩個輸入資料通道 - 靜態和非靜態字向量。 我們只使用一個通道。
將這些擴充套件程式碼新增到這裡是比較簡單的(幾十行程式碼)。 看看帖子結尾的練習。
3. 程式碼實現
為了允許各種超引數配置,我們將程式碼放入TextCNN類中,在init函式中生成模型圖。
import tensorflow as tf
import numpy as np
class TextCNN(object):
def __init__(self,sequence_length, num_classes, vocab_size,
embedding_size, filter_sizes, num_filters):
為了例項化類,我們傳遞以下引數:
- sequence_length - 句子的長度。注意:我們將所有句子填充到相同的長度(我們的資料集為59)。
- num_classes - 輸出層中的類數,在我們的例子中為正(負)。
- vocab_size - 我們的詞彙量的大小。 這需要定義我們的嵌入層的大小,它將具有[vocabulary_size,embedding_size]的形狀。
- embedding_size - 嵌入的維度。
- filter_sizes - 我們想要卷積過濾器覆蓋的字數。 我們將為此處指定的每個大小設定num_filters。 例如,[3,4,5]意味著我們將有一個過濾器,分別滑過3,4和5個字,總共有3 * num_filters過濾器。
- num_filters - 每個過濾器大小的過濾器數量(見上文)。
3.1 INPUT PLACEHOLDERS
首先定義網路的輸入資料
tf.placeholder建立一個佔位符變數,當我們在訓練集或測試時間執行它時,我們將其饋送到網路。 第二個引數是輸入張量的形狀:None意味著該維度的長度可以是任何東西。 在我們的情況下,第一個維度是批量大小,並且使用“None”允許網路處理任意大小的批次。
將神經元保留在丟失層中的概率也是網路的輸入,因為我們僅在訓練期間使用dropout退出。 我們在評估模型時禁用它(稍後再說)。
3.2 EMBEDDING LAYER
我們定義的第一層是嵌入層,它將詞彙詞索引對映到低維向量表示中。 它本質上是一個從資料中學習的lookup table。
with tf.device('/cpu:0'), tf.name_scope("embedding"):
W = tf.Variable(tf.random_uniform([vocab_size,embedding_size],-1.0,1.0),name="W")
self.embedded_chars = tf.nn.embedding_lookup(W,self.input_x)
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars,-1)
我們在這裡使用了幾個功能:
- tf.device(“/ cpu:0”)強制在CPU上執行操作。 預設情況下,TensorFlow將嘗試將操作放在GPU上(如果有的話)可用,但是嵌入式實現當前沒有GPU支援,並且如果放置在GPU上會引發錯誤。
- tf.name_scope建立一個名稱範圍,名稱為“embedding”。 範圍將所有操作新增到名為“嵌入”的頂級節點中,以便在TensorBoard中視覺化網路時獲得良好的層次結構。
W是我們在訓練中學習的嵌入矩陣。 我們使用隨機均勻分佈來初始化它。 tf.nn.embedding_lookup建立實際的嵌入操作。 嵌入操作的結果是形狀為[None,sequence_length,embedding_size]的三維張量。
TensorFlow的卷積轉換操作具有對應於批次,寬度,高度和通道的尺寸的4維張量。 我們嵌入的結果不包含通道尺寸,所以我們手動新增,留下一層shape為[None,sequence_length,embedding_size,1]。
3.3 CONVOLUTION AND MAX-POOLING LAYERS
現在我們已經準備好構建卷積層,然後再進行max-pooling。 注意:我們使用不同大小的filter。 因為每個卷積產生不同形狀的張量,我們需要迭代它們,為它們中的每一個建立一個層,然後將結果合併成一個大特徵向量。
pooled_outputs = []
for i,filter_size in enumerate(filter_sizes):
with tf.name_scope("conv-maxpool-%s" %filter_size):
# Convolution Layer
filter_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"
)
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv,b),name="relu")
# Max-pooling over the outputs
pooled = 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 features
num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(3,pooled_outputs)
self.h_pool_flat = tf.reshape(self.h_pool,[-1,num_filters_total])
這裡,W是我們的濾波器矩陣,h是將非線性應用於卷積輸出的結果。 每個過濾器在整個嵌入中滑動,但是它涵蓋的字數有所不同。 “VALID”填充意味著我們在沒有填充邊緣的情況下將過濾器滑過我們的句子,執行給我們輸出形狀[1,sequence_length - filter_size + 1,1,1]的窄卷積。 在特定過濾器大小的輸出上執行最大值池將留下一張張量的形狀[batch_size,1,num_filters]。 這本質上是一個特徵向量,其中最後一個維度對應於我們的特徵。 一旦我們從每個過濾器大小得到所有的彙總輸出張量,我們將它們組合成一個長形特徵向量[batch_size,num_filters_total]。 在tf.reshape中使用-1可以告訴TensorFlow在可能的情況下平坦化維度。
3.4 DROPOUT LAYER
Dropout可能是卷積神經網路正則最流行的方法。Dropout背後的想法很簡單。Dropout層隨機地“禁用”其神經元的一部分。 這可以防止神經元共同適應(co-adapting),並迫使他們學習個別有用的功能。 我們保持啟用的神經元的分數由我們網路的dropout_keep_prob輸入定義。 在訓練過程中,我們將其設定為0.5,在評估過程中設定為1(禁用Dropout)。
3.5 SCORES AND PREDICTIONS
使用max-pooling(with dropout )的特徵向量,我們可以通過執行矩陣乘法並選擇具有最高分數的類來生成預測。 我們還可以應用softmax函式將原始分數轉換為歸一化概率,但這不會改變我們的最終預測。
with tf.name_scope("output"):
W = tf.Variable(tf.truncated_normal([num_filters_total,num_classes],stddev=0.1),name="W")
b = tf.Variable(tf.constant(0.1,shape=[num_classes]),name="b")
self.scores = tf.nn.xw_plus_b(self.h_drop,W,b,name="scores")
self.predictions = tf.argmax(self.scores,1,name="prediction")
這裡,tf.nn.xw_plus_b是執行Wx + b矩陣乘法的便利包裝器。
3.6 LOSS AND ACCURACY
使用分數我們可以定義損失函式。 損失是對我們網路錯誤的衡量,我們的目標是將其最小化。分類問題的標準損失函式是交叉熵損失 cross-entropy loss。
# Calculate mean cross-entropy loss
with tf.name_scope("loss"):
losses = tf.nn.softmax_cross_entropy_with_logits(self.scores,self.input_y)
self.loss = tf.reduce_mean(losses)
這裡,tf.nn.softmax_cross_entropy_with_logits是一個方便的函式,計算每個類的交叉熵損失,給定我們的分數和正確的輸入標籤。 然後求損失的平均值。 我們也可以使用總和,但這比較難以比較不同批量大小和訓練/測試集資料的損失。
我們還為精度定義一個表示式,這是在訓練和測試期間跟蹤的有用數值。
# Calculate Accuracy
with tf.name_scope("accuracy"):
correct_predictions = tf.equal(self.predictions,tf.argmax(self.input_y,1))
self.accuracy = tf.reduce_mean(tf.cast(correct_predictions,"float"),name="accuracy")
3.7 TRAINING PROCEDURE
在我們為網路定義訓練程式之前,我們需要了解一些關於TensorFlow如何使用Sessions和Graphs的基礎知識。如果您已經熟悉這些概念,請隨時跳過本節。
在TensorFlow中, Session是正在執行graph 操作的環境,它包含有關變數和佇列的狀態。每個 Session都在單個graph上執行。如果在建立變數和操作時未明確使用 Session,則使用TensorFlow建立的當前預設 Session。您可以通過在session.as_default()塊中執行命令來更改預設 Session(見下文)。
Graph包含操作和張量。您可以在程式中使用多個Graph,但大多數程式只需要一個Graph。您可以在多個 Session中使用相同的Graph,但在一個 Session中不能使用多Graph。 TensorFlow始終建立一個預設Graph,但您也可以手動建立一個Graph,並將其設定為新的預設Graph,如下圖所示。顯式建立 Session和Graph可確保在不再需要資源時正確釋放資源。
FLAGS = tf.flags.FLAGS
with tf.Graph().as_default():
session_conf = tf.ConfigProto(
allow_soft_placement=FLAGS.allow_soft_placement,
log_device_placement=FLAGS.log_device_placement
)
sess = tf.Session(config=session_conf)
with sess.as_default():
當優選裝置不存在時,allow_soft_placement設定允許TensorFlow回退到具有特定操作的裝置上。 例如,如果我們的程式碼在GPU上放置一個操作,並且我們在沒有GPU的機器上執行程式碼,則不使用allow_soft_placement將導致錯誤。 如果設定了log_device_placement,TensorFlow會登入哪些裝置(CPU或GPU)進行操作。 這對除錯非常有用。 標記是我們程式的命令列引數。
3.8 INSTANTIATING THE CNN AND MINIMIZING THE LOSS
當我們例項化我們的TextCNN模型時,所有定義的變數和操作將被放置在上面建立的預設圖和會話中。
cnn = TextCNN(
sequence_length=x_train.shape[1],
num_classes=y_train.shape[1],
vocab_size=len(vocab_processor.vocabulary)
embedding_size=FLAGS.num_filters,
filter_sizes = map(int, FLAGS.filter_sizes.split(",")),
num_filters = FLAGS.num_filters)
接下來,我們定義如何優化網路的損失函式。 TensorFlow有幾個內建優化器。 我們正在使用Adam優化器。
# Define Training procedure
global_step = tf.Variable(0,name="global_step",trainable=False)
optimizer = tf.train.AdamOptimizer(1e-4)
grads_and_vars = optimizer.compute_gradients(cnn.loss)
train_op = optimizer.apply_gradients(grads_and_vars,global_step=global_step)
在這裡,train_op這裡是一個新建立的操作,我們可以執行它們來對我們的引數執行更新。 train_op的每次執行都是一個訓練步驟。 TensorFlow自動計算哪些變數是“可訓練的”並計算它們的梯度。 通過定義一個global_step變數並將其傳遞給優化器,讓TensorFlow對訓練步驟進行計數。 每次執行train_op時,global step 將自動遞增1。
3.9 SUMMARIES
TensorFlow有一個概述(summaries),可以在訓練和評估過程中跟蹤和檢視各種數值。 例如,您可能希望跟蹤您的損失和準確性隨時間的變化。您還可以跟蹤更復雜的數值,例如圖層啟用的直方圖。 summaries是序列化物件,並使用SummaryWriter寫入磁碟。
# Output directory for models and summaries
timestamp = str(int(time.time()))
out_dir = os.path.abspath(os.path.join(os.path.curdir, "runs", timestamp))
print("Writing to {}\n".format(out_dir))
# Summaries for loss and accuracy
loss_summary = tf.scalar_summary("loss", cnn.loss)
acc_summary = tf.scalar_summary("accuracy", cnn.accuracy)
# Train Summaries
train_summary_op = tf.merge_summary([loss_summary, acc_summary])
train_summary_dir = os.path.join(out_dir, "summaries", "train")
train_summary_writer = tf.train.SummaryWriter(train_summary_dir, sess.graph_def)
# Dev summaries
dev_summary_op = tf.merge_summary([loss_summary, acc_summary])
dev_summary_dir = os.path.join(out_dir, "summaries", "dev")
dev_summary_writer = tf.train.SummaryWriter(dev_summary_dir, sess.graph_def)
在這裡,我們分別跟蹤培訓和評估的總結。 在我們的情況下,這些數值是相同的,但是您可能只有在訓練過程中跟蹤的數值(如引數更新值)。 tf.merge_summary是將多個摘要操作合併到可以執行的單個操作中的便利函式。
3.10 CHECKPOINTING
通常使用TensorFlow的另一個功能是checkpointing- 儲存模型的引數以便稍後恢復。Checkpoints 可用於在以後的時間繼續訓練,或使用 early stopping選擇最佳引數設定。 使用Saver物件建立 Checkpoints。
# Checkpointing
checkpoint_dir = os.path.abspath(os.path.join(out_dir, "checkpoints"))
checkpoint_prefix = os.path.join(checkpoint_dir, "model")
# Tensorflow assumes this directory already exists so we need to create it
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
saver = tf.train.Saver(tf.all_variables())
3.11 INITIALIZING THE VARIABLES
在訓練模型之前,我們還需要在圖中初始化變數。
# Initialize all variables
sess.run(tf.global_variables_initializer())
global_variables_initializer函式是一個方便函式,它執行我們為變數定義的所有初始值。也可以手動呼叫變數的初始化程式。 如果希望使用預先訓練的值初始化嵌入,這很有用。
3.12 DEFINING A SINGLE TRAINING STEP
現在我們來定義一個訓練步驟的函式,評估一批資料上的模型並更新模型引數。
def train_step(x_batch,y_batch):
"""
A single training step
"""
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
)
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包含我們傳遞到我們網路的佔位符節點的資料。您必須為所有佔位符節點提供值,否則TensorFlow將丟擲錯誤。使用輸入資料的另一種方法是使用佇列,但這超出了這篇文章的範圍。
接下來,我們使用session.run執行我們的train_op,它返回我們要求它進行評估的所有操作的值。請注意,train_op什麼都不返回,它只是更新我們網路的引數。最後,我們列印當前培訓批次的丟失和準確性,並將摘要儲存到磁碟。請注意,如果批量太小,訓練批次的損失和準確性可能會在批次間顯著變化。而且因為我們使用dropout,您的訓練指標可能開始比您的評估指標更糟。
我們寫一個類似的函式來評估任意資料集的丟失和準確性,例如驗證集或整個訓練集。本質上這個功能與上述相同,但沒有訓練操作。它也禁用退出。
def dev_step(x_batch, y_batch, writer=None):
"""
Evaluates model on a dev set
"""
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)
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)
3.13 TRAINING LOOP
最後,準備編寫訓練迴圈。 迭代資料的批次,呼叫每個批次的train_step函式,偶爾評估和檢查我們的模型:
# Generate batches
batches = data_helpers.batch_iter(
zip(x_train, y_train), FLAGS.batch_size, FLAGS.num_epochs)
# Training loop. For each batch...
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("\nEvaluation:")
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("Saved model checkpoint to {}\n".format(path))
這裡,batch_iter是一個批處理資料的幫助函式,而tf.train.global_step是返回global_step值的便利函式。
3.14 VISUALIZING RESULTS IN TENSORBOARD
我們的訓練指令碼將summaries寫入輸出目錄,並將TensorBoard指向該目錄,我們可以將圖和我們建立的summaries視覺化。
tensorboard --logdir `/path/`
有幾件事情脫穎而出:
- 我們的訓練指標並不平滑,因為我們使用小批量。 如果我們使用較大的批次(或在整個訓練集上評估),我們會得到一個更平滑的藍線。
- 因為測試者的準確性顯著低於訓練準確度,我們的網路在訓練資料似乎過擬合了,這表明我們需要更多的資料(MR資料集非常小),更強的正則化或更少的模型引數。 例如,我嘗試在最後一層為重量新增額外的L2正則,並且能夠將準確度提高到76%,接近於原始文獻。
- 因為使用了dropout,訓練損失和準確性開始大大低於測試指標。
您可以使用程式碼進行操作,並嘗試使用各種引數配置執行模型。 Github提供了程式碼和說明。
4. EXTENSIONS AND EXERCISES
以下是一些的練習,可以提高模型的效能:
- 使用預先訓練的word2vec向量初始化嵌入。 為了能夠起作用,您需要使用300維嵌入,並用預先訓練的值初始化它們。
- 限制最後一層權重向量的L2範數,就像原始文獻一樣。 您可以通過定義一個新的操作,在每次訓練步驟之後更新權重值。
- 將L2正規化新增到網路以防止過擬合,同時也提高dropout比率。 (Github上的程式碼已經包括L2正則化,但預設情況下禁用)
- 新增權重更新和圖層操作的直方圖summaries,並在TensorBoard中進行視覺化。