在TensorFlow中實現CNN進行文本分類(譯)

翻譯自:http://www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/

代碼請見:整個代碼都在github中,可用


??在這篇文章中,我們將會實現一個與 Kim Yoon的 Convolutional Neural Networks for Sentence Classification這篇論文中相似的模型,論文中的模型在一系列的文本分類任務(像情感分析)中已經實現了很好的分類性能,已經成為未來新的文本分類體系結構的標準基準。
??我將假設你已經熟悉了在自然語言處理中應用卷積神經網絡的基礎知識,如果沒有,建議先閱讀 Understanding Convolutional Neural Networks for NLP 來了解必要的背景知識。


數據處理

??我們使用的數據集為電影評論 Movie Review data from Rotten Tomatoes
,原始論文中也使用了其中一個數據集。數據集中有10662條評論語句,一半positive,一半negative,此數據集的詞匯量大約20k。注意,使用一個powerful模型可能會造成過擬合。此外,數據集沒有一個官方的train/test分割,因此我們只使用10%的數據作為開發集。原始論文報告的是對數據進行10倍交叉驗證的結果。
??此處不講數據預處理,GitHub有代碼:
??1. 從原始數據文件中加載數據
??2. 用與 原文一樣的代碼清理文本數據
??3. 用<pad>填充每個句子使長度一致,長度為59個詞,長度一致可以高效地批處理數據。
??4. 建立詞匯索引,將每個字標記上一個0到18765(詞匯量)之間的整數,這樣每個句子就成了一個整數向量


模型

我們將在這篇文章中構建的網絡大致如下:

??第一層將字映射成低維向量,下一層用多種過濾器大小在映射成的字向量上進行卷積運算,例如,一次滑動3,4或5個字。然后我們將卷積層的結果通過max-pool化為一個長特征向量,添加dropout正則化,用softmax層將結果分類。

??為方便講解,此處略微簡化下原論文的模型:

??在這里向代碼添加上述擴展是相對簡單的(幾十行代碼),可以看一下本文結尾的練習。

??下面正式開始!


實現

??我們把代碼放在了TextCNN類中,以便進行不同的超級參數(Hyperparameter)設置,在init函數中生成模型圖。

import tensorflow as tf
import numpy as np
 
classTextCNN(object):
    """
    CNN用于文本分類;使用了一個詞向量層,后跟卷積層、最大池化層和softmax層。
    """
    def__init__(
      self, sequence_length, num_classes, vocab_size,
      embedding_size, filter_sizes, num_filters):
        #implementation
參數介紹
  • sequence_length 句子的長度。我們已將所有句子填充至等長(59)
  • num_classes 輸出層的類個數,此項目中是2——positive和negative
  • vocab_size 詞匯規模,用來定義嵌入層的大小,形如[vacabulary_size,embedding_size]
  • embedding_size 嵌入(embeddings)的維數
  • filter_sizes 我們希望的卷積過濾器覆蓋的單詞數。我們將為每個大小指定過濾器,例如,[3,4,5]表示我們分別設置滑過3,4,5個詞的過濾器,即共有3*num_fliters個過濾器
  • num_filters 針對每個過濾器大小的過濾器個數

輸入占位符

??我們從定義傳遞給我們網絡的輸入數據開始:

# 用于輸入、輸出和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創建了一個占位符變量,在訓練或測試時用于反饋給網絡。第二個參數是輸入張量(input tensor)的形式,None表示維度的長度是任意的,在本例中第一維是批處理大小,使用None允許網絡處理任意大小的批處理。
??在dropout層保存一個神經元也是對網絡的輸入,因為我們只在訓練時允許dropout,評估模型時禁用。

嵌入層(Embedding層)

??
嵌入層是我們定義的第一個層,它將字索引映射到低維向量表示,本質上是一個從數據中學習的查找表。

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)
    #embedding_lookup此函數是根據索引input_x查找W中的元素
    self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)
    #拓展最后一維

此處應用了幾個新功能:

  • tf.device("/cpu:0"):必須進行CPU執行的操作,默認情況下如果GPU可用的話,TensorFlow會將操作放在GPU上。但是嵌入層的實現現在沒有GPU支持,放在GPU上會拋出錯誤。
  • tf.name_scope:創建一個名為embeddingNameScope,它將所有操作加至名為embedding的頂層節點以方便你在TensorBoard上可視化操作。

?? W 是在訓練時學習得到的嵌入層矩陣。我們用一個隨機統一分布(random uniform distribution)來初始化它。 tf.nn.embedding_lookup 創建一個實際的嵌入操作,嵌入操作的結果是產生一個三維tensor [None,sequence_length,embedding_size]

??TensorFlow的卷積con2d操作需要一個四維tensor:[batch,width,height,channel]。我們的嵌入層結果沒有channel這一維,需要手動添加,所以最終該層為[None,sequence_length,embedding_size,1]

卷積和最大池層

??現在我們可以建造卷積層和max-pooling了,注意我們用了不同大小的過濾器(filter)。由于每個卷積產生不同形狀的tensor,因此我們需要迭代(iterate)他們,為每一個創建一個層,然后將結果合并進一個大的特征向量。

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 是對卷積輸出層應用非線性處理(nonlinearity)的結果。每個過濾器滑過整個embedding,但是覆蓋的字數不同。“VALID” padding表示過濾器滑過了整個句子沒有填充邊緣,執行一個窄卷積得到輸出[1,sequence_length-filter_size+1,1,1]。對特定大小的過濾器輸出執行max-pooling得到一個tensor[batch_size,1,1,num_filters]。這本質是一個特征向量,最后一維對應我們的特征。得到所有大小的過濾器的池化輸出tensor后將他們合并入一個長特征向量[batch_size,num_filters_total]。在tf.reshape中使用-1告訴TensorFlow在可能時降維(flatten the dimension)。

??可以花點時間理解這些操作的輸出形式,回去看《理解自然語言處理中的卷積神經網絡》( Understanding Convolutional Neural Networks for NLP
)。TensorBoard可視化操作也可以幫助理解。

Dropout 層

?? Dropout也許是正則化卷積神經網絡最流行的方法,Dropout背后的思想很簡單。dropout層隨機“禁用”一部分神經元,有效防止神經元共適應(co-adapting),迫使它們學習獨立有用的特征。可用的那部分神經元由dropout_keep_prob對網絡的輸入定義,在訓練時我們將它設為0.5,評估時設為1(禁用dropout)。

# Add dropout
with tf.name_scope("dropout"):
    self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)

分數和預測

?? 使用來自最大池(使用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="predictions")

?? 在這里,tf.nn.xw_plus_b是執行Wx+b矩陣乘法的容器。

損失和準確度

?? 使用得分可以定義損失函數。損失(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是為每個類計算交叉熵損失的函數,給定我們的分數和正確的輸入標簽。然后我們可以得到損失的平均值;也可以使用總和,但這樣會在 比較不同批量和train/dev數據的損失 的時候加大難度。
也可以定義一個訓練和測試期間追蹤有用的量的準確度表達式。

# 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")

可視化網絡

??網絡定義工作已經完成。所有代碼可以在此The full codes獲得。可以在TensorBoard中可視化網絡得到大圖.


訓練過程

??為網絡定義訓練程序之前,我們需要了解TensorFlow如何使用SessionGraph的基本知識。如果你已經熟悉了這些概念,可以跳過本節。
??在TensorFlow中,會話(Session)是一個你可以在其中進行圖形操作的環境,它包含變量和隊列的狀態。每個會話都在一個圖上進行。如果在創建變量和操作時,沒有明確指定會話,將使用TensorFlow創建的默認會話。可以通過在session.as_default()塊內執行命令來更改會話(具體往下看)。
??圖形包含操作和張量(tensors),可以在程序中使用多個圖,但大多數程序只需一個圖。可以在多個會話中使用相同的圖,但不能在一個會話中使用多個圖。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或GPU),這對debug很有用。FLAGS是我們程序的命令行參數。

實例化CNN并最小化損失

?? 實例化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)自動加一。

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)

?? 這里,我們分別追蹤訓練和測試的summaries 。這該案例中他們是相同的量,但是你可能會有只想在訓練期間追蹤的量,比如變量更新值。tf.merge_summary是一個將多個匯總操作合并成一個可執行操作的函數。

Checkpointing 檢查點

?? 另一個常用的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 函數可以方便地初始化我們定義的所有變量。你也可以手動初始化你的變量。例如,使用預先訓練的值初始化你的嵌入(embeddings)是很有用的。

定義一個訓練步驟

在我們來為單個訓練步驟定義一個函數,在批量數據上評估模型、更新模型參數

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什么也不返回,它只是更新網絡參數。最后輸出當前批次的損失和精度,保存summaries到磁盤。注意,如果批量較小,訓練批次的損失和精度可能因批次而異。由于我們使用dropout,你的訓練指標可能比評估指標更糟。

我們在隨機數據集(arbitrary data set)上寫了個類似的函數來評估損失和精度,例如一個有效集或者整個訓練集。重要的是,這個函數和上邊的函數功能相同,但是沒有訓練操作。它也禁用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)

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...
forbatch in batches:
    x_batch, y_batch = zip(*batch)
    train_step(x_batch, y_batch)
    current_step= tf.train.global_step(sess, global_step)
    ifcurrent_step % FLAGS.evaluate_every ==0:
        print("\nEvaluation:")
        dev_step(x_dev, y_dev, writer=dev_summary_writer)
        print("")
    ifcurrent_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的值的函數。完整訓練代碼見這里: The full code for training


在TensorBoard中將結果可視化

訓練腳本把summaries寫入輸出目錄,通過把TensorBoard指向該目錄,我們可以可視化創建的圖和摘要。

tensorboard --logdir /PATH_TO_CODE/runs/1449760558/summaries/

使用默認參數(128維嵌入,過濾器大小為3,4,5,dropout為0.5,每個大小的過濾有128個過濾器)運行訓練過程會產生以下損失和精度曲線圖(藍色是訓練數據,紅色是10%dev數據)

這里需要說明幾件事:

  • 由于使用了小批量,我們的訓練指標(training metrics)并不平滑。如果使用大批量(或在整個訓練集上進行評估)會得到一條更平滑的藍線
  • 由于開發(dev)精度明顯低于訓練精度,看上與很像網絡過擬合訓練數據。說明我們需要更多的數據(MR數據集很小),需要更強的正則化或更少的模型參數,例如 我嘗試在最后一層為權重添加額外的L2懲罰可以將精度提高到76%,接近于原論文中的結果。
  • 由于使用了dropout訓練損失和精度開始時顯著低于開發指標(dev metrics)

你可以調試代碼,并嘗試用不同的參數配置來運行模型,代碼和說明可在GitHub獲取: Code and instructions are available on Github.


擴展和練習

以下是一些有用的練習,可以提高模型的性能:

  • 使用預先訓練的word2vec向量初始化嵌入。 要完成這項工作,您需要使用300維嵌入并使用預先訓練的值初始化它們。
  • 與原始論文中一樣,限制最后一層中權重向量的L2范數。 您可以通過定義在每個訓練步驟后更新權重值的新操作來完成此操作。
  • 將L2正則化添加到網絡以對抗過度擬合,同時嘗試提高dropout率。 (Github上的代碼已包含L2正則化,但默認情況下禁用)
  • 添加權重更新和圖層操作的直方圖摘要,并在TensorBoard中顯示它們。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容