全面整理LSTM相關原理泄朴,源碼,以及開發(fā)demo露氮,設計習題祖灰。如轉載請注明轉載出處。
LSTM 框架
lstm 由3個門和一個當前細胞輸出值也就是 來控制輸入
- 遺忘門 - 表示 有多少被遺忘.
- 輸入門 - 表示當前時刻有多少被保存下來畔规。
- 輸出門 - 控制多少信息被輸出到隱層局扶。
- 當前Cell輸出 用 - 當前t時刻的實際 cell結果,
通過3個門以及 計算如下:
- - 當前細胞輸出。
- - 隱層輸出油讯。
參數(shù)計算
一共3個門加一個详民,所以一共 4組參數(shù), size是 input_size+hidden_size
, 因為 輸出的維度是與 一樣的也就是 hidden_size
, 所以 的維度是 (input_size+hidden_size, hidden_size)
. 的size就是 hdden_size
. 所以總共的參數(shù)量就是:
4 * ((input_size+hidden_size) * hidden_size + hidden_size)
GRU
GRU由2個門以及一個 隱層輸出值 也叫做 來控制最終的
- - 重置門.
- - 更新門.
- - 隱層細胞輸出.
- - 最后輸出.
從上面來看延欠,GRU 實際上比LSTM 少了一組參數(shù)陌兑。在數(shù)據(jù)量較大的時候使用lstm,而數(shù)據(jù)量較少使用GRU. 同時GRU不像lstm由捎,有兩個輸出兔综,GRU只有一個
pytorch lstm 解析
Parameters
- input_size – The number of expected features in the input x
- hidden_size – The number of features in the hidden state h
- num_layers – Number of recurrent layers. E.g., setting num_layers=2 would mean stacking two LSTMs together to form a stacked LSTM, with the second LSTM taking in outputs of the first LSTM and computing the final results. Default: 1
- bias – If False, then the layer does not use bias weights b_ih and b_hh. Default: True
- batch_first – If True, then the input and output tensors are provided as (batch, seq, feature). Default: False
- dropout – If non-zero, introduces a Dropout layer on the outputs of each LSTM layer except the last layer, with dropout probability equal to dropout. Default: 0
- bidirectional – If True, becomes a bidirectional LSTM. Default: False
Inputs: input, (h_0, c_0)
-
input of shape
(seq_len, batch, input_size)
: tensor containing the features of the input sequence. The input can also be a packed variable length sequence. Seetorch.nn.utils.rnn.pack_padded_sequence
ortorch.nn.utils.rnn.pack_sequence
for details. -
h_0 of shape
(num_layers * num_directions, batch, hidden_size)
: tensor containing the initial hidden state for each element in the batch. If the LSTM is bidirectional, num_directions should be 2, else it should be 1. -
c_0 of shape
(num_layers * num_directions, batch, hidden_size)
: tensor containing the initial cell state for each element in the batch. If(h_0, c_0)
is not provided, both h_0 and c_0 default to zero.
Outputs: output, (h_n, c_n)
-
output of shape
(seq_len, batch, num_directions * hidden_size)
: tensor containing the output features(h_t)
from the last layer of the LSTM, for eacht
. If a :class:torch.nn.utils.rnn.PackedSequence
has been given as the input, the output will also be a packed sequence. For the unpacked case, the directions can be separated usingoutput.view(seq_len, batch, num_directions, hidden_size)
, with forward and backward being direction0
and1
respectively. Similarly, the directions can be separated in the packed case. 從shape來看, 實際上這是所有h_t
的輸出. 所以對于 seq2seq 來說這個結果就夠用了。
-
h_n of shape
(num_layers * num_directions, batch, hidden_size)
: tensor containing the hidden state fort = seq_len
. Like output, the layers can be separated usingh_n.view(num_layers, num_directions, batch, hidden_size)
and similarly for c_n. 從這個shaplenum_layers * num_directions
來看狞玛,說明這是每一層的最后一個輸出软驰。對于最上面的一層來說,這個是包含最后一個輸出的心肪。也就是-1, batch, hidden_size
就是最后一個輸出锭亏。特別注意,即使設置了batch _first=True
,h_n
的維度依然是num_layers * num_directions, batch, hidden_size
如果要轉換需要h_n.transpose(0, 1)
來轉換到batch, num_layers * num_directions, hidden_size
-
c_n of shape
(num_layers * num_directions, batch, hidden_size)
: tensor containing the cell state fort = seq_len
. 與h_n
類似, 是每一層的cell_state
輸出硬鞍。
變長序列處理
我們知道 lstm, 處理的是定長序列慧瘤,那么,對于nlp來說固该,變長序列該如何處理?
對于變長序列處理锅减,采用了:
-
pack_padded_sequence
: 將變長序列打包 -
pad_packed_sequence
: 將打包的結果解包
pack_padded_sequence
在理解這兩個函數(shù)之前,我們看看lstm是如何進行運行的伐坏。
No. | |||||
---|---|---|---|---|---|
0 | I | love | mom | ' | cooking |
1 | Yes | 0 | 0 | 0 | 0 |
2 | No | way | 0 | 0 | 0 |
3 | I | love | you | too | ! |
4 | This | is | the | shit | 0 |
一共有5個序列怔匣,長度各種各樣。其中的 0 表示的是padding. 因為一個batch必須是有同樣維度的才可以桦沉。
對于lstm的實際運行中會將長度一樣的放在每瞒,這樣能夠批量運行同一批。所以會按照長度進行排序纯露。重新排列的結果如下:
No. | |||||
---|---|---|---|---|---|
0 | I | love | mom | ' | cooking |
3 | I | love | you | too | ! |
4 | This | is | the | shit | |
2 | No | way | |||
1 | Yes |
這個過程就是pack的過程剿骨。pack之后,會重新將長度一樣的放在一起苔埋,因為是長度一樣的放在一起懦砂,那么,也就是將padding的0全部去掉后的排列結果。
示例圖:
-
batch_sizes
: 看著其中不同的顏色荞膘。綠色:5, 橘色: 4 以此類推罚随。 那么, pytorch在實際運算的時候是如何運算的呢?
批量運算羽资,會一次性將上面所有數(shù)據(jù)進行運算淘菩。大體流程:
- 設置循環(huán)步數(shù),為
max_length=batch_sizes[0]
, 這里就是5 - 開始循環(huán) i=0:
- 設置輸入為
batch_size[i]
進入 lstm cell 批量運算. (i=0 時是 綠色的5個, i=1時是橘色的4個屠升,一次類推) - i = i + 1
- 設置輸入為
這樣每一次處理實際上是運算遞減的潮改,同時,進行的也是 序列實際長度的lstm運算腹暖。(PS: 早期汇在,因為看到padding, 所以以為會將padding一起運算,這樣就不用進行pack了脏答,但是, 這樣會增加運算量糕殉,同時, 對于 最后一個輸出 不是實際序列最后一個輸出,而是 padding后的輸出殖告。而看了pytorch源碼后阿蝶,理解,在每一次的 lstm cell 運算黄绩,會重新取batch, 而這個batch是變化羡洁,與實際sequence長度一致. 從這個角度來看,我覺得之所以pack爽丹,對長度排序筑煮,是為了方便 每一次 lstm cell 取batch 方便運算; 如果不排序,每一次通過mask取會在lstm循環(huán)運算的時候效率較低)
pytorch c++ 源碼 aten/src/ATen/native/RNN.cpp
:
template<typename hidden_type, typename cell_params>
struct PackedLayer : Layer<PackedSequence, hidden_type, cell_params> {
using output_type = typename Layer<PackedSequence, hidden_type, cell_params>::output_type;
PackedLayer(Cell<hidden_type, cell_params>& cell)
: cell_(cell) {};
output_type operator()(
const PackedSequence& input,
const hidden_type& input_hidden,
const cell_params& params) const override
{
std::vector<at::Tensor> step_outputs;
std::vector<hidden_type> hiddens;
int64_t input_offset = 0;
int64_t num_steps = input.batch_sizes.size(0);
int64_t* batch_sizes = input.batch_sizes.data<int64_t>();
int64_t last_batch_size = batch_sizes[0];
// Batch sizes is a sequence of decreasing lengths, which are offsets
// into a 1D list of inputs. At every step we slice out batch_size elements,
// and possibly account for the decrease in the batch size since the last step,
// which requires us to slice the hidden state (since some sequences
// are completed now). The sliced parts are also saved, because we will need
// to return a tensor of final hidden state.
auto hidden = input_hidden;
for (int64_t i = 0; i < num_steps; ++i) {
int64_t batch_size = batch_sizes[i];
auto step_input = input.data.narrow(0, input_offset, batch_size);
input_offset += batch_size;
int64_t dec = last_batch_size - batch_size;
if (dec > 0) {
hiddens.push_back(hidden_slice(hidden, last_batch_size - dec, last_batch_size));
hidden = hidden_slice(hidden, 0, last_batch_size - dec);
}
last_batch_size = batch_size;
hidden = cell_(step_input, hidden, params);
step_outputs.push_back(hidden_as_output(hidden));
}
hiddens.push_back(hidden);
std::reverse(hiddens.begin(), hiddens.end());
return { PackedSequence{ at::cat(step_outputs, 0), input.batch_sizes }, hidden_concat(hiddens) };
}
Cell<hidden_type, cell_params>& cell_;
};
解釋:
-
num_steps
: 就是最長的batch 其實也就是最長的sequence length -
input.data.narrow(0, input_offset, batch_size)
: 從batch_size
中取每一步 lstm cell 要運算的 所有sequence , 對應到代碼就是 -
dec = last_batch_size - batch_size
: 對于其他沒有參與到 lstm cell 運算的习劫,用0來補上咆瘟,保證lstm運算后,所有的 sequence hidden layer 長度是一樣的诽里。
結論: 在pack后的變長序列袒餐,運算每一步都是有效運算。所以在來看 lstm 的輸出
- output: shape
(seq_len, batch, num_directions * hidden_size)
是包含padding的序列長度(padding的為0) -
h_n
: shape(num_layers * num_directions, batch, hidden_size)
, 實際的sequence 長度計算的lstm 最后一個 隱層谤狡,與padding無關. 從shape看是所有l(wèi)ayer的最后一個隱層. -
c_n
: shape(num_layers * num_directions, batch, hidden_size)
, 實際的sequence 長度計算的 lstm 最后一個 cell state 與padding無關. 從shape看是所有l(wèi)ayer的最后一層
enforce_sorted 參數(shù)
這個參數(shù)特別說明一下, 默認是 True
, 也就是說 輸入的batch sequence 必須是按照長度降序排好序的灸眼。
如果這個參數(shù)是 False
, 那么,這個排序的工作會由 pack_padded_sequence
來做墓懂。
lstm demo
下面的demo包含了前面說的參數(shù)設置焰宣。
import torch
from torch.nn.modules.rnn import LSTM
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
X_SORTED = torch.tensor([
[
[1, 2], [3, 4], [0, 0]
],
[
[5, 6], [7, 8], [0, 0]
],
[
[9, 10], [0, 0], [0, 0]
]
], dtype=torch.float)
X_UNSORTED = torch.tensor([
[
[1, 2], [3, 4], [0, 0]
],
[
[9, 10], [0, 0], [0, 0]
],
[
[5, 6], [7, 8], [0, 0]
]
], dtype=torch.float)
sequence_length_sorted = torch.tensor([2, 2, 1], dtype=torch.long)
sequence_length_unsorted = torch.tensor([2, 1, 2], dtype=torch.long)
def demo_pack_padded_sequence(x, sequence_length, is_sorted):
print(f"x: {x.numpy()}")
print(f"Batch size: {x.shape[0]}, "
f"Sequence length: {x.shape[1]}, "
f"hidden dim: {x.shape[2]}")
print(f"sequence length: {sequence_length.numpy()}")
pack = pack_padded_sequence(input=x,
lengths=sequence_length,
batch_first=True,
enforce_sorted=is_sorted)
print(f"pack: {pack}")
pad = pad_packed_sequence(sequence=pack, batch_first=True)
print(f"pad: {pad}")
lstm = LSTM(input_size=x.shape[-1],
hidden_size=4,
num_layers=1,
batch_first=True,
bidirectional=False)
output, (hn, cn) = lstm(pack)
print("output", "-" * 80)
print(output)
pad_output = pad_packed_sequence(output, batch_first=True, padding_value=0.0)
print("+" * 80)
print(f"output: {pad_output}")
print(f"hn: {hn}")
print(f"cn: {cn}")
print(f"output[:-1:]: {output[:-1:]}")
demo_pack_padded_sequence(x=X_SORTED,
sequence_length=sequence_length_sorted,
is_sorted=True)
x: [[[ 1. 2.]
[ 3. 4.]
[ 0. 0.]]
[[ 5. 6.]
[ 7. 8.]
[ 0. 0.]]
[[ 9. 10.]
[ 0. 0.]
[ 0. 0.]]]
Batch size: 3, Sequence length: 3, hidden dim: 2
sequence length: [2 2 1]
pack: PackedSequence(data=tensor([[ 1., 2.],
[ 5., 6.],
[ 9., 10.],
[ 3., 4.],
[ 7., 8.]]), batch_sizes=tensor([3, 2]), sorted_indices=None, unsorted_indices=None)
pad: (tensor([[[ 1., 2.],
[ 3., 4.]],
[[ 5., 6.],
[ 7., 8.]],
[[ 9., 10.],
[ 0., 0.]]]), tensor([2, 2, 1]))
output --------------------------------------------------------------------------------
PackedSequence(data=tensor([[-0.0644, 0.1670, 0.1466, 0.0274],
[-0.0926, 0.1344, 0.4401, 0.0731],
[-0.0940, 0.1009, 0.5912, 0.0238],
[-0.1254, 0.2073, 0.3820, 0.0958],
[-0.1485, 0.1642, 0.5456, 0.0616]], grad_fn=<CatBackward>), batch_sizes=tensor([3, 2]), sorted_indices=None, unsorted_indices=None)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
output: (tensor([[[-0.0644, 0.1670, 0.1466, 0.0274],
[-0.1254, 0.2073, 0.3820, 0.0958]],
[[-0.0926, 0.1344, 0.4401, 0.0731],
[-0.1485, 0.1642, 0.5456, 0.0616]],
[[-0.0940, 0.1009, 0.5912, 0.0238],
[ 0.0000, 0.0000, 0.0000, 0.0000]]], grad_fn=<TransposeBackward0>), tensor([2, 2, 1]))
hn: tensor([[[-0.1254, 0.2073, 0.3820, 0.0958],
[-0.1485, 0.1642, 0.5456, 0.0616],
[-0.0940, 0.1009, 0.5912, 0.0238]]], grad_fn=<StackBackward>)
cn: tensor([[[-0.1698, 0.4718, 0.7097, 0.4341],
[-0.1641, 0.3882, 0.8470, 1.2613],
[-0.0996, 0.2737, 0.8373, 0.9169]]], grad_fn=<StackBackward>)
output[:-1:]: (tensor([[-0.0644, 0.1670, 0.1466, 0.0274],
[-0.0926, 0.1344, 0.4401, 0.0731],
[-0.0940, 0.1009, 0.5912, 0.0238],
[-0.1254, 0.2073, 0.3820, 0.0958],
[-0.1485, 0.1642, 0.5456, 0.0616]], grad_fn=<CatBackward>), tensor([3, 2]), None)
特別提示: hn
的結果是包含在 output
中的, 如何從 output 中提取出 hn
參考習題4
lstm 應用
從前面闡述,明白了lstm實際的原理和輸出捕仔。那么匕积,在實際應用的時候盈罐,如果是 使用 每一個時間步的隱層進行運算, 那么,要注意將mask進入運算闪唆,因為輸出的隱層是包含padding部分的盅粪。當然可以利用,padding的值全是0,是有些便利計算方法的悄蕾,但是不推薦票顾,要使用mask運算。
如果是使用lstm最后一個隱層的輸出帆调,那么奠骄,直接使用就可以了。
習題
sequence 長度是 , embedding 維度是 , 隱層輸出維度是 , 計算 lstm 參數(shù)是多少?
lstm 對變長序列padding番刊,在實際計算lstm cell的時候 padding 部分是否參與計算? 如果不參與計算含鳞,lstm是如何進行變長計算的?
lstm 輸出
output
(也就是 每個時間步的hidden輸出) 是否 包含h_n
輸出?如果不包含撵枢,請說明情況?使用
h_n
, 如何提取 最后一個 最后的 最后一層的 hidden 輸出 (最后的hidden輸出常常作為整個句子的編碼結果); 在不使用h_n
的情況下, 使用 lstm 輸出output
(也就是 每個時間步的hidden輸出), 如何提取出最后一個 最后一層的 hidden 輸出?是否注意到了mask的使用民晒?