從零開始的機器學習實戰(十四):RNN
當擊球手擊出棒球,你就開始跑,預測球的軌跡然後抓住他,你所做的就是預測未來。在這一章,我們討論的就是迴圈神經網路(RNN),一類預測未來的網路。它們可以分析時間序列資料,諸如股票價格,並告訴你什麼時候買入和賣出。或者預測行車軌跡,避免發生交通意外。一般地說,它們可在任意長度的序列上工作,而不是目前我們討論的只能在固定長度的輸入上工作的網路。它們可以把可變長的語句輸入,使得它們在諸如自動翻譯,語音到文字或者情感分析等自然語言處理系統中極為有用。
迴圈神經元
到目前為止,我們主要關注的是前饋神經網路,其中啟用僅從輸入層到輸出層流動。 RNN與其十分類似,除了它也有連線指向後方。如圖,迴圈神經元收到輸入
和本身上一個時間步的輸出
,我們可以沿著時間軸展開網路:
同理,你也可以很容易的建立一個迴圈神經元層,輸入
和本身上一個時間步的輸出
,都是向量而不是一個神經元下的標量
每個神經元有兩組權重,一組用於輸入
, 另一組用於上一個時間步的輸出
,
公式也可以向量化,對整個小批次進行計算
一般情況下,時間步t處的單元狀態,記為
(h代表“隱藏”),是該時間步的某些輸入和前一時間步的狀態的函式:
。其在時間步t處的輸出,表示為
,我們前面的例子中,輸出等於單元狀態(y=h),但是在更復雜的單元中並不總是如此。
輸入和輸出序列
RNN 可以
輸入一系列,併產生一系列輸出,比如根據前面的情況預測每天股票的情況,根據前面n-1天情況預測第n天的,並向前移動(如左上圖)。
輸入一系列,並忽略除最後一個之外的所有輸出,比如分析一句話的情感是喜歡還是反感(如右上圖)
提供一個輸入(其他為零),輸出一個序列。 這是一個向量到序列的網路。 比如,輸入一幅影象,輸出該影象的標題(如左下角)
有一個序列到向量網路,稱為編碼器,後面跟著一個稱為解碼器的向量到序列網路(右下角)。 例如,這可以用於將句子從一種語言翻譯成另一種語言。聽完整個句子翻譯比一個個單詞翻譯準確的多
TensorFlow實現基本的RNN
我們可以試著不用高階的API,來完成一個簡單的RNN,這個RNN 只執行兩個時間步,每個時間步輸入大小為 3 的向量
import
numpy
as
np
import
tensorflow
as
tf
if
__name__
==
‘__main__’
:
n_inputs
=
3
n_neurons
=
5
X0
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_inputs
])
X1
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_inputs
])
Wx
=
tf
。
Variable
(
tf
。
random_normal
(
shape
=
[
n_inputs
,
n_neurons
],
dtype
=
tf
。
float32
))
Wy
=
tf
。
Variable
(
tf
。
random_normal
(
shape
=
[
n_neurons
,
n_neurons
],
dtype
=
tf
。
float32
))
b
=
tf
。
Variable
(
tf
。
zeros
([
1
,
n_neurons
],
dtype
=
tf
。
float32
))
Y0
=
tf
。
tanh
(
tf
。
matmul
(
X0
,
Wx
)
+
b
)
Y1
=
tf
。
tanh
(
tf
。
matmul
(
Y0
,
Wy
)
+
tf
。
matmul
(
X1
,
Wx
)
+
b
)
init
=
tf
。
global_variables_initializer
()
# Mini-batch: instance 0,instance 1,instance 2,instance 3
X0_batch
=
np
。
array
([[
0
,
1
,
2
],
[
3
,
4
,
5
],
[
6
,
7
,
8
],
[
9
,
0
,
1
]])
# t = 0
X1_batch
=
np
。
array
([[
9
,
8
,
7
],
[
0
,
0
,
0
],
[
6
,
5
,
4
],
[
3
,
2
,
1
]])
# t = 1
with
tf
。
Session
()
as
sess
:
init
。
run
()
Y0_val
,
Y1_val
=
sess
。
run
([
Y0
,
Y1
],
feed_dict
=
{
X0
:
X0_batch
,
X1
:
X1_batch
})
(
Y0_val
,
‘
\n
’
)
(
Y1_val
)
這個網路看起來很像一個雙層前饋神經網路,有一些改動:
兩個層共享相同的權重和偏差項,
每一層都有輸入,並從每個層獲得輸出。
時間上的靜態展開
static_rnn()
函式透過連結單元來建立一個展開的 RNN 網路,下面程式碼展示的和上文相同
X0
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_inputs
])
X1
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_inputs
])
basic_cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
)
output_seqs
,
states
=
tf
。
contrib
。
rnn
。
static_rnn
(
basic_cell
,
[
X0
,
X1
],
dtype
=
tf
。
float32
)
Y0
,
Y1
=
output_seqs
建立輸入佔位符和上文一樣。
建立一個
BasicRNNCell
,它類似一個工廠,建立單元的副本以構建展開的 RNN(每個時間步一個)。
呼叫
static_rnn()
,向它提供單元工廠和輸入張量,並告訴它輸入的資料型別(用來建立初始狀態矩陣,預設情況下是全零)
static_rnn()
函式返回兩個物件:
第一個是包含每個時間步的輸出張量的 Python 列表。
第二個是包含網路最終狀態的張量。 (基本單元時,狀態等於輸出)
如果有50個時間步長,你必須定義50個佔位符和50個輸出的張量,我們可以簡化一下:
X
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_steps
,
n_inputs
])
#增加了一維表示時間步
X_seqs
=
tf
。
unstack
(
tf
。
transpose
(
X
,
perm
=
[
1
,
0
,
2
]))
#首先使用transpose()函式交換前兩個維度,以便時間步驟現在是第一維度。
#然後, unstack()函式沿第一維(時間步)提取張量的 Python 列表
basic_cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
)
output_seqs
,
states
=
tf
。
contrib
。
rnn
。
static_rnn
(
basic_cell
,
X_seqs
,
dtype
=
tf
。
float32
)
outputs
=
tf
。
transpose
(
tf
。
stack
(
output_seqs
),
perm
=
[
1
,
0
,
2
])
X_batch
=
np
。
array
([
# t = 0 t = 1
[[
0
,
1
,
2
],
[
9
,
8
,
7
]],
# instance 1
[[
3
,
4
,
5
],
[
0
,
0
,
0
]],
# instance 2
[[
6
,
7
,
8
],
[
6
,
5
,
4
]],
# instance 3
[[
9
,
0
,
1
],
[
3
,
2
,
1
]],
# instance 4
])
with
tf
。
Session
()
as
sess
:
init
。
run
()
outputs_val
=
outputs
。
eval
(
feed_dict
=
{
X
:
X_batch
}
但是,這種方法仍然會建立一個每個時間步包含一個單元的圖。 如果有 50 個時間步,這個圖看起來會非常難看。 這有點像寫一個程式而沒有使用迴圈。而且可能會發生記憶體不足錯誤,因為它必須在正向傳遞期間儲存所有張量值,以便可以使用它們在反向傳播期間計算梯度。
下面介紹一種更好的方法
時間上的動態展開
dynamic_rnn()
函式使用
while_loop()
操作,在單元上執行適當的次數,如果要在反向傳播期間將 GPU內 存交換到 CPU 記憶體,可以設定
swap_memory = True
,以避免記憶體不足錯誤。 方便的是,還可以在每個時間步接受所有輸入的單個張量,並且在每個時間步上輸出所有輸出的單個張量。 沒有必要堆疊,拆散或轉置。
import
numpy
as
np
import
tensorflow
as
tf
import
pandas
as
pd
if
__name__
==
‘__main__’
:
n_steps
=
2
n_inputs
=
3
n_neurons
=
5
X
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_steps
,
n_inputs
])
basic_cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
)
outputs
,
states
=
tf
。
nn
。
dynamic_rnn
(
basic_cell
,
X
,
dtype
=
tf
。
float32
)
init
=
tf
。
global_variables_initializer
()
X_batch
=
np
。
array
([
[[
0
,
1
,
2
],
[
9
,
8
,
7
]],
# instance 1
[[
3
,
4
,
5
],
[
0
,
0
,
0
]],
# instance 2
[[
6
,
7
,
8
],
[
6
,
5
,
4
]],
# instance 3
[[
9
,
0
,
1
],
[
3
,
2
,
1
]],
# instance 4
])
with
tf
。
Session
()
as
sess
:
init
。
run
()
outputs_val
=
outputs
。
eval
(
feed_dict
=
{
X
:
X_batch
})
(
outputs_val
)
處理變長輸入序列
如果輸入序列具有可變長度(如句子)呢?
這種情況下,你應該在呼叫
dynamic_rnn()
設定
sequence_length
引數,如:
n_steps
=
2
n_inputs
=
3
n_neurons
=
5
reset_graph
()
X
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_steps
,
n_inputs
])
basic_cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
)
seq_length
=
tf
。
placeholder
(
tf
。
int32
,
[
None
])
outputs
,
states
=
tf
。
nn
。
dynamic_rnn
(
basic_cell
,
X
,
dtype
=
tf
。
float32
,
sequence_length
=
seq_length
)
X_batch
=
np
。
array
([
# step 0 step 1
[[
0
,
1
,
2
],
[
9
,
8
,
7
]],
# instance 1
[[
3
,
4
,
5
],
[
0
,
0
,
0
]],
# instance 2 (padded with zero vectors)
[[
6
,
7
,
8
],
[
6
,
5
,
4
]],
# instance 3
[[
9
,
0
,
1
],
[
3
,
2
,
1
]],
# instance 4
])
seq_length_batch
=
np
。
array
([
2
,
1
,
2
,
2
])
with
tf
。
Session
()
as
sess
:
init
。
run
()
outputs_val
,
states_val
=
sess
。
run
(
[
outputs
,
states
],
feed_dict
=
{
X
:
X_batch
,
seq_length
:
seq_length_batch
})
處理變長輸出序列
如果輸出序列長度不一樣呢? 如果事先知道長度,可以按照上面所述設定
sequence_length
引數。但是, 不幸的是通常這是不可能的:例如,翻譯後的句子的長度通常與輸入句子的長度不同。
這種情況下,最常見的解決方案是定義一個稱為序列結束標記(EOS 標記)的特殊輸出。 任何在 EOS 後面的輸出應該被忽略(稍後具體討論)。
訓練 RNN
訓練 RNN,訣竅是在時間上展開(如上所示),然後簡單地使用常規反向傳播(見圖 14-5)。 這個策略被稱為時間上的反向傳播(BPTT)。
和正常的反向傳播一樣,首先是沿著時間前向傳播(虛線),然後輸出部分的時間步被評估(隱藏部分不會計算)。損失函式的梯度透過展開的網路向後傳播(實線);最後使用在 BPTT 期間計算的梯度來更新模型引數。
和一般神經網路不同的是,梯度在損失函式所使用的所有輸出中反向流動,而不僅僅透過最終輸出(例如圖中,Y(2)-Y(4)都是輸出)
訓練序列分類器
我們訓練一個 RNN 來分類 MNIST 影象。 我們將使用 150 個迴圈神經元的單元,再加上一個全連線層,,然後是一個 softmax 層(見圖 14-6)。其他的程式碼和前面幾章介紹的相同。
n_steps
=
28
n_inputs
=
28
n_neurons
=
150
n_outputs
=
10
learning_rate
=
0。001
X
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_steps
,
n_inputs
])
y
=
tf
。
placeholder
(
tf
。
int32
,
[
None
])
basic_cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
)
outputs
,
states
=
tf
。
nn
。
dynamic_rnn
(
basic_cell
,
X
,
dtype
=
tf
。
float32
)
logits
=
tf
。
layers
。
dense
(
states
,
n_outputs
)
xentropy
=
tf
。
nn
。
sparse_softmax_cross_entropy_with_logits
(
labels
=
y
,
logits
=
logits
)
loss
=
tf
。
reduce_mean
(
xentropy
)
optimizer
=
tf
。
train
。
AdamOptimizer
(
learning_rate
=
learning_rate
)
training_op
=
optimizer
。
minimize
(
loss
)
correct
=
tf
。
nn
。
in_top_k
(
logits
,
y
,
1
)
accuracy
=
tf
。
reduce_mean
(
tf
。
cast
(
correct
,
tf
。
float32
))
init
=
tf
。
global_variables_initializer
()
from
tensorflow。examples。tutorials。mnist
import
input_data
mnist
=
input_data
。
read_data_sets
(
“/tmp/data/”
)
X_test
=
mnist
。
test
。
images
。
reshape
((
-
1
,
n_steps
,
n_inputs
))
y_test
=
mnist
。
test
。
labels
batch_size
=
150
with
tf
。
Session
()
as
sess
:
init
。
run
()
for
epoch
in
range
(
n_epochs
):
for
iteration
in
range
(
mnist
。
train
。
num_examples
//
batch_size
):
X_batch
,
y_batch
=
mnist
。
train
。
next_batch
(
batch_size
)
X_batch
=
X_batch
。
reshape
((
-
1
,
n_steps
,
n_inputs
))
sess
。
run
(
training_op
,
feed_dict
=
{
X
:
X_batch
,
y
:
y_batch
})
acc_train
=
accuracy
。
eval
(
feed_dict
=
{
X
:
X_batch
,
y
:
y_batch
})
acc_test
=
accuracy
。
eval
(
feed_dict
=
{
X
:
X_test
,
y
:
y_test
})
(
epoch
,
“Train accuracy:”
,
acc_train
,
“Test accuracy:”
,
acc_test
)
我們獲得了超過 98% 的準確性 ! 另外,透過調整超引數,使用 He 初始化初始化 RNN 權重,更長時間訓練或新增一些正則化(例如,droupout),你肯定會獲得更好的結果
訓練預測時間序列
現在來關注如何處理一個時間序列,比如,股票走勢,氣溫變化,腦電波的規律等。我們將訓練一個 RNN 來預測生成的時間序列中的下一個值。 每個訓練例項是從時間序列中隨機選取的 20 個連續值的序列,如圖:
程式碼與之前幾乎相同
n_steps
=
20
n_inputs
=
1
n_neurons
=
100
n_outputs
=
1
X
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_steps
,
n_inputs
])
y
=
tf
。
placeholder
(
tf
。
float32
,
[
None
,
n_steps
,
n_outputs
])
cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
,
activation
=
tf
。
nn
。
relu
)
outputs
,
states
=
tf
。
nn
。
dynamic_rnn
(
cell
,
X
,
dtype
=
tf
。
float32
)
一般來說,你將不只有一個輸入功能。 例如,預測股票價格時,可能有一些輔助資訊,如分析師的評價等。
每個時間步將會有100個輸出,但是我們只希望給出一個預測值。最簡單的解決方法是將單元包裝在
OutputProjectionWrapper
中。 單元包裝器就像一個普通的單元,但也增加了一些功能。
OutputProjectionWrapper
在每個輸出之上新增一個全連線的線性神經元層,如圖:
cell
=
tf
。
contrib
。
rnn
。
OutputProjectionWrapper
(
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
,
activation
=
tf
。
nn
。
relu
),
output_size
=
n_outputs
)
outputs
,
states
=
tf
。
nn
。
dynamic_rnn
(
cell
,
X
,
dtype
=
tf
。
float32
)
現在我們需要定義損失函式。 我們將使用均方誤差(MSE),建立一個 Adam 最佳化器,訓練操作和變數初始化操作:
learning_rate = 0。001
loss = tf。reduce_mean(tf。square(outputs - y)) # MSE
optimizer = tf。train。AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer。minimize(loss)
init = tf。global_variables_initializer()
saver = tf。train。Saver()
n_iterations = 1500
batch_size = 50
with tf。Session() as sess:
init。run()
for iteration in range(n_iterations):
X_batch, y_batch = next_batch(batch_size, n_steps)
sess。run(training_op, feed_dict={X: X_batch, y: y_batch})
if iteration % 100 == 0:
mse = loss。eval(feed_dict={X: X_batch, y: y_batch})
print(iteration, “\tMSE:”, mse)
saver。save(sess, “。/my_time_series_model”) # not shown in the book
RNN生成新序列
既然我們的模型可以預測未來,就可以生成一些新的序列,正如之前討論的那樣。
提供長度為
n_steps
的種子序列,然後透過模型預測下一時刻的值;把該預測值新增到種子序列的末尾,用最後面長度為
n_steps
的序列做為新的種子序列,做下一次預測,以此類推,如圖:
sequence
=
[
0。
]
*
n_steps
for
iteration
in
range
(
300
):
X_batch
=
np
。
array
(
sequence
[
-
n_steps
:]
。
reshape
(
1
,
n_steps
,
1
)
y_pred
=
sess
。
run
(
outputs
,
feed_dict
=
{
X
:
X_batch
}
sequence
。
append
(
y_pred
[
0
,
-
1
,
0
])
如果你試圖把約翰·列儂的唱片塞給一個 RNN 模型,看它能不能生成下一張《想象》專輯。
約翰·列儂 有一張專輯《Imagine》(1971),這裡取雙關的意思
深度RNN
一個簡單的想法是,把一層層神經元堆疊起來,如圖:
在TF中,可先建立一些神經單元,然後堆疊進
MultiRNNCell
n_neurons
=
100
n_layers
=
3
basic_cell
=
tf
。
contrib
。
rnn
。
BasicRNNCell
(
num_units
=
n_neurons
)
multi_layer_cell
=
tf
。
contrib
。
rnn
。
MultiRNNCell
([
basic_cell
]
*
n_layers
)
outputs
,
states
=
tf
。
nn
。
dynamic_rnn
(
multi_layer_cell
,
X
,
dtype
=
tf
。
float32
)
status
變數包含了每層的一個張量,這個張量就代表了該層神經單元的最終狀態(維度為[batch_size, n_neurons])。如果在建立
MultiRNNCell
時設定了
state_is_tuple=False
,那麼
status
變數就變成了單個張量,它包含了每一層的狀態,其在列的方向上進行了聚合,維度為 [batch_size, n_layers*n_neurons]
在多個 GPU 上分散式部署深度 RNN 網路
如果你嘗試在不同的
device()
塊中建立每個單元格,它將無法工作:
with tf。device(“/gpu:0”): # BAD! This is ignored。
layer1 = tf。contrib。rnn。BasicRNNCell(num_units=n_neurons)
with tf。device(“/gpu:1”): # BAD! Ignored again。
layer2 = tf。contrib。rnn。BasicRNNCell(num_units=n_neurons)
失敗是因為
BasicRNNCell
是一個單元工廠,而不是一個單元本身。建立工廠時不會建立單元格,因此也沒有變數。裝置塊被簡單地忽略了。 單元實際上是後來建立的。
當你呼叫
dynamic_rnn()
時,
dynamic_rnn()
呼叫
MultiRNNCell
,呼叫每個單獨的
BasicRNNCell
,
BasicRNNCell
建立實際的單元格。
秘訣是建立自己的cell Wrapper,你需要一個
DeviceCellWrapper
,這個包裝器只是代理每個方法呼叫到另一個單元格,除了它在裝置塊中包裝
__call __()
函式 。 現在,你可以在不同的GPU上分發每個層:
import tensorflow as tf
class DeviceCellWrapper(tf。contrib。rnn。RNNCell):
def __init__(self, device, cell):
self。_cell = cell
self。_device = device
@property
def state_size(self):
return self。_cell。state_size
@property
def output_size(self):
return self。_cell。output_size
def __call__(self, inputs, state, scope=None):
with tf。device(self。_device):
return self。_cell(inputs, state, scope)
n_inputs = 5
n_steps = 20
n_neurons = 100
X = tf。placeholder(tf。float32, shape=[None, n_steps, n_inputs])
devices = [“/cpu:0”, “/cpu:0”, “/cpu:0”]
# replace with [“/gpu:0”, “/gpu:1”, “/gpu:2”] if you have 3 GPUs
cells = [DeviceCellWrapper(dev,tf。contrib。rnn。BasicRNNCell(num_units=n_neurons))
for dev in devices]
multi_layer_cell = tf。contrib。rnn。MultiRNNCell(cells)
outputs, states = tf。nn。dynamic_rnn(multi_layer_cell, X, dtype=tf。float32)
或者,從TensorFlow 1。1開始,你可以使用
tf。contrib。rnn。DeviceWrapper
類(自TF 1。2以來別名
tf。nn。rnn_cell。DeviceWrapper
)。
應用Dropout
對於深層深度 RNN,在訓練集上很容易過擬合。Dropout 是防止過擬合的常用技術。可以簡單的在 RNN 層之前或之後新增一層 Dropout 層,但如果需要在 RNN 層之間應用 Dropout 技術就需要
DropoutWrapper
。
但是問題在於,Dropout 不管是在訓練還是測試時都起作用了,而我們想要的僅僅是在訓練時應用 Dropout。很不幸的是
DropoutWrapper
不支援
is_training
這樣一個設定選項。因此必須自己寫 Dropout 包裝類,或者建立兩個計算圖,一個用來訓練,一個用來測試。後者可透過如下面程式碼這樣實現:
import sys
is_training = (sys。argv[-1] == “train”)
X = tf。placeholder(tf。float32, [None, n_steps, n_inputs])
y = tf。placeholder(tf。float32, [None, n_steps, n_outputs])
cell = tf。contrib。rnn。BasicRNNCell(num_units=n_neurons)
if is_training:
cell = tf。contrib。rnn。DropoutWrapper(cell, input_keep_prob=keep_prob)
multi_layer_cell = tf。contrib。rnn。MultiRNNCell([cell]*n_layers)
rnn_outpus, status = tf。nn。dynamic_rnn(multi_layer_cell, X, dtype=tf。float32)
[。。。] # bulid the rest of the graph
init = tf。global_variables_initializer()
saver = tf。train。Saver()
with tf。Session() as sess:
if is_training:
init。run()
for iteration in range(n_iterations):
[。。。] # train the model
save_path = saver。save(sess, “/tmp/my_model。ckpt”)
else:
saver。restore(sess, “/tmp/my_model。ckpt”)
[。。。] # use the model
長期訓練的困難
要處理很長的序列,你的RNN要經過許多時間步,就像普通DNN一樣,可能會遭受梯度爆炸/消失的問題,同樣許多解決這些問題的策略也適用於RNN:好的引數初始化方式,非飽和的啟用函式(如 ReLU),批次規範化, 梯度截斷,更快的最佳化器。但即便如此, RNN 在處理適中的長序列(如 100 輸入序列)也在訓練時表現的很慢。
最簡單和常見的方法是僅僅展開限定時間步長的 RNN 網路,可以透過截斷輸入序列來簡單實現這種功能。如減小
n_steps
來實現截斷。當然這種方法會限制模型在長期模式的學習能力。一種折衷方案是包含舊資料和新資料,從而使模型獲得兩者資訊,但是也很難保證從細分類中獲取的資料有效性,這種方案存在著明顯的不足
第二個問題是記憶會在長時間執行的 RNN 網路中逐漸淡去。比如說,你想要分析長篇幅的影評的情感,以
“I love this movie”
開篇,並輔以各種缺點的建議。但如果 RNN 網路逐漸忘記了開頭的幾個詞,RNN 網路的判斷完全有可能完全相反。
為了解決這個問題,各種能夠攜帶長時記憶的神經單元的變體被提出。這些變體是有效的,往往基本形式RNN不怎麼被使用了。
LSTM單元
長短時記憶單元
(LSTM)在 1997 年Sepp Hochreiter和JürgenSchmidhuber於1997年提出,多年來由Alex Graves,HaşimSak,Wojciech Zaremba等研究人員逐漸改進。如果把 LSTM 單元看作一個黑盒,他與基本的RNN單元很相似,但 LSTM 單元會比基本單元效能更好,收斂更快,能夠感知資料的長時依賴。TensorFlow 中透過
BasicLSTMCell
實現 LSTM 單元。
lstm_cell = tf。contrib。rnn。BasicLSTMCell(num_units=n_neurons)
LSTM 單元的工作機制如圖:
LSTM和普通單元的不同在於,狀態分為
兩個向量,h表示短期記憶,c表示長期記憶
長期記憶
從左向右傳播,依次經過遺忘門遺忘一些記憶,然後經過輸入門加上一些新的記憶。新的長期記憶
直接輸出,不再經過任何轉化。另一方面,長期記憶經過tanh變化再透過輸入門,得到短期記憶
,和這時的輸出
輸入
和前一時間的短期記憶
輸入到了四個不同的門中:
最重要的是輸出是
,它分析輸入和上一時間的短期記憶,和基本RNN 一樣,直接輸出到了y和h,不同點在於LSTM 單元會將一部分g儲存在長期記憶
其它三個為
門控制器
。採用 Logistic 作為啟用函式,當輸入為 0 時門關閉,輸出為 1 時門開啟。分別為:
遺忘門
由 f 控制,決定哪些長期記憶需要被擦除;
輸入門
由 i 控制,決定處理哪部分應該被新增到長期記憶中。
輸出門
由 o 控制,決定從長時狀態中讀取哪些記憶到這時的輸出中。
簡要來說,LSTM 單元能夠學習到識別重要輸入(輸入門作用),儲存進長時狀態,並儲存必要的時間(遺忘門功能),並學會提取當前輸出所需要的記憶。這也解釋了 LSTM 單元能夠在提取長時序列,長文字,錄音等資料中的長期模式的驚人成功的原因。
窺孔連線
基本形式的 LSTM 單元中,門的控制僅有當前的輸入x和前一時刻的短期記憶
。有一種思路是讓各個控制門窺視一下長時狀態,獲取一些上下文資訊。該想法由 Felix Gers和Jürgen Schmidhuber於2000年提出,這種LSTM 的變體擁有叫做窺孔連線的額外連線:把前一時刻的長時狀態 加入遺忘門和輸入門控制的輸入,當前時刻的長時狀態加入輸出門的控制輸入。
TensorFlow 中由
LSTMCell
設定
use_peepholes=True
可以實現這種。
lstm_cell = tf。contrib。rnn。LSTMCell(num_units=n_neurons, use_peepholes=True)
GRU單元
GRU由Kyunghyun Cho等人在2014年的論文中提出。GRU單元是LSTM單元的簡化版本,它似乎表現得同樣好(這解釋了它越來越受歡迎)。
GRU的主要簡化是:
長期記憶c和短期記憶h狀態向量合併為單個向量h
單個門控制器控制遺忘門門和輸入門。如果門控制器輸出1,則輸入門開啟,遺忘門關閉。反之亦然。
沒有輸出門,狀態就是輸出。與此同時,增加了一個控制門 r 來控制哪部分前一時間步的狀態在該時刻的單元內呈現。
在 TensoFlow 中建立 GRU 單元很簡單:
gru_cell = tf。contrib。rnn。GRUCell(n_units=n_neurons)
自然語言處理中的應用
詞嵌入
表示一個單詞,一種選擇是one-hot向量。假設你的詞彙表包含 5 萬個單詞,那麼第n個單詞將被表示為 50,000 維的向量,除了第n個位置為 1 之外,其它全部為 0。 然而,你希望相似的單詞具有相似的表示形式,這樣學習到的內容可以推廣到相似的單詞。
最常見的解決方案是,用一個相當小且密集的向量(例如 150 維)表示詞彙表中的每個單詞,稱為嵌入,並讓神經網路在訓練過程中,為每個單詞學習一個良好的嵌入。 在訓練開始時,嵌入只是隨機選擇的,但在訓練過程中,反向傳播會自動更新嵌入,來幫助神經網路執行任務。 通常這意味著,相似的詞會逐漸彼此靠近。
你應該首先對句子進行預處理並將其分解成已知單詞的列表。 例如,你刪除不必要的字元,替換未知單詞,替換數字值等。應用embedding_lookup()函式來獲取相應的嵌入:
vocabulary_size
=
50000
embedding_size
=
150
embeddings
=
tf
。
Variable
(
tf
。
random_uniform
([
vocabulary_size
,
embedding_size
],
-
1。0
,
1。0
))
train_inputs
=
tf
。
placeholder
(
tf
。
int32
,
shape
=
[
None
])
# from ids。。。
embed
=
tf
。
nn
。
embedding_lookup
(
embeddings
,
train_inputs
)
# 。。。to embeddings
一旦你的模型習得了良好的詞嵌入,它們實際上可以在任何 NLP 應用中高效複用:milk和water總是相近的,不論什麼情況,都不可能和shoes相近。 實際上,你可能需要下載預訓練的單詞嵌入,而不是訓練自己的單詞嵌入。 就像複用預訓練一樣,你可以選擇凍結預訓練嵌入(例如,使用
trainable=False
建立嵌入變數),或者讓反向傳播為你的應用調整它們。 第一種選擇將加速訓練,但第二種選擇可能會產生稍高的效能。
對於表示可能擁有大量不同值的類別屬性,嵌入也很有用,特別是當值之間存在複雜的相似性的時候。 例如,考慮職業,愛好,菜品,物種,品牌等。
用於機器翻譯的編解碼器網路
如圖,顯示了一個英語-法語翻譯,英語句子被送進編碼器,解碼器輸出法語翻譯。 注意,法語翻譯也被用作解碼器的輸入,不過向後推了一個時刻(第一個時刻的輸入是
注意,英語句子在送入編碼器之前會反轉。 例如,
“I drink milk”
與
“milk drink I”
相反。這確保了英語句子的開頭將會最後送到編碼器,這很有用,因為這通常是解碼器需要翻譯的第一個東西。
每個單詞最初由簡單整數識別符號表示, 接下來,嵌入查詢返回詞的嵌入。 這些詞的嵌入是實際送到編碼器和解碼器的內容。然後,解碼器輸出輸出詞彙表(法語)中每個詞的得分, 機率最高的詞會輸出。
注意,在預測時期,你不再將目標句子送入解碼器。 相反,只需向解碼器提供它在上一步輸出的單詞,如圖所示(這將需要嵌入查詢,它未在圖中顯示)。
上面大概介紹了Encoder-Decoder過程,但如果檢視
rnn/translate/seq2seq_model。py
中的程式碼(在 TensorFlow 模型中),你會注意到一些重要的區別:
首先,我們已經假定所有輸入序列具有定長。但顯然可能會有所不同。可以採用類似前面介紹的
sequence_length
引數。然而,該程式碼中使用了另一種方法(大概是出於效能原因):句子分到長度相似的桶中,並且使用特殊的填充標記(例如
“
)來填充較短的句子。例如,
“I drink milk”
變成
“
,翻譯成
“Je bois du lait
。然後忽略任何 EOS 標記之後的輸出。
其次,當輸出詞彙表很大時,softmax過程將變得很慢,實際採用了sampled softmax 技術(Sébastien Jean 2015 )在 TensorFlow 中,你可以使用
sampled_softmax_loss()
函式。
第三,該程式碼使用了一種注意力機制,你可以閱讀相關文獻來了解
最後,該程式碼的實現使用了
tf。nn。legacy_seq2seq
模組,提供了輕鬆構建各種編解碼器模型的工具。