Kiến trúc mạng học sâu: từ nền tảng đến LLM·Chương 5 — GRU: recurrent unit gọn hơn

Chế độ xem

GRU: reset gate, update gate và trạng thái ẩn

Mục tiêu

Sau bài này, bạn cần:

  • giải thích vì sao GRU được xem là một recurrent unit gọn hơn LSTM;
  • phân biệt vai trò của reset gate rtr_t và update gate ztz_t;
  • đọc được các phương trình của GRU và theo dõi dòng thông tin qua một cell;
  • hiểu vì sao GRU chỉ cần một trạng thái hth_t thay vì cặp (ht,ct)(h_t,c_t) như LSTM;
  • hiểu ở mức ý tưởng vì sao phép nội suy giữa trạng thái cũ và trạng thái ứng viên giúp học phụ thuộc dài;
  • nhận ra khác biệt nhỏ giữa công thức GRU gốc và cách torch.nn.GRU tính candidate state;
  • tránh kết luận sai kiểu “GRU luôn nhanh hơn LSTM” hoặc “GRU giải quyết hoàn toàn vanishing gradient”.

1. Từ Vanilla RNN và LSTM đến GRU

Vanilla RNN cập nhật trạng thái bằng một phép biến đổi kiểu:

ht=tanh⁡(Wxxt+Whht−1+b)h_t=\tanh(W_xx_t+W_hh_{t-1}+b)

Cấu trúc rất gọn, nhưng mỗi bước gần như tạo lại toàn bộ trạng thái mới. Khi cần học quan hệ cách nhau nhiều bước thời gian, gradient phải đi qua một chuỗi dài các phép biến đổi và có thể suy giảm hoặc tăng mạnh.

LSTM xử lý vấn đề này bằng một cơ chế bộ nhớ có cổng. Phiên bản LSTM hiện đại thường có:

  • cell state ctc_t;
  • hidden state hth_t;
  • forget gate;
  • input gate;
  • output gate;
  • candidate state.

GRU - Gated Recurrent Unit - giữ ý tưởng cốt lõi “hãy để mạng tự học khi nào cần giữ và khi nào cần cập nhật”, nhưng gộp bớt thành phần.

GRU xuất phát từ recurrent hidden unit được Cho et al. đề xuất năm 2014 trong mô hình RNN Encoder-Decoder cho dịch máy thống kê. Thiết kế này được mô tả là lấy cảm hứng từ LSTM nhưng đơn giản hơn về tính toán và triển khai.

Điểm cần nhớ ngay từ đầu:

GRU không phải “LSTM bị cắt bớt tùy ý”. Nó dùng một cách tổ chức trạng thái khác.

2. GRU chỉ truyền một trạng thái

LSTM truyền hai vector:

(ht,ct)(h_t,c_t)

Trong khi GRU chủ yếu truyền:

hth_t

Không có cell state ctc_t riêng.

Điều đó không có nghĩa GRU “không có bộ nhớ”. Trong RNN, chính trạng thái hồi tiếp mang thông tin từ quá khứ. GRU điều khiển việc giữ hay thay đổi trạng thái đó bằng các cổng.

Hai cổng chính là:

  • reset gate rtr_t - điều khiển lượng thông tin quá khứ được dùng để tạo nội dung ứng viên;
  • update gate ztz_t - điều khiển mức giữ trạng thái cũ so với cập nhật bằng nội dung ứng viên.

3. Bốn phương trình cốt lõi

Ta dùng dạng gần với công thức gốc của Cho et al. (2014).

Reset gate

rt=σ(Wrxt+Urht−1+br)r_t=\sigma(W_rx_t+U_rh_{t-1}+b_r)

Update gate

zt=σ(Wzxt+Uzht−1+bz)z_t=\sigma(W_zx_t+U_zh_{t-1}+b_z)

Trạng thái ứng viên

h~t=tanh⁡(Whxt+Uh(rt⊙ht−1)+bh)\tilde{h}_t=\tanh\left(W_hx_t+U_h(r_t\odot h_{t-1})+b_h\right)

Trạng thái mới

ht=zt⊙ht−1+(1−zt)⊙h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t

Trong đó:

  • σ\sigma là sigmoid;
  • ⊙\odot là phép nhân từng phần tử;
  • rt,zt,ht,h~tr_t,z_t,h_t,\tilde{h}_t là các vector;
  • các ma trận WW xử lý đầu vào hiện tại;
  • các ma trận UU xử lý trạng thái hồi tiếp.

4. Toàn bộ một GRU cell

Có thể đọc cell theo hai câu hỏi:

  1. Khi tạo một phương án trạng thái mới, cần nhìn lại quá khứ bao nhiêu? - rtr_t.
  2. Sau khi đã có phương án mới, nên giữ trạng thái cũ hay chuyển sang trạng thái mới bao nhiêu? - ztz_t.

5. Reset gate: quá khứ nào cần dùng để tạo trạng thái ứng viên?

Reset gate được tính từ cả đầu vào hiện tại và trạng thái trước đó:

rt=σ(Wrxt+Urht−1+br)r_t=\sigma(W_rx_t+U_rh_{t-1}+b_r)

Vì sigmoid cho giá trị giữa 0 và 1, từng chiều của rtr_t có thể điều tiết từng chiều của ht−1h_{t-1}.

Trong công thức gốc:

h~t=tanh⁡(Whxt+Uh(rt⊙ht−1)+bh)\tilde{h}_t=\tanh\left(W_hx_t+U_h(r_t\odot h_{t-1})+b_h\right)

Nếu một phần tử của rtr_t gần 0, phần tương ứng của trạng thái cũ gần như bị loại khỏi quá trình tạo candidate.

Nếu gần 1, candidate có thể dùng mạnh thông tin cũ ở chiều đó.

Ví dụ ý tưởng

Giả sử mô hình đọc chuỗi sự kiện:

Sinh viên A -> đăng nhập -> làm quiz -> đăng xuất -> Sinh viên B -> đăng nhập

Tại ranh giới sang “Sinh viên B”, một số thông tin gắn riêng với sinh viên A có thể không còn hữu ích khi tạo trạng thái mới. Reset gate cho phép candidate giảm phụ thuộc vào các chiều trạng thái đó.

Không nên hiểu rằng toàn bộ GRU “reset về 0”. Gate là vector và chỉ tham gia vào cách tính candidate.

6. Update gate: giữ trạng thái cũ hay nhận trạng thái mới?

Phương trình quan trọng nhất của GRU là:

ht=zt⊙ht−1+(1−zt)⊙h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t

Đây là phép nội suy từng phần tử giữa:

  • trạng thái cũ ht−1h_{t-1};
  • trạng thái ứng viên h~t\tilde{h}_t.

Khi ztz_t gần 1

ht≈ht−1h_t\approx h_{t-1}

Mô hình chủ yếu giữ trạng thái cũ.

Khi ztz_t gần 0

ht≈h~th_t\approx\tilde{h}_t

Mô hình chủ yếu cập nhật bằng candidate mới.

Khi ở giữa

Ví dụ zt=0.7z_t=0.7:

ht=0.7ht−1+0.3h~th_t=0.7h_{t-1}+0.3\tilde{h}_t

Trạng thái thay đổi mềm thay vì bị thay hoàn toàn.

7. Cẩn thận với tên gọi và quy ước của update gate

Trong công thức gốc của Cho et al.:

ht=zt⊙ht−1+(1−zt)⊙h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t

Do đó ztz_t lớn nghĩa là giữ nhiều trạng thái cũ.

Một số tài liệu khác có thể viết công thức với ký hiệu hoặc quy ước ngược lại. Vì vậy khi đọc paper hay code:

Đừng suy luận từ tên biến. Hãy nhìn trực tiếp phương trình cập nhật hth_t.

Đây là một thói quen đọc paper quan trọng vì cùng tên “update gate” nhưng ký hiệu trong từng nguồn có thể khác nhau.

8. Reset gate và update gate không làm cùng một việc

Hai gate thường bị nhầm.

Reset gate

Tác động vào câu hỏi:

Candidate mới có cần dựa nhiều vào quá khứ không?

Update gate

Tác động vào câu hỏi:

Trạng thái cuối cùng ở bước này nên giống trạng thái cũ hay candidate mới?

Một reset gate nhỏ không đồng nghĩa trạng thái cũ chắc chắn biến mất khỏi hth_t, vì update gate vẫn có thể giữ trực tiếp ht−1h_{t-1}.

9. Ví dụ số một chiều

Giả sử một chiều trạng thái có:

ht−1=0.8h_{t-1}=0.8

Sau khi tính reset gate và candidate, ta có:

rt=0.25,h~t=0.1r_t=0.25,\qquad \tilde{h}_t=0.1

Giả sử update gate:

zt=0.75z_t=0.75

Khi đó:

ht=0.75×0.8+(1−0.75)×0.1h_t=0.75\times0.8+(1-0.75)\times0.1 ht=0.60+0.025=0.625h_t=0.60+0.025=0.625

Ta có thể đọc kết quả như sau:

  • 75% đường cập nhật trực tiếp ưu tiên trạng thái cũ;
  • 25% ưu tiên candidate mới;
  • candidate đã được tạo sau khi reset gate giảm ảnh hưởng của một phần quá khứ.

Đây chỉ là cách diễn giải ở một chiều để hiểu cơ chế. Trong mô hình thật, các giá trị là vector và chúng được học từ dữ liệu.

10. Một ví dụ nhiều chiều

Giả sử:

ht−1=[0.9,  −0.4,  0.6]h_{t-1}=[0.9,\;-0.4,\;0.6]

và:

zt=[0.95,  0.10,  0.50]z_t=[0.95,\;0.10,\;0.50]

Ta chưa cần biết candidate cụ thể để thấy rằng:

  • chiều 1 có xu hướng giữ mạnh trạng thái cũ;
  • chiều 2 sẵn sàng cập nhật mạnh;
  • chiều 3 pha trộn tương đối cân bằng.

Vì gate là vector, một GRU cell có thể đồng thời giữ thông tin dài hạn ở một số chiều và thay đổi nhanh ở những chiều khác.

Cho et al. mô tả chính khả năng này như việc các hidden unit có thể học những thang thời gian khác nhau.

11. Vì sao update gate có thể giúp học phụ thuộc dài?

Vanilla RNN luôn có dạng biến đổi mạnh:

ht=tanh⁡(…ht−1…)h_t=\tanh(\ldots h_{t-1}\ldots)

Trong GRU, tồn tại một đường cộng trực tiếp:

ht=zt⊙ht−1+(1−zt)⊙h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t

Nếu tại nhiều bước liên tiếp mô hình học ztz_t tương đối lớn, thông tin trong ht−1h_{t-1} có thể được truyền tiếp với mức thay đổi nhỏ.

Đây là khác biệt quan trọng so với việc buộc toàn bộ trạng thái phải đi qua một phép biến đổi phi tuyến hoàn toàn mới ở mỗi bước.

Nhưng không nên nói quá mức

Không thể viết đơn giản:

∂ht∂ht−1=zt\frac{\partial h_t}{\partial h_{t-1}}=z_t

và coi đó là toàn bộ gradient.

Lý do:

  • ztz_t phụ thuộc vào ht−1h_{t-1};
  • rtr_t phụ thuộc vào ht−1h_{t-1};
  • h~t\tilde{h}_t cũng phụ thuộc vào ht−1h_{t-1}.

Gradient thật còn nhiều nhánh khác.

Điểm đúng ở mức ý tưởng là GRU tạo đường cập nhật cộng và có cổng giúp trạng thái cũ có thể được truyền trực tiếp hơn. GRU làm việc học phụ thuộc dài dễ hơn Vanilla RNN trong nhiều trường hợp, nhưng không đảm bảo loại bỏ hoàn toàn vanishing hay exploding gradient.

12. GRU liên hệ với LSTM như thế nào?

Một cách so sánh cơ chế:

Thành phầnLSTMGRU
Trạng thái hồi tiếphth_t và ctc_thth_t
Cổng chínhforget, input, outputreset, update
Candidatec~t\tilde{c}_th~t\tilde{h}_t
Đường giữ thông tinqua cell statetrực tiếp trong hth_t
Đầu ra của cellđược điều tiết bởi output gatechính là hth_t

GRU không có một output gate riêng. Trạng thái sau khi cập nhật vừa là bộ nhớ hồi tiếp vừa là biểu diễn đầu ra của recurrent unit.

13. Vì sao gọi GRU là “gọn hơn”?

Với input dimension DD và hidden dimension HH, bỏ qua chi tiết bias:

Vanilla RNN

Cần một nhóm phép biến đổi chính:

DH+H2DH+H^2

GRU

Có ba nhóm affine chính:

  • reset gate;
  • update gate;
  • candidate.

Do đó số trọng số lõi xấp xỉ:

3(DH+H2)3(DH+H^2)

LSTM

Có bốn nhóm:

  • forget gate;
  • input gate;
  • output gate;
  • candidate.

Do đó:

4(DH+H2)4(DH+H^2)

Với cùng DD và HH, phần recurrent affine của GRU có số trọng số bằng khoảng:

34=75%\frac{3}{4}=75\%

so với LSTM.

Nói cách khác, ít hơn khoảng 25% trọng số recurrent trong cấu hình cơ bản cùng kích thước.

Điều này không đồng nghĩa mô hình chắc chắn chạy nhanh hơn đúng 25%. Tốc độ thực tế còn phụ thuộc:

  • implementation;
  • kernel GPU/CPU;
  • batch size;
  • sequence length;
  • memory access;
  • backend như cuDNN.

14. Một chi tiết implementation quan trọng: paper gốc và PyTorch không hoàn toàn giống nhau

Công thức candidate trong paper gốc có dạng:

h~t=tanh⁡(Wxxt+Uh(rt⊙ht−1)+b)\tilde{h}_t=\tanh\left(W_xx_t+U_h(r_t\odot h_{t-1})+b\right)

Tức là reset gate được nhân với hidden state trước khi qua recurrent matrix UhU_h.

Trong torch.nn.GRU, tài liệu PyTorch mô tả candidate theo dạng:

nt=tanh⁡(Winxt+bin+rt⊙(Whnht−1+bhn))n_t=\tanh\left(W_{in}x_t+b_{in}+r_t\odot(W_{hn}h_{t-1}+b_{hn})\right)

Tức là recurrent linear transformation được tính trước, sau đó mới nhân reset gate.

PyTorch ghi rõ đây là khác biệt có chủ đích để tăng hiệu quả implementation.

Hệ quả khi học và đọc code

  • về mặt ý tưởng, reset gate vẫn điều tiết ảnh hưởng của quá khứ lên candidate;
  • nhưng nếu bạn tự cài GRU theo paper rồi so từng số với nn.GRU, không nên kỳ vọng kết quả trung gian giống hệt chỉ vì dùng cùng tên tham số;
  • khi tái lập paper, phải kiểm tra đúng biến thể/công thức của framework.

15. Tự viết một GRU cell nhỏ để nhìn gate

Ví dụ sau không nhằm thay nn.GRU, mà để quan sát các giá trị bên trong.

import torch
import torch.nn as nn

class SimpleGRUCell(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.hidden_size = hidden_size

        self.reset = nn.Linear(input_size + hidden_size, hidden_size)
        self.update = nn.Linear(input_size + hidden_size, hidden_size)
        self.candidate = nn.Linear(input_size + hidden_size, hidden_size)

    def forward(self, x_t, h_prev):
        joined = torch.cat([x_t, h_prev], dim=-1)

        r_t = torch.sigmoid(self.reset(joined))
        z_t = torch.sigmoid(self.update(joined))

        candidate_input = torch.cat([x_t, r_t * h_prev], dim=-1)
        h_tilde = torch.tanh(self.candidate(candidate_input))

        h_t = z_t * h_prev + (1 - z_t) * h_tilde
        return h_t, r_t, z_t, h_tilde


torch.manual_seed(7)
cell = SimpleGRUCell(input_size=3, hidden_size=4)

x_t = torch.tensor([[0.4, -0.2, 0.7]])
h_prev = torch.tensor([[0.5, 0.1, -0.3, 0.8]])

h_t, r_t, z_t, h_tilde = cell(x_t, h_prev)

print("reset gate    :", r_t.detach())
print("update gate   :", z_t.detach())
print("candidate     :", h_tilde.detach())
print("new state h_t :", h_t.detach())

Khi chạy, tập trung vào quan hệ:

h_prev -----> giữ theo z ------\
                              + ---> h_t
candidate ---> nhận theo 1-z -/

Không cần cố gán nghĩa cố định cho từng hidden dimension. Các chiều ẩn là biểu diễn do mạng tự học.

16. Unroll GRU qua nhiều bước

Một GRU layer chỉ lặp lại cùng một cell theo thời gian với cùng bộ tham số.

Các hộp GRU cell trên sơ đồ không phải ba bộ tham số khác nhau. Chúng là cùng một hàm được áp dụng ở các thời điểm khác nhau.

Đây vẫn là recurrent neural network, vì:

ht=f(xt,ht−1)h_t=f(x_t,h_{t-1})

và hidden state tiếp tục tạo đường phụ thuộc theo thời gian.

17. GRU vẫn được huấn luyện bằng BPTT

Có gate không làm thay đổi nguyên lý huấn luyện cơ bản.

Ta vẫn:

  1. unroll mô hình qua các bước;
  2. tính loss;
  3. backpropagate gradient qua computational graph theo thời gian;
  4. cập nhật các ma trận của reset gate, update gate và candidate.

Các gate được học bằng gradient descent như các tham số neural network khác. Ta không tự viết luật “nếu gặp dấu chấm thì reset” hay “nếu câu dài thì giữ”.

18. Reset gate có bắt buộc để GRU hoạt động tốt không?

Về mặt lịch sử, GRU gốc có cả reset và update gate. Nhưng các nghiên cứu về kiến trúc recurrent cho thấy có nhiều biến thể có thể hoạt động tốt, và không nên xem một cấu trúc là tối ưu tuyệt đối cho mọi nhiệm vụ.

Jozefowicz, Zaremba và Sutskever (2015) khảo sát hơn mười nghìn kiến trúc recurrent và tìm thấy các kiến trúc vượt LSTM/GRU trên một số nhiệm vụ nhưng không phải mọi nhiệm vụ.

Ý nghĩa sư phạm:

Các gate là một thiết kế học được để hỗ trợ dòng thông tin. Hiệu quả cuối cùng vẫn phải được kiểm chứng trên dữ liệu cụ thể.

19. Các lỗi hiểu thường gặp

Sai 1 - “Reset gate bằng 0 thì hidden state mới bằng 0”

Không đúng.

Reset gate chỉ tác động mạnh vào cách tạo candidate. Hidden state cuối còn phụ thuộc update gate:

ht=zt⊙ht−1+(1−zt)⊙h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t

Sai 2 - “Update gate gần 1 nghĩa là cập nhật nhiều”

Không đúng với quy ước công thức đang dùng ở đây.

Với:

ht=ztht−1+(1−zt)h~th_t=z_t h_{t-1}+(1-z_t)\tilde h_t

ztz_t gần 1 nghĩa là giữ cũ nhiều.

Sai 3 - “GRU không có cell state nên không học dài hạn”

Không đúng. Hidden state hth_t chính là trạng thái hồi tiếp mang thông tin. Update gate tạo đường giữ thông tin qua nhiều bước.

Sai 4 - “GRU luôn tốt hơn Vanilla RNN”

Không có đảm bảo như vậy cho mọi dữ liệu, mọi budget và mọi metric. Gated units thường thuận lợi hơn trên nhiệm vụ có phụ thuộc thời gian phức tạp, nhưng baseline đơn giản vẫn cần thiết.

Sai 5 - “GRU giải quyết xong exploding gradient”

Không đúng. Gradient clipping vẫn có thể cần thiết.

20. Kiểm tra nhanh

Câu 1

Với một chiều:

ht−1=0.6,h~t=−0.2,zt=0.9h_{t-1}=0.6,\quad \tilde h_t=-0.2,\quad z_t=0.9

Tính hth_t.

Đáp ánht=0.9(0.6)+0.1(−0.2)=0.54−0.02=0.52h_t=0.9(0.6)+0.1(-0.2)=0.54-0.02=0.52

Trạng thái mới vẫn gần trạng thái cũ vì update gate ưu tiên đường giữ.

Câu 2

Nếu rtr_t rất nhỏ nhưng ztz_t rất lớn, điều gì có thể xảy ra?

Gợi ý

Candidate mới ít dựa vào trạng thái cũ, nhưng hidden state cuối vẫn có thể giữ phần lớn ht−1h_{t-1} qua nhánh zt⊙ht−1z_t\odot h_{t-1}.

Câu 3

Vì sao không nên nói gradient của GRU qua một bước đơn giản bằng ztz_t?

Gợi ý

Vì các gate và candidate cũng phụ thuộc vào ht−1h_{t-1}, nên đạo hàm đầy đủ có thêm nhiều nhánh.

21. Bài tập

Bài 1 - theo dõi một cell

Cho:

ht−1=[0.8,−0.5]h_{t-1}=[0.8,-0.5] zt=[0.75,0.20]z_t=[0.75,0.20] h~t=[0.0,0.6]\tilde h_t=[0.0,0.6]

Tính hth_t và giải thích chiều nào giữ trạng thái cũ mạnh hơn.

Bài 2 - giải thích reset gate

Một sinh viên nói:

“Reset gate quyết định trực tiếp bao nhiêu phần trăm hidden state cũ được copy sang hidden state mới.”

Hãy chỉ ra vì sao câu này chưa chính xác và viết lại cho đúng.

Bài 3 - đọc công thức framework

Đối chiếu công thức candidate của paper Cho et al. (2014) với tài liệu torch.nn.GRU.

Trả lời:

  1. phép nhân với reset gate nằm trước hay sau recurrent linear transformation ở mỗi nguồn;
  2. PyTorch giải thích lý do thay đổi là gì;
  3. sự khác biệt này có làm thay đổi ý tưởng chung của reset gate không.

Bài 4 - tự visualize gate

Chạy SimpleGRUCell ở trên cho một chuỗi 10 bước. Lưu r_t và z_t, sau đó vẽ heatmap có:

  • trục ngang: timestep;
  • trục dọc: hidden dimension;
  • giá trị: gate activation.

Quan sát xem có hidden dimension nào thường giữ ztz_t cao hơn các chiều khác hay không.

22. Những điều cần nhớ

  1. GRU là gated RNN với một hidden state chính hth_t.
  2. Reset gate điều tiết quá khứ khi tạo candidate.
  3. Update gate nội suy giữa trạng thái cũ và candidate mới.
  4. Với công thức gốc đang dùng, ztz_t lớn nghĩa là giữ trạng thái cũ nhiều.
  5. GRU có ít nhóm affine hơn LSTM: ba nhóm so với bốn nhóm trong cấu hình cơ bản.
  6. Đường cập nhật cộng giúp thông tin cũ có thể được truyền qua nhiều bước thuận lợi hơn Vanilla RNN, nhưng không loại bỏ mọi vấn đề gradient.
  7. torch.nn.GRU có khác biệt nhỏ, có chủ đích, ở vị trí áp dụng reset gate so với công thức paper gốc.
  8. GRU vẫn được huấn luyện bằng BPTT và vẫn cần đánh giá, debug, masking và quản lý trạng thái như các RNN khác.

Tài liệu tham khảo

  1. Cho, K., van Merrienboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. EMNLP 2014, 1724-1734. https://aclanthology.org/D14-1179/
  2. Chung, J., Gulcehre, C., Cho, K., & Bengio, Y. (2014). Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling. NIPS 2014 Deep Learning and Representation Learning Workshop. https://arxiv.org/abs/1412.3555
  3. Jozefowicz, R., Zaremba, W., & Sutskever, I. (2015). An Empirical Exploration of Recurrent Network Architectures. ICML 2015, PMLR 37, 2342-2350. https://proceedings.mlr.press/v37/jozefowicz15.html
  4. Bengio, Y., Simard, P., & Frasconi, P. (1994). Learning Long-Term Dependencies with Gradient Descent Is Difficult. IEEE Transactions on Neural Networks, 5(2), 157-166. https://doi.org/10.1109/72.279181
  5. Pascanu, R., Mikolov, T., & Bengio, Y. (2013). On the difficulty of training recurrent neural networks. ICML 2013, PMLR 28(3), 1310-1318. https://proceedings.mlr.press/v28/pascanu13.html
  6. PyTorch documentation. torch.nn.GRU. https://docs.pytorch.org/docs/stable/generated/torch.nn.GRU.html