如何用TensorFlow構建RNN?這裡有一份極簡的教程
@王小新
編譯自 Medium
量子位 出品 | 公眾號 QbitAI
本文作者Erik Hallström是一名深度學習研究工程師,他的這份教程以Echo-RNN為例,介紹瞭如何在TensorFlow環境中構建一個簡單的迴圈神經網路。
什麼是RNN?
RNN是迴圈神經網路(Recurrent Neural Network)的英文縮寫,它能結合資料點之間的特定順序和幅值大小等多個特徵,來處理序列資料。更重要的是,這種網路的輸入序列可以是任意長度的。
舉一個簡單的例子:數字時間序列,具體任務是根據先前值來預測後續值。在每個時間步中,迴圈神經網路的輸入是當前值,以及一個表徵該網路在之前的時間步中已經獲得資訊的狀態向量。該狀態向量是RNN網路的編碼記憶單元,在訓練網路之前初始化為零向量。
圖1:RNN處理序列資料的步驟示意圖。
本文只對RNN做簡要介紹,主要專注於實踐:如何構建RNN網路。如果有網路結構相關的疑惑,建議多看看說明性文章。
關於RNN的介紹,強烈推薦《A Critical Review of Recurrent Neural Networks for Sequence Learning》,這篇出自加州大學聖地亞哥分校研究人員的文章介紹了幾乎所有最新最全面的迴圈神經網路。(地址:
https://
arxiv。org/pdf/1506。0001
9。pdf
)
在瞭解RNN網路的基本知識後,就很容易理解以下內容。
構建網路
我們先建立一個簡單的回聲狀態網路(Echo-RNN)。這種網路能記憶輸入資料資訊,在若干時間步後將其回傳。我們先設定若干個網路常數,讀完文章你就能明白它們的作用。
from
__future__
import
print_function
,
division
import
numpy
as
np
import
tensorflow
as
tf
import
matplotlib。pyplot
as
plt
num_epochs
=
100
total_series_length
=
50000
truncated_backprop_length
=
15
state_size
=
4
num_classes
=
2
echo_step
=
3
batch_size
=
5
num_batches
=
total_series_length
//
batch_size
//
truncated_backprop_length
生成資料
現在生成隨機的訓練資料,輸入為一個隨機的二元向量,在echo_step個時間步後,可得到輸入的“回聲”,即輸出。
def
generateData
():
x
=
np
。
array
(
np
。
random
。
choice
(
2
,
total_series_length
,
p
=
[
0。5
,
0。5
]))
y
=
np
。
roll
(
x
,
echo_step
)
y
[
0
:
echo_step
]
=
0
x
=
x
。
reshape
((
batch_size
,
-
1
))
# The first index changing slowest, subseries as rows
y
=
y
。
reshape
((
batch_size
,
-
1
))
return
(
x
,
y
)
包含batch_size的兩行程式碼,將資料重構為新矩陣。神經網路的訓練,需要利用小批次資料(mini-batch),來近似得到關於神經元權重的損失函式梯度。在訓練過程中,隨機批次操作能防止過擬合和降低硬體壓力。整個資料集透過資料重構轉化為一個矩陣,並將其分解為多個小批次資料。
圖2:重構資料矩陣的示意圖,箭頭曲線指示了在不同行上的相鄰時間步。淺灰色矩形代表“0”,深灰色矩形代表“1”。
構建計算圖
首先在TensorFlow中建立一個計算圖,指定將要執行的運算。該計算圖的輸入和輸出通常是多維陣列,也被稱為張量(tensor)。我們可以利用CPU、GPU和遠端伺服器的計算資源,在會話中迭代執行該計算圖。
變數和佔位符
本文所用的基本TensorFlow資料結構是變數和佔位符。佔位符是計算圖的“起始節點”。在執行每個計算圖時,批處理資料被傳遞到佔位符中。另外,RNN狀態向量也是儲存在佔位符中,在每一次執行後更新輸出。
batchX_placeholder
=
tf
。
placeholder
(
tf
。
float32
,
[
batch_size
,
truncated_backprop_length
])
batchY_placeholder
=
tf
。
placeholder
(
tf
。
int32
,
[
batch_size
,
truncated_backprop_length
])
init_state
=
tf
。
placeholder
(
tf
。
float32
,
[
batch_size
,
state_size
])
網路的權重和偏差作為TensorFlow的變數,在執行時保持不變,並在輸入批資料後進行逐步更新。
W
=
tf
。
Variable
(
np
。
random
。
rand
(
state_size
+
1
,
state_size
),
dtype
=
tf
。
float32
)
b
=
tf
。
Variable
(
np
。
zeros
((
1
,
state_size
)),
dtype
=
tf
。
float32
)
W2
=
tf
。
Variable
(
np
。
random
。
rand
(
state_size
,
num_classes
),
dtype
=
tf
。
float32
)
b2
=
tf
。
Variable
(
np
。
zeros
((
1
,
num_classes
)),
dtype
=
tf
。
float32
)
下圖表示了輸入資料矩陣,以及虛線視窗指出了佔位符的當前位置。在每次執行時,這個“批處理視窗”根據箭頭指示方向,以定義好的長度從左邊滑到右邊。在示意圖中,batch_size(批資料數量)為3,truncated_backprop_length(截斷反傳長度)為3,total_series_length(全域性長度)為36。這些引數是用來示意的,與實際程式碼中定義的值不一樣。在示意圖中序列各點也以數字標出。
圖3:訓練資料的示意圖,用虛線矩形指示當前批資料,用數字標明瞭序列順序。
拆分序列
現在開始構建RNN計算圖的下個部分,首先我們要以相鄰的時間步分割批資料。
# Unpack columns
inputs_series
=
tf
。
unstack
(
batchX_placeholder
,
axis
=
1
)
labels_series
=
tf
。
unstack
(
batchY_placeholder
,
axis
=
1
)
如下圖所示,可以按批次分解各列,轉成list格式檔案。RNN會同時從不同位置開始訓練時間序列:在示例中分別從4到6、從16到18和從28到30。用plural和series做變數名,是為了強調該變數為list檔案,用來在每一步中表示具有多個位置的時間序列。
圖4:將資料拆分為多列的原理圖,用數字標出序列順序,箭頭表示相鄰的時間步。
在我們的時間序列資料中,在三個位置同時開啟訓練,所以在前向傳播時需要儲存三個狀態。我們在引數定義時就已經考慮到這一點了,故將init_state設定為3。
前向傳播
接下來,我們繼續構建計算圖中執行RNN計算功能的模組。
# Forward pass
current_state
=
init_state
states_series
=
[]
for
current_input
in
inputs_series
:
current_input
=
tf
。
reshape
(
current_input
,
[
batch_size
,
1
])
input_and_state_concatenated
=
tf
。
concat
([
current_input
,
current_state
],
1
)
# Increasing number of columns
next_state
=
tf
。
tanh
(
tf
。
matmul
(
input_and_state_concatenated
,
W
)
+
b
)
# Broadcasted addition
states_series
。
append
(
next_state
)
current_state
=
next_state
在這段程式碼中,我們透過計算current_input * Wa + current_state * Wbin,得到兩個仿射變換的總和input_and_state_concatenated。在連線這兩個張量後,只用了一個矩陣乘法即可在每個批次中新增所有樣本的偏置b。
圖5:第8行程式碼的矩陣計算示意圖,省略了非線性變換arctan。
你可能會想知道變數truncated_backprop_lengthis的作用。在訓練時,RNN被看做是一種在每一層都有冗餘權重的深層神經網路。在訓練開始時,這些層由於展開後佔據了太多的計算資源,因此要在有限的時間步內截斷。在每個批次訓練時,網路誤差反向傳播了三次。
計算Loss
這是計算圖的最後一部分,我們建立了一個從狀態到輸出的全連線層,用於softmax分類,標籤採用One-hot編碼,用於計算每個批次的Loss。
logits_series
=
[
tf
。
matmul
(
state
,
W2
)
+
b2
for
state
in
states_series
]
#Broadcasted addition
predictions_series
=
[
tf
。
nn
。
softmax
(
logits
,
labels
)
for
logits
in
logits_series
]
losses
=
[
tf
。
nn
。
sparse_softmax_cross_entropy_with_logits
(
logits
,
labels
)
for
logits
,
labels
in
zip
(
logits_series
,
labels_series
)]
total_loss
=
tf
。
reduce_mean
(
losses
)
train_step
=
tf
。
train
。
AdagradOptimizer
(
0。3
)
。
minimize
(
total_loss
)
最後一行是新增訓練函式,TensorFlow將自動執行反向傳播函式:對每批資料執行一次計算圖,並逐步更新網路權重。
這裡呼叫的tosparse_softmax_cross_entropy_with_logits函式,能在內部算得softmax函式值後,繼續計算交叉熵。在示例中,各類是互斥的,非0即1,這也是將要採用稀疏自編碼的原因。標籤的格式為[batch_size,num_classes]。
視覺化結果
我們利用視覺化功能tensorboard,在訓練過程中觀察網路訓練情況。它將會在時間維度上繪製Loss值,顯示在訓練批次中資料輸入、資料輸出和網路結構對不同樣本的實時預測效果。
def
plot
(
loss_list
,
predictions_series
,
batchX
,
batchY
):
plt
。
subplot
(
2
,
3
,
1
)
plt
。
cla
()
plt
。
plot
(
loss_list
)
for
batch_series_idx
in
range
(
5
):
one_hot_output_series
=
np
。
array
(
predictions_series
)[:,
batch_series_idx
,
:]
single_output_series
=
np
。
array
([(
1
if
out
[
0
]
<
0。5
else
0
)
for
out
in
one_hot_output_series
])
plt
。
subplot
(
2
,
3
,
batch_series_idx
+
2
)
plt
。
cla
()
plt
。
axis
([
0
,
truncated_backprop_length
,
0
,
2
])
left_offset
=
range
(
truncated_backprop_length
)
plt
。
bar
(
left_offset
,
batchX
[
batch_series_idx
,
:],
width
=
1
,
color
=
“blue”
)
plt
。
bar
(
left_offset
,
batchY
[
batch_series_idx
,
:]
*
0。5
,
width
=
1
,
color
=
“red”
)
plt
。
bar
(
left_offset
,
single_output_series
*
0。3
,
width
=
1
,
color
=
“green”
)
plt
。
draw
()
plt
。
pause
(
0。0001
)
建立訓練會話
已經完成構建網路的工作,開始訓練網路。在TensorFlow中,該計算圖會在一個會話中執行。在每一步開始時,都會隨機生成新的資料。
with
tf
。
Session
()
as
sess
:
sess
。
run
(
tf
。
global_variable
_initializer
())
plt
。
ion
()
plt
。
figure
()
plt
。
show
()
loss_list
=
[]
for
epoch_idx
in
range
(
num_epochs
):
x
,
y
=
generateData
()
_current_state
=
np
。
zeros
((
batch_size
,
state_size
))
(
“New data, epoch”
,
epoch_idx
)
for
batch_idx
in
range
(
num_batches
):
start_idx
=
batch_idx
*
truncated_backprop_length
end_idx
=
start_idx
+
truncated_backprop_length
batchX
=
x
[:,
start_idx
:
end_idx
]
batchY
=
y
[:,
start_idx
:
end_idx
]
_total_loss
,
_train_step
,
_current_state
,
_predictions_series
=
sess
。
run
(
[
total_loss
,
train_step
,
current_state
,
predictions_series
],
feed_dict
=
{
batchX_placeholder
:
batchX
,
batchY_placeholder
:
batchY
,
init_state
:
_current_state
})
loss_list
。
append
(
_total_loss
)
if
batch_idx
%
100
==
0
:
(
“Step”
,
batch_idx
,
“Loss”
,
_total_loss
)
plot
(
loss_list
,
_predictions_series
,
batchX
,
batchY
)
plt
。
ioff
()
plt
。
show
()
從第15-19行可以看出,在每次迭代中往前移動truncated_backprop_length步,但可能有不同的stride值。這樣做的缺點是,為了封裝相關的訓練資料,truncated_backprop_length的值要顯著大於時間依賴值(本文中為3步),否則可能會丟失很多有效資訊,如圖6所示。
圖6:資料示意圖
我們用多個正方形來代表時間序列,上升的黑色方塊表示回波輸出,由輸入回波(黑色方塊)經過三次啟用後得到。滑動批處理視窗在每次執行時也滑動了三次,在示例中之前沒有任何批資料,用來封裝依賴關係,因此它不能進行訓練。
請注意,本文只是用一個簡單示例解釋了RNN如何工作,可以輕鬆地用幾行程式碼中來實現此網路。此網路將能夠準確地瞭解回聲行為,因此不需要任何測試資料。
在訓練過程中,該程式實時更新圖表,如圖7所示。藍色條表示用於訓練的輸入訊號,紅色條表示訓練得到的輸出回波,綠色條是RNN網路產生的預測回波。不同的條形圖顯示了在當前批次中多個批資料的預測回波。
我們的演算法能很快地完成訓練任務。左上角的圖表輸出了損失函式,但為什麼曲線上有尖峰?答案就在下面。
圖7:各圖分別為Loss,訓練的輸入和輸出資料(藍色和紅色)以及預測回波(綠色)。
尖峰的產生原因是在新的迭代開始時,會產生新的資料。由於矩陣重構,每行上的第一個元素與上一行中的最後一個元素會相鄰。但是所有行中的前幾個元素(第一個除外)都具有不包含在該狀態中的依賴關係,因此在最開始的批處理中,網路的預測功能不良。