Tensorflor實現文本分類
Tensorflor實現文本分類
下面我們使用CNN做文本分類
cnn實現文本分類的原理
下圖展示了如何使用cnn進行句子分類。輸入是一個句子,為了使其可以進行卷積,首先需要將其轉化為向量表示,通常使用word2vec實現。d=5表示每個詞轉化為5維的向量,矩陣的形狀是[sentence_length × 5],即[7 ×5]。6個filter(卷積核),與圖像中使用的卷積核不同的是,nlp使用的卷積核的寬與句子矩陣的寬相同,只是長度不同。這裏有(2,3,4)三種size,每種size有兩個filter,一共有6個filter。然後開始卷積,從圖中可以看出,stride是1,因為對於高是4的filter,最後生成4維的向量,(7-4)/1+1=4。對於高是3的filter,最後生成5維的向量,(7-3)/1+1=5。卷積之後,我們得到句子的特征,使用activation function和1-max-pooling得到最後的值,每個filter最後得到兩個特征。將所有特征合並後,使用softmax進行分類。圖中沒有用到chanel,下文的實驗將會使用兩個通道,static和non-static,有相關的具體解釋。
本文使用的模型
主要包括五層,第一層是embedding layer,第二層是convolutional layer,第三層是max-pooling layer,第四層是fully connected layer,最後一層是softmax layer.接下來依次介紹相關代碼實現。
Input placeholder
# Placeholders for input, output and dropout
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 創建一個占位符變量,在訓練或者測試的時候,需要將占位符輸入到網絡中進行計算,其中的第二個參數是輸入張量的形狀。None 意味著它可以是任何維度的長度,在我們的實驗中它代表批處理的大小,None使得網絡可以處理任意長度的batches。
失活率同樣也是輸入的一部分,在訓練的時候使用dropout ,測試的時候不使用dropout 。
EMBEDDING LAYER
這一層將單詞索引映射到低維的向量表示,它本質上是一個查找表,我們從數據中通過學習得到。
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)
其中,W 是 在訓練時得到的embedding matrix.,用隨機均勻分布進行初始化。tf.nn.embedding_lookup 實現embedding操作,得到 一個3-dimensional 的張量,形狀是 [None, sequence_length, embedding_size].sequence_length 是數據集中最長句子的長度,其他句子都通過添加“PAD”補充到這個長度。embedding_size是詞向量的大小。
TensorFlow的卷積函數-conv2d 需要四個參數, 分別是batch, width, height 以及channel。 embedding之後不包括 channel, 所以我們人為地添加上它,並設置為1。現在就是[None, sequence_length, embedding_size, 1]
CONVOLUTION AND MAX-POOLING LAYERS
由圖中可知, 我們有不同size的filters。因為每次卷積都會產生不同形狀的張量,所以我們要遍歷每個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 是filter 矩陣,h 是對卷積結果進行非線性轉換之後的結果。每個 filter都從整個embedding劃過,不同之處在於覆蓋多少單詞。 “VALID” padding意味著沒有對句子的邊緣進行padding,也就是用了narrow convolution,輸出的形狀是 [1, sequence_length - filter_size + 1, 1, 1]。narrow convolution與 wide convolution的區別是是否對邊緣進行填充。例如,對一個有7個詞的句子來說, filter size是5,使用narrow convolution,輸出的大小是(7-5)+1=3;使用wide convolution,輸出的大小是(7+2*4-5)+1=11.
對輸出進行max-pooling後得到形狀是 [batch_size, 1, 1, num_filters] 的張量,本質上是一個特征向量,最後一個維度是特征代表數量。把每一個max-pooling之後的張量合並起來之後得到一個長向量[batch_size, num_filters_total]. in tf.reshape 中的 -1表示T將向量展平。
DROPOUT LAYER
Dropout也許是cnn中最流行的正則化方法。dropout的想法很簡單,dropout layer隨機地選擇一些神經元,使其失活。這樣可以阻止co-adapting,迫使它們每一個都學習到有用的特征。失活的神經單元個數由dropout_keep_prob 決定。在訓練的時候設為 0.5 ,測試的時候設為 1 (disable dropout) .
# Add dropout
with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)
SCORES AND PREDICTIONS
利用特征向量,我們可以用矩陣相乘計算兩類的得分,也可以用 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="predictions")
LOSS AND ACCURACY
可以用得分定義損失值。損失計算的是網絡的誤差,我們的目標是將其最小化,分類問題標準的損失函數是交叉熵損失。
# 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)
計算正確率
# 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")
MINIMIZING THE LOSS
利用TensorFlow 內置的optimizers,例如 Adam optimizer,優化網絡損失。
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 這個變量可以計算訓練的步數,每訓練一次自動加一。
CHECKPOINTING
TensorFlow 中可以用checkpointing 保存模型的參數。checkpointing中的參數也可以用來繼續訓練。
# 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())
DEFINING A SINGLE TRAINING STEP
用一個batch的數據進行一次訓練。
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)
train_op 什麽也不返回,只是更新網絡中的參數。最終,打印出當前訓練的損失值與正確率。如果batch的size很小的話,這兩者在不同的batch中差別很大。因為使用了dropout,訓練的metrics可能要比測試的metrics糟糕。
同樣的函數也可以用在測試時。
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)
TRAINING LOOP
通過叠代數據進行訓練。
# 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))
VISUALIZING RESULTS IN TENSORBOARD
從上圖中我們可以觀察到:
- 我們的訓練 metrics不平滑,因為用的batch sizes很小。如果用大的batches (或者在整個測試集上進行評估),會得到平滑的線。
- 測試集的 accuracy明顯比訓練集的低,說明網絡過擬合了,我們應該用更大的數據集,更強的正則化,更少的模型參數。
- 訓練集上的 loss 和 accuracy比測試集低的原因是用了dropout.
Tensorflor實現文本分類