TensorFlow構建迴圈神經網路

超人汪小建發表於2017-08-29

前言

前面在《迴圈神經網路》文章中已經介紹了深度學習的迴圈神經網路模型及其原理,接下去這篇文章將嘗試使用TensorFlow來實現一個迴圈神經網路,該例子能通過訓練給定的語料生成模型並實現對字元的預測。這裡選擇使用最原始的迴圈神經網路RNN模型。

語料庫的準備

這裡就簡單用紀伯倫的《On Friendship》作為語料吧。

RNN簡要說明

用下面兩張圖簡要說明下,RNN模型有多個時刻的輸入,從第一個圖中看到輸入x、隱層s和輸出o都與時刻t有關,可以看到上一時刻的隱含層會影響到當前時刻的輸出,這也就使迴圈神經網路具有記憶功能。

為了更加清晰理解,看第二個圖,從神經元來看RNN,輸入層有3個神經元,隱含層有4個神經元,輸出層有2個神經元,保留當前時刻狀態參與下一時刻計算。

建立詞彙

def create_vocab(text):
    unique_chars = list(set(text))
    print(unique_chars)
    vocab_size = len(unique_chars)
    vocab_index_dict = {}
    index_vocab_dict = {}
    for i, char in enumerate(unique_chars):
        vocab_index_dict[char] = i
        index_vocab_dict[i] = char
    return vocab_index_dict, index_vocab_dict, vocab_size複製程式碼

處理字元首先就是需要建立包含語料中所有的詞的詞彙,需要一個從字元到詞彙位置索引的詞典,也需要一個從位置索引到字元的詞典。

詞彙儲存及讀取

def save_vocab(vocab_index_dict, vocab_file):
    with codecs.open(vocab_file, 'w', encoding='utf-8') as f:
        json.dump(vocab_index_dict, f, indent=2, sort_keys=True)

def load_vocab(vocab_file):
    with codecs.open(vocab_file, 'r', encoding='utf-8') as f:
        vocab_index_dict = json.load(f)
    index_vocab_dict = {}
    vocab_size = 0
    for char, index in iteritems(vocab_index_dict):
        index_vocab_dict[index] = char
        vocab_size += 1
    return vocab_index_dict, index_vocab_dict, vocab_size複製程式碼

第一次建立詞彙後我們需要將它儲存下來,後面在使用模型預測時需要讀取該詞彙,如果不儲存而每次都建立的話則可能導致詞彙順序不同。

批量生成器

class BatchGenerator(object):
    def __init__(self, text, batch_size, seq_length, vocab_size, vocab_index_dict):
        self._text = text
        self._text_size = len(text)
        self._batch_size = batch_size
        self.vocab_size = vocab_size
        self.seq_length = seq_length
        self.vocab_index_dict = vocab_index_dict

        segment = self._text_size // batch_size
        self._cursor = [offset * segment for offset in range(batch_size)]
        self._last_batch = self._next_batch()

    def _next_batch(self):
        batch = np.zeros(shape=(self._batch_size), dtype=np.float)
        for b in range(self._batch_size):
            batch[b] = self.vocab_index_dict[self._text[self._cursor[b]]]
            self._cursor[b] = (self._cursor[b] + 1) % self._text_size
        return batch

    def next(self):
        batches = [self._last_batch]
        for step in range(self.seq_length):
            batches.append(self._next_batch())
        self._last_batch = batches[-1]
        return batches複製程式碼

建立一個批量生成器用於將文字生成批量的訓練樣本,其中text為整個語料,batch_size為批大小,vocab_size為詞彙大小,seq_length為序列長度,vocab_index_dict為詞彙索引詞典。生成器的生成結構大致如下圖,按文字順序豎著填進矩陣,而矩陣的列大小為batch_size。

這裡寫圖片描述
這裡寫圖片描述

構建圖

cell_fn = tf.contrib.rnn.BasicRNNCell
cell = cell_fn(hidden_size)
cells = [cell]
for i in range(rnn_layers - 1):
    higher_layer_cell = cell_fn(hidden_size)
    cells.append(higher_layer_cell)
multi_cell = tf.contrib.rnn.MultiRNNCell(cells)
self.zero_state = multi_cell.zero_state(self.batch_size, tf.float32)複製程式碼

這裡我們僅僅使用基礎的RNN,所以直接例項化一個BasicRNNCell物件,需要指定隱含層的神經元數hidden_size,另外因為單層的RNN學習能力有限,這裡可以設定網路的層數rnn_layers來增強神經網路的學習能力,最終用MultiRNNCell封裝起來。

接著需要給我們的multi cell進行初始化狀態,初始狀態全設為0,即用multi_cell.zero_state來實現。這裡需要傳入一個batch_size引數,它會生成rnn_layers層的(batch_size ,hidden_size)個初始狀態。

self.initial_state = create_tuple_placeholders_with_default(multi_cell.zero_state(self.batch_size, tf.float32), shape=multi_cell.state_size)
self.input_data = tf.placeholder(tf.int64, [self.batch_size, self.seq_length], name='inputs')
self.targets = tf.placeholder(tf.int64, [self.batch_size, self.seq_length], name='targets')

def create_tuple_placeholders_with_default(inputs, shape):
    if isinstance(shape, int):
        result = tf.placeholder_with_default(
            inputs, list((None,)) + [shape])
    else:
        subplaceholders = [create_tuple_placeholders_with_default(
            subinputs, subshape)
            for subinputs, subshape in zip(inputs, shape)]
        t = type(shape)
        if t == tuple:
            result = t(subplaceholders)
        else:
            result = t(*subplaceholders)
    return result複製程式碼

接著我們開始建立佔位符,有三個佔位符需要建立,分別為初始狀態佔位符、輸入佔位符和target佔位符。首先看初始狀態佔位符,這個主要是根據multi_cell.zero_state(self.batch_size, tf.float32)的結構使用tf.placeholder_with_default建立。其次看輸入佔位符,與批大小和序列長度相關的結構[batch_size, seq_length]。最後是target佔位符,結構與輸入佔位符是一樣的。為更好理解這裡給輸入和target畫個圖,如下:

這裡寫圖片描述
這裡寫圖片描述

這裡寫圖片描述
這裡寫圖片描述

self.embedding = tf.get_variable('embedding', [vocab_size, embedding_size])
inputs = tf.nn.embedding_lookup(self.embedding, self.input_data)複製程式碼

一般我們會需要一個嵌入層將詞彙嵌入到指定的維度空間上,維度由embedding_size指定。同時vocab_size為詞彙大小,這樣就可以將所有單詞都對映到指定的維數空間上。嵌入層結構如下圖,通過tf.nn.embedding_lookup就能找到輸入對應的詞空間向量了,這裡解釋下embedding_lookup操作,它會從詞彙中取到inputs每個元素對應的詞向量,inputs為2維的話,通過該操作後變為3維,因為已經將詞用embedding_size維向量表示了。

這裡寫圖片描述
這裡寫圖片描述

sliced_inputs = [tf.squeeze(input_, [1]) for input_ in
                         tf.split(axis=1, num_or_size_splits=self.seq_length, value=inputs)]
outputs, final_state = tf.contrib.rnn.static_rnn(multi_cell, sliced_inputs, initial_state=self.initial_state)複製程式碼

上面得到的3維的嵌入層空間向量,我們無法直接傳入迴圈神經網路,需要一些處理。需要根據序列長度切割,通過split後再經過squeeze操作後得到一個list,這個list就是最終要進入到迴圈神經網路的輸入,list的長度為seq_length,這個很好理解,就是由這麼多個時刻的輸入。每個輸入的結構為(batch_size,embedding_size),也即是(20,128)。注意這裡的embedding_size,剛好也是128,與迴圈神經網路的隱含層神經元數量一樣,這裡不是巧合,而是他們必須要相同,這樣嵌入層出來的矩陣輸入到神經網路才能剛好與神經網路的各個權重完美相乘。最終得到迴圈神經網路的輸出和最終狀態。

flat_outputs = tf.reshape(tf.concat(axis=1, values=outputs), [-1, hidden_size])
flat_targets = tf.reshape(tf.concat(axis=1, values=self.targets), [-1])

softmax_w = tf.get_variable("softmax_w", [hidden_size, vocab_size])
softmax_b = tf.get_variable("softmax_b", [vocab_size])
self.logits = tf.matmul(flat_outputs, softmax_w) + softmax_b

loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits, labels=flat_targets)
mean_loss = tf.reduce_mean(loss)複製程式碼

經過2層迴圈神經網路得到了輸出outputs,但該輸出是一個list結構,我們要通過tf.reshape轉成tf張量形式,該張量結構為(200,128)。同樣target佔位符也要連線起來,結構為(200,)。接著構建softmax層,權重結構為[hidden_size, vocab_size],偏置項結構為[vocab_size],輸出矩陣與權重矩陣相乘並加上偏置項得到logits,然後使用sparse_softmax_cross_entropy_with_logits計算交叉熵損失,最後求損失平均值。

count = tf.Variable(1.0, name='count')
sum_mean_loss = tf.Variable(1.0, name='sum_mean_loss')
update_loss_monitor = tf.group(sum_mean_loss.assign(sum_mean_loss + mean_loss), count.assign(count + 1),
                                       name='update_loss_monitor')
with tf.control_dependencies([update_loss_monitor]):
    self.average_loss = sum_mean_loss / count
self.global_step = tf.get_variable('global_step', [], initializer=tf.constant_initializer(0.0))複製程式碼

這裡邏輯比較清晰了,用於計算平均損失,另外global_step變數用於記錄訓練的全域性步數。

tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(mean_loss, tvars), max_grad_norm)
optimizer = tf.train.AdamOptimizer(self.learning_rate)
self.train_op = optimizer.apply_gradients(zip(grads, tvars), global_step=self.global_step)複製程式碼

最後使用優化器對損失函式進行優化。為了防止梯度爆炸或梯度消失需要用clip_by_global_norm對梯度進行修正。

所以最後構建的圖可以用下面的圖大致描述。

這裡寫圖片描述
這裡寫圖片描述

建立會話

with tf.Session(graph=graph) as session:
tf.global_variables_initializer().run()
for i in range(num_epochs):
    model.train(session, train_size, train_batches)

def train(self, session, train_size, train_batches):
    epoch_size = train_size // (self.batch_size * self.seq_length)
    if train_size % (self.batch_size * self.seq_length) != 0:
        epoch_size += 1
    state = session.run(self.zero_state)
    start_time = time.time()
    for step in range(epoch_size):
        data = train_batches.next()
        inputs = np.array(data[:-1]).transpose()
        targets = np.array(data[1:]).transpose()
        ops = [self.average_loss, self.final_state, self.train_op, self.global_step, self.learning_rate]
        feed_dict = {self.input_data: inputs, self.targets: targets,
                     self.initial_state: state}
        average_loss, state, __, global_step, lr = session.run(ops, feed_dict)複製程式碼

建立會話開始訓練,設定需要訓練多少輪,由num_epochs指定。epoch_size為完整訓練一遍語料庫需要的輪數。執行self.zero_state得到初始狀態,通過批量生成器獲取一批樣本資料,因為當前時刻的輸入對應的正確輸出為下一時刻的值,所以用data[:-1]和data[1:]得到輸入和target。組織ops並將輸入、target和狀態對應輸入到佔位符上,執行。

預測

module_file = tf.train.latest_checkpoint(restore_path)
model_saver.restore(session, module_file)
start_text = 'your'
length = 20
print(model.predict(session, start_text, length, vocab_index_dict, index_vocab_dict))複製程式碼

將前面訓練的模型儲存下來後就可以載入該模型並且進行RNN預測了。

github

github.com/sea-boat/De…

========廣告時間========

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

歡迎關注:

這裡寫圖片描述
這裡寫圖片描述

相關文章