Python · RNN

(這裡是最終成品的 GitHub 地址)

(這裡是本章會用到的 GitHub 地址)

(知乎的無序列表崩了啊豈可修!!!)

========== 寫在前面的話 ==========

最近折騰 RNN 時發現 Tensorflow 居然不支持返回所有的 State 而只支持返回最後一個 State……然後我看了源碼之後發現在簡單的場景下、這功能挺好實現的……然後我就怒 PR 了一下,不知道 Tensorflow 那邊會不會 merge(趴

相關數學理論:

* 數學 · RNN(一)· 從 NN 到 RNN

* 數學 · RNN(二)· BPTT 演算法(這個不看也並無大礙,畢竟 Tensorflow 會幫你處理所有梯度……)

========== 分割線的說 ==========

LSTMs cell 的實現

由於 Tensorflow 中有相應的 RNN 封裝,所以我們只需要把 cell 中的「前向傳導演算法」實現出來即可(這或多或少地參考了 Tensorflow 的源碼):

class LSTMCell(tf.contrib.rnn.BasicRNNCell):n def __call__(self, x, state, scope="LSTM"):n with tf.variable_scope(scope):n # 由於有兩種 State,所以要用 split 函數把它們分開n s_old, h_old = tf.split(state, 2, 1)n # 算出各個 Gate 的值n gates = layers.fully_connected(n tf.concat([x, s_old], 1),n num_outputs=4 * self._num_units,n activation_fn=None)n # 用 split 函數把各個 Gate 分開n r1, g1, g2, g3 = tf.split(gates, 4, 1)n # 用激活函數作用於各個 Gate 的值n r1 = tf.nn.sigmoid(r1)n g1 = tf.nn.sigmoid(g1)n g2 = tf.nn.tanh(g2)n g3 = tf.nn.sigmoid(g3)n # 照著公式敲即可 ( σω)σn h_new = h_old * r1 + g1 * g2n s_new = tf.nn.tanh(h_new) * g3n # 用 concat 函數「打包」兩種 Staten return s_new, tf.concat([s_new, h_new], 1)nn @propertyn def state_size(self):n # 由於有兩種 State,所以 State 大小是 Gate 中隱藏神經元的兩倍n return 2 * self._num_unitsn

不過雖說定義 cell 的過程比較平凡,調用它卻不是一件特別容易的事,我們會通過解決 Mnist 問題來進行相應的演示

定義數據生成器

為了在實現 RNN 封裝時代碼顯得更簡潔,定義一個數據生成器來幫助我們 handle 數據是有必要的:

class MnistGenerator:n def __init__(self, im=None, om=None):n self._im, self._om = im, omn self._cursor = self._indices = Nonen # DataUtil.get_dataset 的實現這裡就不贅述了n # 總之它能返回一個打亂後的迷你 Mnist 數據集n self._x, self._y = DataUtil.get_dataset(n "mnist",n "../../_Data/mnist.txt",n quantized=True, one_hot=Truen )n # 將輸入 reshape 一下,原因會在後文說明n self._x = self._x.reshape(-1, 28, 28)n # 劃分訓練集和測試集n self._x_train, self._x_test = self._x[:1800], self._x[1800:]n self._y_train, self._y_test = self._y[:1800], self._y[1800:]nn # 重新打亂數據集,相當於「刷新」n def refresh(self):n self._cursor = 0n self._indices = np.random.permutation(len(self._x_train))nn def gen(self, batch, test=False):n # 如果 batch 為 0、則返回所有訓練集(或測試集)n if batch == 0:n if test:n return self._x_test, self._y_testn return self._x_train, self._y_trainn # 否則,生成下一個 Batchn end = min(self._cursor + batch, len(self._x_train))n start, self._cursor = self._cursor, endn if start == end:n self.refresh()n end = batchn start = self._cursor = 0n indices = self._indices[start:end]n return self._x_train[indices], self._y_train[indices]n

應該還是挺直觀的 ( σω)σ

實現 RNN 的封裝

為簡潔,我們只針對 Mnist 問題進行靈活性比較差的實現,更靈活、應用場合更全面、廣泛的實現可以參見這裡(比如 One-to-One、Many-to-Many、State concatenating、Embedding、支持輸入序列長度、支持指定使用多少歷史信息、支持畫出訓練曲線之類的)

下面就看看一個最基本的封裝框架應該如何搭建吧:

class RNNWrapper:n """n 初始化框架n self._generator:存儲數據生成器的屬性n self._tfx, self._tfy:Tensorflow 的 placeholdern self._output, self._cell:模型的輸出和所用的 celln self._im, self._om, self._hidden_units:n 輸入、輸出維度和 Gate 中隱藏神經元個數n """n def __init__(self):n self._generator = Nonen self._tfx = self._tfy = self._output = Nonen self._cell = self._im = self._om = self._hidden_units = Nonen self._sess = tf.Session()nn def fit(self, im, om, generator, hidden_units=128, cell=LSTMCell):n self._generator = generatorn self._im, self._om, self._hidden_units = im, om, hidden_unitsn self._tfx = tf.placeholder(tf.float32, shape=[None, None, im])n self._tfy = tf.placeholder(tf.float32, shape=[None, om])nn self._cell = cell(self._hidden_units)n # 調用相應函數獲得各個 Outputn rnn_outputs, _ = tf.nn.dynamic_rnn(n self._cell, self._tfx,n initial_state=self._cell.zero_state(n tf.shape(self._tfx)[0], tf.float32n )n )n # 調用相應方法獲得模型輸出、損失n self._get_output(rnn_outputs)n # 計算n loss = tf.nn.softmax_cross_entropy_with_logits(n logits=self._output, labels=self._tfyn )n train_step = tf.train.AdamOptimizer(0.01).minimize(loss)n self._sess.run(tf.global_variables_initializer())n # 10 個 epochn # 每個 epoch 循環訓練 29 個 Batchn # 每個 Batch 含 64 個數據n # 注意最後一個 Batch 只剩 1800 - 28 * 64 = 8 個數據n for _ in range(10):n # 「刷新」一下數據生成器n self._generator.refresh()n # ceil(1800 / n for __ in range(29):n x_batch, y_batch = self._generator.gen(64)n self._sess.run(n train_step,n {self._tfx: x_batch, self._tfy: y_batch})n self._verbose()n

其中,self._get_output 這個方法的實現如下:

def _get_output(self, rnn_outputs):n # 利用最後三個 Output 的信息來生成模型的輸出n outputs = tf.reshape(n rnn_outputs[..., -3:, :],n [-1, self._hidden_units * 3]n )n self._output = layers.fully_connected(n outputs, num_outputs=self._om,n activation_fn=tf.nn.sigmoidn )n

應該還是挺簡潔的 ( σω)σ

使用 RNN 解決計算機視覺問題

由於 RNN 接收的是序列數據,所以我們應該想辦法把圖像轉化為序列。一種自然的做法就是:把每一行像素看成一個輸入向量,然後輸入序列的長度即為圖片的行數。以Mnist為例,我們知道其中每個圖片都是28times28的,那麼輸入維度和序列長度就都是28。一般而言,如果某張圖片是Htimes Wtimes C的,亦即:

  • 該圖片一共有H行和C個頻道,一般來說C=3(RGB 通道)、不過我們也不是不可以拿 CNN 的中間結果來餵給 RNN,這樣的話C就可以很大
  • 每一行一般視為有Wtimes C個像素

那麼此時 RNN 的輸入維度即為Wtimes C,序列長度即為H

在將圖像翻譯為序列後,剩下要做的無非就是調用上述封裝罷了。具體而言:

generator = MnistGenerator()nrnn = RNNWrapper()nrnn.fit(28, 10, generator)n

是不是很方便呢 ( σω)σ

對於這個迷你 Mnist 問題而言,我們模型在 GPU 上大概需要 6 秒來完成訓練,最終準確率大概在 90% 左右;相比之下,Tflearn 在相同的模型結構、參數、訓練強度下,需要 23.5 秒左右才能完成訓練,不過它的準確率在 93% 左右……

如果用相同的時間訓練的話,我們模型在訓練 22.5 秒左右之後能夠將準確率穩定在 95% 左右,這說明其表現還是可以的……(Tflearn 可能會記錄各種雜七雜八的東西,所以效率相對而言可能就會差一些)

不過調用我們的 RNN 封裝的一大問題就是:我們需要花不少時間去定義數據生成器;當然這也不一定是壞處,不過對我這種懶人來說果然還是能夠端到端(這裡能用「端到端」這個詞吧?……)最好

然而由於我太懶所以我不想實現端到端(雖然不難)(喂

希望觀眾老爺們能夠喜歡~


推薦閱讀:

為什麼梯度反方向是函數值局部下降最快的方向?
Caffe入門與實踐-簡介
1.7 重談多層神經網路與BP演算法實現
關於神經網路輸入標準化

TAG:Python | 机器学习 | 神经网络 |