運用計算圖搭建遞歸神經網路(RNN)

  • 2019 年 11 月 21 日
  • 筆記

文章作者:張覺非 360

編輯整理:Hoh Xil

內容來源:作者授權

出品社區:DataFun

註:歡迎轉載,轉載請註明出處

繼續玩我們的計算圖框架。這一次我們運用計算圖搭建遞歸神經網路(RNN,Recursive Neural Network)。RNN 處理前後有承接關係的序列狀數據,例如時序數據。當然,前後的承接也不一定是時間上的,但總之是有前後關係的序列。

▌RNN

RNN 的思想是:網路也分步,每步以輸入序列的該步數據(向量)和上一步數據(第一步沒有)為輸入,進行變換,得到這一步的輸出(向量)。這樣的話,序列的每一步就會對下一步產生影響。RNN 用變換的參數把握序列每一步之間的關係。最後一步的輸出可以送給全連接層,最終用於分類或回歸。RNN 有很多種,有一些複雜的變體,本文搭建一種最簡單的 RNN ,它的結構是這樣的:

藍色長條表示 m 維輸入向量,一共 n 個。這表示數據是長度為 n 的序列,每一步是一個 m 維向量。綠色的矩形就是每一步的變換。yi 是每一步的 k 維輸出向量。每一步用 k x k 的權值矩陣 Y 去乘前一步的輸出向量(第一步沒有),用 k x m 的權值矩陣 W 去乘這一步的輸入向量,加和後再加上 k 維偏置向量 b ,施加激活函數 ϕ (我們取 ReLU),就得到這一步的輸出。

最後一步的輸出也是 k 維向量,把它送給全連接層,最後施加 SoftMax 後得到各個類別的概率,再接上一個交叉熵損失就可以用來訓練分類問題了。用我們的計算圖框架可以這樣搭建這個簡單的 RNN(程式碼):

seq_len = 96  # 序列長度  dimension = 16  # 序列每一步的向量維度  hidden_dim = 12  # RNN 時間單元的輸出維度    # 時間序列變數,每一步一個 dimension 維向量(Variable 節點),保存在數組 input 中  input_vectors = []  for i in range(seq_len):      input_vectors.append(Variable(dim=(dimension, 1), init=False, trainable=False))    # 對於本步輸入的權值矩陣  W = Variable(dim=(hidden_dim, dimension), init=True, trainable=True)    # 對於上步輸入的權值矩陣  Y = Variable(dim=(hidden_dim, hidden_dim), init=True, trainable=True)    # 偏置向量  b = Variable(dim=(hidden_dim, 1), init=True, trainable=True)    # 構造 RNN  last_step = None  # 上一步的輸出,第一步沒有上一步,先將其置為 None  for iv in input_vectors:      y = Add(MatMul(W, iv), b)        if last_step is not None:          y = Add(MatMul(Y, last_step), y)        y = ReLU(y)        last_step = y      fc1 = fc(y, hidden_dim, 6, "ReLU")  # 第一全連接層  fc2 = fc(fc1, 6, 2, "None")  # 第二全連接層    # 分類概率  prob = SoftMax(fc2)    # 訓練標籤  label = Variable((2, 1), trainable=False)    # 交叉熵損失  loss = CrossEntropyWithSoftMax(fc2, label)

這就是構造 RNN 以及交叉熵損失的計算圖的程式碼,很簡單,right ?有了計算圖以及自動求導,我們只管搭建網路即可,網路的訓練就交給計算圖去做了。否則你可以想像,按照示意圖表示的計算,推導交叉熵損失對 RNN 的各個權值矩陣和偏置的梯度是多麼困難。

▌時間序列問題

我們構造一份數據,它包含兩類時間序列,一類是方波,一類是正弦波,程式碼如下:

def get_sequence_data(number_of_classes=2, dimension=10, length=10, number_of_examples=1000, train_set_ratio=0.7, seed=42):      """      生成兩類序列數據。      """      xx = []      xx.append(np.sin(np.arange(0, 10, 10 / length)))  # 正弦波      xx.append(np.array(signal.square(np.arange(0, 10, 10 / length))))  # 方波          data = []      for i in range(number_of_classes):          x = xx[i]          for j in range(number_of_examples):              sequence = x + np.random.normal(0, 1.0, (dimension, len(x)))  # 加入高斯雜訊              label = np.array([int(i == j) for j in range(number_of_classes)])                data.append(np.c_[sequence.reshape(1, -1), label.reshape(1, -1)])        # 把各個類別的樣本合在一起      data = np.concatenate(data, axis=0)        # 隨機打亂樣本順序      np.random.shuffle(data)        # 計算訓練樣本數量      train_set_size = int(number_of_examples * train_set_ratio)  # 訓練集樣本數量        # 將訓練集和測試集、特徵和標籤分開      return (data[:train_set_size, :-number_of_classes],              data[:train_set_size, -number_of_classes:],              data[train_set_size:, :-number_of_classes],              data[train_set_size:, -number_of_classes:])

我們用這一行程式碼獲取長度為 96 ,維度為 16 的兩類(各 1000 個)序列:

# 獲取兩類時間序列:正弦波和方波  train_x, train_y, test_x, test_y = get_sequence_data(length=seq_len, dimension=dimension)

看一看時間序列樣本,先看正弦波:

正弦波序列

這是一個正弦波時間序列樣本,它包含 16 條曲線,每一條都是 sin 曲線加雜訊。之所以包含 16 條曲線,因為我們的時間序列的每一步是一個 16 維向量,按時間列起來就有了 16 條正弦曲線。正弦波時間序列是我們的正樣本。方波時間序列是負樣本:

方波序列

一個方波時間序列先維持 +1 一段時間,變為 -1 維持一段時間,再回到 +1 ,循環往複。由於我們的高斯雜訊加得較大,可以看到正弦波和方波還是有可能混淆的,但也能看出它們之間的差異。

▌訓練

現在就用我們構造的 RNN 訓練一個分類模型,分類正弦波和方波,程式碼如下:

from sklearn.metrics import accuracy_score    from layer import *  from node import *  from optimizer import *    seq_len = 96  # 序列長度  dimension = 16  # 序列每一步的向量維度  hidden_dim = 12  # RNN 時間單元的輸出維度    # 獲取兩類時間序列:正弦波和方波  train_x, train_y, test_x, test_y = get_sequence_data(length=seq_len, dimension=dimension)    # 時間序列變數,每一步一個 dimension 維向量(Variable 節點),保存在數組 input 中  input_vectors = []  for i in range(seq_len):      input_vectors.append(Variable(dim=(dimension, 1), init=False, trainable=False))    # 對於本步輸入的權值矩陣  W = Variable(dim=(hidden_dim, dimension), init=True, trainable=True)    # 對於上步輸入的權值矩陣  Y = Variable(dim=(hidden_dim, hidden_dim), init=True, trainable=True)    # 偏置向量  b = Variable(dim=(hidden_dim, 1), init=True, trainable=True)    # 構造 RNN  last_step = None  # 上一步的輸出,第一步沒有上一步,先將其置為 None  for iv in input_vectors:      y = Add(MatMul(W, iv), b)        if last_step is not None:          y = Add(MatMul(Y, last_step), y)        y = ReLU(y)        last_step = y      fc1 = fc(y, hidden_dim, 6, "ReLU")  # 第一全連接層  fc2 = fc(fc1, 6, 2, "None")  # 第二全連接層    # 分類概率  prob = SoftMax(fc2)    # 訓練標籤  label = Variable((2, 1), trainable=False)    # 交叉熵損失  loss = CrossEntropyWithSoftMax(fc2, label)    # Adam 優化器  optimizer = Adam(default_graph, loss, 0.005, batch_size=16)    # 訓練  print("start training", flush=True)  for e in range(10):        for i in range(len(train_x)):          x = np.mat(train_x[i, :]).reshape(dimension, seq_len)          for j in range(seq_len):              input_vectors[j].set_value(x[:, j])          label.set_value(np.mat(train_y[i, :]).T)            # 執行一步優化          optimizer.one_step()            if i > 1 and (i + 1) % 100 == 0:                # 在測試集上評估模型正確率              probs = []              losses = []              for j in range(len(test_x)):                  # x = test_x[j, :].reshape(dimension, seq_len)                  x = np.mat(test_x[j, :]).reshape(dimension, seq_len)                  for k in range(seq_len):                      input_vectors[k].set_value(x[:, k])                  label.set_value(np.mat(test_y[j, :]).T)                    # 前向傳播計算概率                  prob.forward()                  probs.append(prob.value.A1)                    # 計算損失值                  loss.forward()                  losses.append(loss.value[0, 0])                    # print("test instance: {:d}".format(j))                # 取概率最大的類別為預測類別              pred = np.argmax(np.array(probs), axis=1)              truth = np.argmax(test_y, axis=1)              accuracy = accuracy_score(truth, pred)                default_graph.draw()              print("epoch: {:d}, iter: {:d}, loss: {:.3f}, accuracy: {:.2f}%".format(e + 1, i + 1, np.mean(losses),                                                                                      accuracy * 100), flush=True)

訓練 10 個 epoch 後,測試集上的正確率達到了 99% :

epoch: 1, iter: 100, loss: 0.693, accuracy: 51.08%  epoch: 1, iter: 200, loss: 0.692, accuracy: 51.08%  epoch: 1, iter: 300, loss: 0.677, accuracy: 78.31%  epoch: 1, iter: 400, loss: 0.573, accuracy: 49.31%  epoch: 1, iter: 500, loss: 0.520, accuracy: 53.92%  epoch: 1, iter: 600, loss: 0.599, accuracy: 97.08%  epoch: 1, iter: 700, loss: 0.617, accuracy: 99.00%  epoch: 2, iter: 100, loss: 0.601, accuracy: 94.46%  epoch: 2, iter: 200, loss: 0.579, accuracy: 82.08%  epoch: 2, iter: 300, loss: 0.558, accuracy: 76.15%  epoch: 2, iter: 400, loss: 0.531, accuracy: 67.85%  epoch: 2, iter: 500, loss: 0.507, accuracy: 63.77%  epoch: 2, iter: 600, loss: 0.493, accuracy: 61.15%  epoch: 2, iter: 700, loss: 0.479, accuracy: 62.23%  epoch: 3, iter: 100, loss: 0.443, accuracy: 69.92%  epoch: 3, iter: 200, loss: 0.393, accuracy: 85.85%  epoch: 3, iter: 300, loss: 0.365, accuracy: 97.69%  epoch: 3, iter: 400, loss: 0.284, accuracy: 95.08%  epoch: 3, iter: 500, loss: 0.199, accuracy: 95.69%  epoch: 3, iter: 600, loss: 0.490, accuracy: 80.62%  epoch: 3, iter: 700, loss: 0.264, accuracy: 94.31%  epoch: 4, iter: 100, loss: 0.320, accuracy: 83.46%  epoch: 4, iter: 200, loss: 0.333, accuracy: 80.92%  epoch: 4, iter: 300, loss: 0.276, accuracy: 90.15%  epoch: 4, iter: 400, loss: 0.242, accuracy: 95.00%  epoch: 4, iter: 500, loss: 0.217, accuracy: 96.38%  epoch: 4, iter: 600, loss: 0.191, accuracy: 95.31%  epoch: 4, iter: 700, loss: 0.167, accuracy: 94.00%  epoch: 5, iter: 100, loss: 0.142, accuracy: 94.62%  epoch: 5, iter: 200, loss: 0.111, accuracy: 96.85%  epoch: 5, iter: 300, loss: 0.116, accuracy: 96.85%  epoch: 5, iter: 400, loss: 0.080, accuracy: 96.77%  epoch: 5, iter: 500, loss: 0.059, accuracy: 98.54%  epoch: 5, iter: 600, loss: 0.054, accuracy: 98.54%  epoch: 5, iter: 700, loss: 0.042, accuracy: 99.00%  epoch: 6, iter: 100, loss: 0.047, accuracy: 98.46%  epoch: 6, iter: 200, loss: 0.049, accuracy: 98.08%  epoch: 6, iter: 300, loss: 0.030, accuracy: 99.15%  epoch: 6, iter: 400, loss: 0.029, accuracy: 99.23%  epoch: 6, iter: 500, loss: 0.028, accuracy: 99.08%  epoch: 6, iter: 600, loss: 0.029, accuracy: 99.08%  epoch: 6, iter: 700, loss: 0.024, accuracy: 99.15%  epoch: 7, iter: 100, loss: 0.023, accuracy: 99.15%  epoch: 7, iter: 200, loss: 0.031, accuracy: 98.85%  epoch: 7, iter: 300, loss: 0.023, accuracy: 99.46%  epoch: 7, iter: 400, loss: 0.022, accuracy: 99.54%  epoch: 7, iter: 500, loss: 0.022, accuracy: 99.38%  epoch: 7, iter: 600, loss: 0.027, accuracy: 98.77%  epoch: 7, iter: 700, loss: 0.019, accuracy: 99.46%  epoch: 8, iter: 100, loss: 0.018, accuracy: 99.54%  epoch: 8, iter: 200, loss: 0.018, accuracy: 99.46%  epoch: 8, iter: 300, loss: 0.018, accuracy: 99.54%  epoch: 8, iter: 400, loss: 0.018, accuracy: 99.62%  epoch: 8, iter: 500, loss: 0.017, accuracy: 99.54%  epoch: 8, iter: 600, loss: 0.026, accuracy: 99.00%  epoch: 8, iter: 700, loss: 0.021, accuracy: 99.23%  epoch: 9, iter: 100, loss: 0.017, accuracy: 99.62%  epoch: 9, iter: 200, loss: 0.016, accuracy: 99.54%  epoch: 9, iter: 300, loss: 0.015, accuracy: 99.54%  epoch: 9, iter: 400, loss: 0.014, accuracy: 99.69%  epoch: 9, iter: 500, loss: 0.014, accuracy: 99.62%  epoch: 9, iter: 600, loss: 0.014, accuracy: 99.69%  epoch: 9, iter: 700, loss: 0.014, accuracy: 99.62%  epoch: 10, iter: 100, loss: 0.014, accuracy: 99.54%  epoch: 10, iter: 200, loss: 0.014, accuracy: 99.54%  epoch: 10, iter: 300, loss: 0.015, accuracy: 99.69%  epoch: 10, iter: 400, loss: 0.014, accuracy: 99.69%  epoch: 10, iter: 500, loss: 0.013, accuracy: 99.62%  epoch: 10, iter: 600, loss: 0.016, accuracy: 99.38%  epoch: 10, iter: 700, loss: 0.017, accuracy: 99.38%

這就是我們的簡單 RNN ,以後有機會我們再嘗試搭建類似 LSTM 這種更複雜的 RNN 。

作者介紹

張覺非,本科畢業於復旦大學,碩士畢業於中國科學院大學,先後任職於新浪微博、阿里,目前就職於奇虎360,任機器學習技術專家。

對作者感興趣的小夥伴,歡迎點擊文末閱讀原文,與作者交流。

——END——

文章推薦:

深度神經網路的梯度消失(動畫)

計算圖反向傳播的原理及實現

DataFun:

專註於大數據、人工智慧領域的知識分享平台。

一個「在看」,一段時光!?