原文:Implementing a CNN for Text Classification in TensorFlowhtml
做者:Denny Britznode
翻譯:Kaiserpython
歡迎訪問集智主站:集智,通向智能時代的引擎
git
完整代碼可見做者Githubgithub
本文所部署的模型與Kim Yoon的Convolutional Neural Networks for Sentence Classification類似。論文中的模型在多種文本分類任務(好比情感分析)上都表現良好,現已加入標準基線(baseline)豪華套餐。api
我假設你已經很是熟悉卷積神經網絡用於天然語言處理的那一套理論了,若是還沒的話,建議先閱讀前篇:一文讀懂CNN如何用於NLP。bash
本文全部代碼都可在集智原貼中運行調試,有須要的同窗能夠點擊這裏前往原貼
網絡
此次要用到的數據集是Movie Review data from Rotten Tomatoes——也是原論文中用到的。數據集包含10662個影評語句樣本,正負面各半。數據集詞彙量大約2萬,注意由於數據量並不大,因此很強的模型反而容易過擬合。數據集自己沒有劃分出訓練/測試,因此咱們簡單地用10%做爲測試集,原論文則是用了10折交叉驗證。session
數據預處理過程以下:app
本文將要搭建的網絡大體以下:
第一層將詞嵌入低維向量。下一層用不一樣大小的卷積覈對詞嵌入作卷積,每次3-5個詞,而後再最大池化得到長特徵向量(注:這裏的「特徵向量」是feature vector,不是eigenvector),通過Dropout正則化以後由softmax分類。
由於是教程文章因此我決定對原論文的模型作些簡化:
固然要加上這些擴展也不難,只須要幾行代碼,能夠看本文最後的練習。下面開始說正事:
爲了便於調教超參數,咱們把代碼放進名爲TextCNN
的類裏,用init
函數生成模型圖。
import tensorflow as tf
import numpy as np
class TextCNN(object):
""" A CNN for text classification. Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer. """
def __init__(self, sequence_length, num_classes, vocab_size,
embedding_size, filter_sizes, num_filters):
# Implementation...
複製代碼
爲了建立對象咱們須要傳入如下參數:
sequence_length
:句子長度,在預處理環節咱們已經填充句子,以保持相同長度(59);num_classes
:輸出層的類別數,這裏是2(好評/差評);vocab_size
:詞空間大小,用於定義嵌入層維度:[vocabulary_size, embedding_size];embedding_size
:嵌入維度;filter_size
:卷積核覆蓋的詞彙數,每種尺寸的數量由num_filters
定義,好比[3,4,5]表示咱們有3 * num_filters
個卷積核,分別每次滑過三、四、5個詞。num_filters
:如上。class TextCNN(object):
def __init__(self, sequence_length, num_classes, vocab_size, embedding_size, filter_sizes, num_filters):
# 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
表示這一維度能夠是任意值,使網絡能夠處理任意數量的批數據。
在dropout層保留一個神經元的機率也是網絡的輸入之一,由於咱們在訓練過程當中開啓了dropout,在評估和與測試這一項會停用。
網絡的第一層是嵌入層,將詞彙映射到低維向量表徵,就像是從數據中學習到一張速查表。
class TextCNN(object):
def __init__():
...
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)
複製代碼
注意這裏的縮進,以上代碼仍然是
def init():
中的一部分。
tf.device("/cpu:0")
:強制使用CPU。默認狀況下TensorFlow會嘗試調用GPU,可是詞嵌入操做們目前尚未GPU支持,因此有可能會報錯。tf.name_scope
:新建了一個命名域「embedding」,這樣在TensorBoard可視化網絡的時候,操做會具備更好的繼承性。W
就是咱們的嵌入矩陣,也是訓練中學習的目標,用隨機均勻分佈初始化。tf.nn.embedding_lookup
建立了實際的嵌入操做,輸出結果是3D張量,形如[None, sequence_length, embedding_size]
{:tensorflow:}TensorFlow的卷積操做conv2d接收4維張量[batch, width, height, channel],而咱們的嵌入結果沒有通道維,因此手動加一個變成[None, sequence_length, embedding, 1]。
如今咱們來搭建卷積層和緊隨其後的池化層,注意咱們的卷積核有多種不一樣的尺寸。由於每一個卷積產生的張量形狀不一,咱們須要迭代對每個建立一層,而後再把結果融合到一個大特徵向量裏。
class TextCNN(object):
def __init__():
...
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" padding 表示卷積核不在邊緣作填補,也就是「窄卷積」,輸出形狀是[1, sequence_length - filter_size + 1, 1, 1]。
最大池化讓咱們的張量形狀變成了[batch_size, 1, 1, num_filters],最後一位對應特徵。把全部通過池化的輸出張量組合成一個很長的特徵向量,形如[batch_size, num_filter_total]。在TensorFlow裏,若是須要將高維向量展平,能夠在tf.reshape
中設置-1
。
Dropout大概是正則化卷積神經網絡最流行的方法。其背後的原理很簡單,就是隨機「拋棄」一部分神經元,以此防止他們共同適應(co-adapting),並強制他們獨立學習有用而特徵。不被拋棄的比例咱們經過dropout_keep_prob
這個變量來控制,訓練過程當中設爲0.5,評估過程當中設爲1.
class TextCNN(object):
def __init__():
...
# Add dropout
with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)
複製代碼
藉助最大池化+dropout所得的特徵向量,咱們能夠作個矩陣乘法並選擇分數最高的類,來作個預測。固然也能夠用softmax函數來把生數據轉化成正規化的機率,但這並不會改變最終的預測結果。
class TextCNN(object):
def __init__():
...
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")
複製代碼
這裏tf.nn.xw_plus_b
是的封裝。
有了這個分數即咱們就能夠定義損失函數了,損失函數(loss)是衡量網絡預測偏差的指標。咱們的目標即是最小化。分類問題中標準的損失函數是交叉熵, cross-entropy loss。
class TextCNN(object):
def __init__():
...
# 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是給定分數和正確輸入標籤以後,計算交叉熵的函數,而後對損失取平均值。咱們也能夠求和,但那樣的話不一樣batch size的損失就很難比較了。
咱們也定義了精度的表達,這個量在追蹤訓練和測試過程當中頗有用。
class TextCNN(object):
def __init__():
...
# 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")
複製代碼
至此咱們已經完成了網絡的定義工做,經過TensorBoard可視化以下:
在啓動訓練進程以前咱們仍是有必要了解一些{:tensorflow:}TensorFlow關於「會話」(Session
)和圖(Graphs
)的基本概念。若是你已經很是熟悉這一套,能夠跳過本節。
TensorFlow 1.5版本已經正式推出,引入了「動態圖」機制,所以本文所述並不是最新用法。
在TensorFlow當中,Session是執行計算圖操做所在的環境,包含變量和隊列的狀態。每一個Session執行一個圖,若是沒有顯式地調用session,那麼在建立變量和操做時,就使用TF默認建立的。能夠運行命令session.as_default()
更改默認session。
一個圖(Graph)包含操做和張量,每一個程序中能夠含有多個圖,但大多數程序也只須要一個圖就夠了。咱們能夠在多個session中重複使用一個圖,可是不能在一個session中調用多個圖。TensorFlow會建立默認圖,你也能夠自行建立新圖並設爲默認。顯式地建立會話和圖保證資源在不須要的時候合理釋放。
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():
# Code that operates on the default graph and session comes here...
複製代碼
allow_soft_placement設置容許TensorFlow在指定設備不存在時自動調整設備。例如,若是咱們的代碼把一個操做放在GPU上,但又在一臺沒有GPU的機器上運行,若是沒有allow_soft_placement
就會報錯。
若是設置了log_device_placement,TensorFlow日誌就會在指定設備(CPU or GPU)上存儲日誌文件。這對debug頗有幫助。FLAGS
是程序接收的命令行參數。
當咱們實例化TextCNN模型,全部定義的變量和操做就會被放進默認的計算圖和會話。
cnn = TextCNN(
sequence_length=x_train.shape[1],
num_classes=2,
vocab_size=len(vocabulary),
embedding_size=FLAGS.embedding_dim,
filter_sizes=map(int, FLAGS.filter_sizes.split(",")),
num_filters=FLAGS.num_filters)
複製代碼
接下來或作的是優化網絡損失函數。TensorFlow內置幾種優化器,這裏使用Adam優化器。
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。
(並非本文的彙總){: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函數能夠很方便地把合併多個彙總融合到一個操做。
另外一個TensorFlow的特性是checkpointing——存儲模型參數,以備不時之需。檢查點能夠用來繼續以前中斷的訓練,或者提早結束獲取最佳參數。檢查點是經過Saver對象存儲的。
# 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())
複製代碼
在訓練模型以前咱們須要初始化計算圖中全部的參數:
sess.run(tf.initialize_all_variables())
複製代碼
initialize_all_variables很方便,一次性初始化全部變量。
如今來定義一個單獨的訓練步,來評估模型在一批數據上的表現,並相應地更新參數。
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又要報錯了。另外一個輸入數據的方法是queues,本文先不討論。
接下來,咱們經過session.run()
運行train_op
,返回值就是咱們想我評估的操做結果。注意train_op
自己沒有返回值,它只是更新了網絡參數。最後咱們打印出本輪訓練的損失函數和精確度,存儲彙總至磁盤。loss和accuracy在不一樣的batch之間可能差別很是大,由於咱們的batch_size很小。又由於使用了dropout,因此訓練過程開始時的表現可能遜於評估過程。
咱們寫了一個類似的函數來評估任意數據集的損失和精度,好比驗證集或整個訓練集。本質上這個函數和以前的同樣,可是沒有訓練操做,也禁用了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)
複製代碼
最後呢,咱們要來寫訓練循環了。咱們一批一批的在數據上迭代,調用train_step
函數,而後評估並存儲檢查點:
atch_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
的值。訓練過程的完整代碼可見這裏。
咱們的訓練腳本把彙總寫到輸出路徑,而後把TensorBoard指向那個路徑,就能夠可視化計算圖和彙總。
tensorboard --logdir /PATH_TO_CODE/runs/1449760558/summaries/
複製代碼
用默認參數訓練(128維詞嵌入,卷積核尺寸三、四、5,dropout率爲0.5,每種尺寸的卷積核各128個)產出的是以下損失與精度表(藍色爲訓練數據,紅色是10%的測試數據)。
從圖上能夠看出幾個事:
有幾個練習能夠幫助優化咱們的模型: