9.2. Bộ nhớ Ngắn hạn Dài (LSTM)

Thách thức đối với việc lưu trữ những thông tin dài hạn và bỏ qua đầu vào ngắn hạn trong các mô hình biến tiềm ẩn đã tồn tại trong một thời gian dài. Một trong những phương pháp tiếp cận sớm nhất để giải quyết vấn đề này là LSTM [Hochreiter & Schmidhuber, 1997]. Nó có nhiều tính chất tương tự Nút Hồi tiếp có Cổng (GRU). Điều thú vị là thiết kế của LSTM chỉ phức tạp hơn GRU một chút nhưng đã xuất hiện trước GRU gần hai thập kỷ.

Có thể cho rằng thiết kế này được lấy cảm hứng từ các cổng logic trong máy tính. Để kiểm soát một ô nhớ chúng ta cần một số các cổng. Một cổng để đọc các thông tin từ ô nhớ đó (trái với việc đọc từ các ô khác). Chúng ta sẽ gọi cổng này là cổng đầu ra (output gate). Một cổng thứ hai để quyết định khi nào cần ghi dữ liệu vào ô nhớ. Chúng ta gọi cổng này là cổng đầu vào (input gate). Cuối cùng, chúng ta cần một cơ chế để thiết lập lại nội dung chứa trong ô nhớ, được chi phối bởi một cổng quên (forget gate). Động lực của thiết kế trên cũng tương tự như trước đây, đó là để đưa ra quyết định khi nào cần nhớ và khi nào nên bỏ qua đầu vào trong trạng thái tiềm ẩn thông qua một cơ chế chuyên dụng. Chúng ta hãy xem thiết kế này hoạt động như thế nào trong thực tế.

9.2.1. Các Ô nhớ có Cổng

Ba cổng được giới thiệu trong LSTM đó là: cổng đầu vào, cổng quên và cổng đầu ra. Bên cạnh đó chúng ta sẽ giới thiệu một ô nhớ có kích thước giống với trạng thái ẩn. Nói đúng hơn đây chỉ là phiên bản đặc biệt của trạng thái ẩn, được thiết kế để ghi lại các thông tin bổ sung.

9.2.1.1. Cổng Đầu vào, Cổng Quên và Cổng Đầu ra

Tương tự như với GRU, dữ liệu được đưa vào các cổng LSTM là đầu vào ở bước thời gian hiện tại \(\mathbf{X}_t\) và trạng thái ẩn ở bước thời gian trước đó \(\mathbf{H}_{t-1}\). Những đầu vào này được xử lý bởi một tầng kết nối đầy đủ và một hàm kích hoạt sigmoid để tính toán các giá trị của các cổng đầu vào, cổng quên và cổng đầu ra. Kết quả là, tất cả các giá trị đầu ra tại ba cổng đều nằm trong khoảng \([0, 1]\). :numref:lstm_0 minh hoạ luồng dữ liệu cho các cổng đầu vào, cổng quên, và cổng đầu ra.

../_images/lstm_0.svg

Fig. 9.2.1 Các phép tính tại cổng đầu vào, cổng quên và cổng đầu ra trong một đơn vị LSTM.

Chúng ta giả sử rằng có \(h\) nút ẩn, mỗi minibatch có kích thước \(n\) và kích thước đầu vào là \(d\). Như vậy, đầu vào là \(\mathbf{X}_t \in \mathbb{R}^{n \times d}\) và trạng thái ẩn của bước thời gian trước đó là \(\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}\). Tương tự, các cổng được định nghĩa như sau: cổng đầu vào là \(\mathbf{I}_t \in \mathbb{R}^{n \times h}\), cổng quên là \(\mathbf{F}_t \in \mathbb{R}^{n \times h}\), và cổng đầu ra là \(\mathbf{O}_t \in \mathbb{R}^{n \times h}\). Chúng được tính như sau:

(9.2.1)\[\begin{split}\begin{aligned} \mathbf{I}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xi} + \mathbf{H}_{t-1} \mathbf{W}_{hi} + \mathbf{b}_i),\\ \mathbf{F}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xf} + \mathbf{H}_{t-1} \mathbf{W}_{hf} + \mathbf{b}_f),\\ \mathbf{O}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xo} + \mathbf{H}_{t-1} \mathbf{W}_{ho} + \mathbf{b}_o), \end{aligned}\end{split}\]

trong đó \(\mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h}\)\(\mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h}\) là các trọng số và \(\mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h}\) là các hệ số điều chỉnh.

9.2.1.2. Ô nhớ Tiềm năng

Tiếp theo, chúng ta sẽ thiết kế một ô nhớ. Vì ta vẫn chưa chỉ định tác động của các cổng khác nhau, nên đầu tiên ta sẽ giới thiệu ô nhớ tiềm năng \(\tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h}\). Các phép tính toán cũng tương tự như ba cổng mô tả ở trên, ngoài trừ việc ở đây ta sử dụng hàm kích hoạt \(\tanh\) với miền giá trị nằm trong khoảng \([-1, 1]\). Điều này dẫn đến phương trình sau tại bước thời gian \(t\).

(9.2.2)\[\tilde{\mathbf{C}}_t = \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c).\]

Ở đây \(\mathbf{W}_{xc} \in \mathbb{R}^{d \times h}\)\(\mathbf{W}_{hc} \in \mathbb{R}^{h \times h}\) là các tham số trọng số và \(\mathbf{b}_c \in \mathbb{R}^{1 \times h}\) là một hệ số điều chỉnh.

Ô nhớ tiềm năng được mô tả ngắn gọn trong Fig. 9.2.2.

../_images/lstm_1.svg

Fig. 9.2.2 Các phép tính toán trong ô nhớ tiềm năng của LSTM.

9.2.1.3. Ô nhớ

Trong GRU, chúng ta chỉ có một cơ chế duy nhất để quản lý cả việc nhớ và quên. Trong LSTM, chúng ta có hai tham số, \(\mathbf{I}_t\) điều chỉnh lượng dữ liệu mới được lấy vào thông qua \(\tilde{\mathbf{C}}_t\) và tham số quên \(\mathbf{F}_t\) chỉ định lượng thông tin cũ cần giữ lại trong ô nhớ \(\mathbf{C}_{t-1} \in \mathbb{R}^{n \times h}\). Sử dụng cùng một phép nhân theo từng điểm (pointwise) như trước đây, chúng ta đi đến phương trình cập nhật như sau.

(9.2.3)\[\mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t.\]

Nếu giá trị ở cổng quên luôn xấp xỉ bằng \(1\) và cổng đầu vào luôn xấp xỉ bằng \(0\), thì giá trị ô nhớ trong quá khứ \(\mathbf{C}_{t-1}\) sẽ được lưu lại qua thời gian và truyền tới bước thời gian hiện tại. Thiết kế này được giới thiệu nhằm giảm bớt vấn đề tiêu biến gradient cũng như nắm bắt các phụ thuộc dài hạn trong chuỗi thời gian tốt hơn. Do đó chúng ta có sơ đồ luồng trong Fig. 9.2.3.

../_images/lstm_2.svg

Fig. 9.2.3 Các phép tính toán trong ô nhớ của LSTM. Ở đây, ta sử dụng phép nhân theo từng phần tử.

9.2.1.4. Các Trạng thái Ẩn

Cuối cùng, chúng ta cần phải xác định cách tính trạng thái ẩn \(\mathbf{H}_t \in \mathbb{R}^{n \times h}\). Đây là nơi cổng đầu ra được sử dụng. Trong LSTM, đây chỉ đơn giản là một phiên bản có kiểm soát của hàm kích hoạt \(\tanh\) trong ô nhớ. Điều này đảm bảo rằng các giá trị của \(\mathbf{H}_t\) luôn nằm trong khoảng \((-1, 1)\). Bất cứ khi nào giá trị của cổng đầu ra là \(1\), thực chất chúng ta đang đưa toàn bộ thông tin trong ô nhớ tới bộ dự đoán. Ngược lại, khi giá trị của cổng đầu ra là \(0\), chúng ta giữ lại tất cả các thông tin trong ô nhớ và không xử lý gì thêm. Fig. 9.2.4 minh họa các luồng dữ liệu.

(9.2.4)\[\mathbf{H}_t = \mathbf{O}_t \odot \tanh(\mathbf{C}_t).\]
../_images/lstm_3.svg

Fig. 9.2.4 Các phép tính của trạng thái ẩn. Phép tính nhân được thực hiện trên từng phần tử.

9.2.2. Lập trình Từ đầu

Bây giờ ta sẽ lập trình một LSTM từ đầu. Giống như các thử nghiệm trong các phần trước, đầu tiên ta sẽ nạp tập dữ liệu The Time Machine.

from d2l import mxnet as d2l
from mxnet import np, npx
from mxnet.gluon import rnn
npx.set_np()

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

9.2.2.1. Khởi tạo Tham số Mô hình

Tiếp theo ta cần định nghĩa và khởi tạo các tham số mô hình. Cũng giống như trước đây, siêu tham số num_hiddens định nghĩa số lượng các nút ẩn. Ta sẽ khởi tạo các trọng số theo phân phối Gauss với độ lệch chuẩn bằng \(0.01\) và đặt các hệ số điều chỉnh bằng \(0\).

def get_lstm_params(vocab_size, num_hiddens, ctx):
    num_inputs = num_outputs = vocab_size

    def normal(shape):
        return np.random.normal(scale=0.01, size=shape, ctx=ctx)

    def three():
        return (normal((num_inputs, num_hiddens)),
                normal((num_hiddens, num_hiddens)),
                np.zeros(num_hiddens, ctx=ctx))

    W_xi, W_hi, b_i = three()  # Input gate parameters
    W_xf, W_hf, b_f = three()  # Forget gate parameters
    W_xo, W_ho, b_o = three()  # Output gate parameters
    W_xc, W_hc, b_c = three()  # Candidate cell parameters
    # Output layer parameters
    W_hq = normal((num_hiddens, num_outputs))
    b_q = np.zeros(num_outputs, ctx=ctx)
    # Attach gradients
    params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
              b_c, W_hq, b_q]
    for param in params:
        param.attach_grad()
    return params

9.2.2.2. Định nghĩa Mô hình

Trong hàm khởi tạo, trạng thái ẩn của LSTM cần trả về thêm một ô nhớ có giá trị bằng \(0\) và kích thước bằng (kích thước batch, số lượng các nút ẩn). Do đó ta có hàm khởi tạo trạng thái sau đây.

def init_lstm_state(batch_size, num_hiddens, ctx):
    return (np.zeros(shape=(batch_size, num_hiddens), ctx=ctx),
            np.zeros(shape=(batch_size, num_hiddens), ctx=ctx))

Mô hình thực sự được định nghĩa giống như những gì ta đã thảo luận trước đây: nó có ba cổng và một ô nhớ phụ. Lưu ý rằng chỉ có trạng thái ẩn được truyền tới tầng đầu ra. Các ô nhớ \(\mathbf{C}_t\) không tham gia trực tiếp vào việc tính toán đầu ra.

def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
     W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = npx.sigmoid(np.dot(X, W_xi) + np.dot(H, W_hi) + b_i)
        F = npx.sigmoid(np.dot(X, W_xf) + np.dot(H, W_hf) + b_f)
        O = npx.sigmoid(np.dot(X, W_xo) + np.dot(H, W_ho) + b_o)
        C_tilda = np.tanh(np.dot(X, W_xc) + np.dot(H, W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * np.tanh(C)
        Y = np.dot(H, W_hq) + b_q
        outputs.append(Y)
    return np.concatenate(outputs, axis=0), (H, C)

9.2.2.3. Huấn luyện và Dự đoán

Như đã từng làm trong Section 9.1, ta sẽ huấn luyện một LSTM bằng cách gọi hàm RNNModelScratch đã được giới thiệu ở Section 8.5.

vocab_size, num_hiddens, ctx = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, ctx, get_lstm_params,
                            init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, ctx)
perplexity 1.1, 10273.7 tokens/sec on gpu(0)
time traveller  it s against reason said filby  what reason said
traveller cond there wiln o do eare we save pock and means
../_images/output_lstm_vn_009064_9_1.svg

9.2.3. Lập trình Súc tích

Trong Gluon, ta có thể gọi trực tiếp lớp LSTM trong mô-đun rnn. Lớp này gói gọn tất cả các chi tiết cấu hình mà ta đã lập trình một cách chi tiết ở trên. Mã nguồn sẽ chạy nhanh hơn đáng kể vì nó sử dụng các toán tử đã được biên dịch thay vì các toán tử Python cho nhiều phép tính mà ta đã lập trình trước đây.

lstm_layer = rnn.LSTM(num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, ctx)
perplexity 1.1, 139732.2 tokens/sec on gpu(0)
time traveller for so it will be convenient to speak of him was
traveller the ear sion sowird we meally ho dry direction al
../_images/output_lstm_vn_009064_11_1.svg

Trong nhiều trường hợp, các mô hình LSTM hoạt động tốt hơn một chút so với các mô hình GRU nhưng việc huấn luyện và thực thi các mô hình này khá là tốn kém do chúng có kích thước trạng thái tiềm ẩn lớn hơn. LSTM là nguyên mẫu điển hình của một mô hình tự hồi quy biến tiềm ẩn có cơ chế kiểm soát trạng thái phức tạp. Nhiều biến thể đã được đề xuất qua từng năm, ví dụ như các kiến trúc đa tầng, các kết nối phần dư hay các kiểu điều chuẩn khác nhau. Tuy nhiên, việc huấn luyện LSTM và các mô hình chuỗi khác (như GRU) khá là tốn kém do sự phụ thuộc dài hạn của chuỗi. Sau này ta có thể sử dụng các mô hình khác như Transformer để song song hoá việc huấn luyện chuỗi.

9.2.4. Tóm tắt

  • LSTM có ba loại cổng để kiểm soát luồng thông tin: cổng đầu vào, cổng quên và cổng đầu ra.
  • Đầu ra tầng ẩn của LSTM bao gồm các trạng thái ẩn và các ô nhớ. Chỉ các trạng thái ẩn là được truyền tới tầng đầu ra. Các ô nhớ hoàn toàn được sử dụng nội bộ trong tầng.
  • LSTM có thể đối phó với vấn đề tiêu biến và bùng nổ gradient.

9.2.5. Bài tập

  1. Thay đổi các siêu tham số. Quan sát và phân tích tác động đến thời gian chạy, perplexity và đầu ra.
  2. Cần thay đổi mô hình như thế nào để sinh ra các từ hoàn chỉnh thay vì các chuỗi ký tự?
  3. So sánh chi phí tính toán của GRU, LSTM và RNN thông thường với cùng một chiều ẩn. Đặc biệt chú ý đến chi phí huấn luyện và dự đoán.
  4. Dù các ô nhớ tiềm năng đã đảm bảo rằng phạm vi giá trị nằm trong khoảng từ \(-1\) đến \(1\) bằng cách sử dụng hàm \(\tanh\), tại sao trạng thái ẩn vẫn phải sử dụng tiếp hàm \(\tanh\) để đảm bảo rằng phạm vi giá trị đầu ra nằm trong khoảng từ \(-1\) đến \(1\)?
  5. Lập trình một mô hình LSTM để dự đoán chuỗi thời gian thay vì dự đoán chuỗi ký tự.

9.2.6. Thảo luận

9.2.7. Những người thực hiện

Bản dịch trong trang này được thực hiện bởi:

  • Đoàn Võ Duy Thanh
  • Nguyễn Văn Quang
  • Nguyễn Lê Quang Nhật
  • Nguyễn Văn Cường
  • Lê Khắc Hồng Phúc
  • Nguyễn Duy Du
  • Phạm Minh Đức
  • Phạm Hồng Vinh