Chuyển đến nội dung chính

Mô hình LSTM: Hướng dẫn đầy đủ về mạng Bộ nhớ Ngắn-Dài hạn

Nắm vững cơ chế nội tại của mạng LSTM, nền tảng của các LLM hiện đại. Khám phá cơ chế cổng, gradient, và xây dựng bộ phân loại cảm xúc với PyTorch.
· 15 phút đọc

Khám phá cùng AI

Các mạng nơ-ron hồi quy tiêu chuẩn gặp khó khăn với trí nhớ. Khi chuỗis vượt quá 20-30 bước thời gian, gradient hoặc thu nhỏ về 0 hoặc bùng nổ trong quá trình lan truyền ngược, và mạng mất khả năng kết nối các phần xa nhau của đầu vào. Hochreiter và Schmidhuber đã giải quyết vấn đề này trong bài báo năm 1997 bằng cách giới thiệu mạng Bộ nhớ Ngắn-Dài hạn (LSTM).

Trong hướng dẫn này, tôi sẽ đi qua phần nội tại của LSTM trước khi chuyển sang triển khai thực tế bằng Python. Các phần cuối sẽ so sánh LSTM với Transformer để bạn có thể chọn kiến trúc phù hợp cho trường hợp sử dụng của mình.

Tôi rất khuyến nghị khóa học Giới thiệu về Học sâu với PyTorch của chúng tôi để có trải nghiệm thực hành sử dụng PyTorch để huấn luyện mạng nơ-ron, vốn là nền tảng cho các mô hình LSTM.

Mô hình LSTM là gì?

LSTM là viết tắt của Long Short-Term Memory (Bộ nhớ Ngắn-Dài hạn), một loại mạng nơ-ron hồi quy (RNN) được thiết kế để xử lý các chuỗi mà ngữ cảnh từ rất sớm trong đầu vào vẫn còn quan trọng.

Ý tưởng cốt lõi đằng sau LSTM là cơ chế cổng (gating) điều tiết luồng thông tin qua mạng. Thay vì để mọi tín hiệu đi qua như nhau, các cổng học khi nào cần giữ ngữ cảnh và khi nào nên bỏ qua. 

Bộ nhớ có chọn lọc này cho phép LSTM theo dõi các phụ thuộc qua hàng trăm bước thời gian, điều này tỏ ra hữu ích cho mô hình ngôn ngữ, nhận dạng giọng nói và dự báo chuỗi thời gian. Để hiểu vì sao kiến trúc này tồn tại, sẽ hữu ích khi xem điều gì bị hỏng ở RNN tiêu chuẩn.

Vấn đề gradient biến mất trong RNN

Mạng hồi quy truyền thông tin từ bước thời gian này sang bước tiếp theo thông qua trạng thái ẩn. Mỗi bước nhân trạng thái này với trọng số học được, cập nhật nó, rồi truyền tiếp. Huấn luyện yêu cầu gửi tín hiệu lỗi ngược qua tất cả các bước đó, và mỗi lần lan truyền ngược làm tín hiệu nhỏ đi một chút. Sau đủ nhiều bước, gần như không còn gì.

Nó giống như photocopy một bản photocopy. Làm vài lần thì bản sao vẫn ổn. Làm năm mươi lần, bạn hầu như không đọc được. RNN gặp bức tường này ở khoảng 20-30 bước thời gian, đó là lý do chúng khó xử lý các chuỗi dài hơn.

Cách LSTM giải quyết các phụ thuộc dài hạn

LSTM thêm một đường bộ nhớ riêng, gọi là trạng thái ô (cell state), chạy song song với trạng thái ẩn tiêu chuẩn. Khác biệt nằm ở cấu trúc: thông tin trong trạng thái ô chảy qua phép cộng thay vì nhân lặp đi lặp lại, vì vậy nó không bị suy giảm theo cách cũ.

Để kiểm soát những gì đi vào và đi ra khỏi bộ nhớ này, LSTM sử dụng các cổng. Mỗi cổng là một mạng nơ-ron nhỏ học cách xuất ra giá trị trong khoảng 0 đến 1, hoạt động như một bộ lọc. Giá trị gần 0 chặn thông tin; giá trị gần 1 cho phép thông tin đi qua. Mạng học trong quá trình huấn luyện xem các mẫu nào đáng nhớ qua chuỗi dài và mẫu nào có thể bỏ qua.

Cơ chế cổng này là điều tách biệt LSTM với RNN nguyên bản. Thay vì bắt buộc mọi thông tin đi cùng một đường, LSTM có thể chọn lọc giữ ngữ cảnh qua hàng trăm bước. Phần tiếp theo sẽ phân tích chính xác cách các cổng này hoạt động và cách chúng kết hợp để cập nhật trạng thái ô.

Các trường hợp sử dụng mô hình LSTM

LSTM xuất hiện ở bất cứ nơi nào dữ liệu tuần tự quan trọng. Một vài ví dụ vượt ra ngoài các ứng dụng NLP và dự báo thường thấy:

  • Ngân hàng chạy chuỗi giao dịch qua LSTM để bắt các mẫu gian lận mà hệ thống dựa trên luật bỏ sót
  • Bệnh viện giám sát luồng ECG với LSTM hai chiều, phát hiện bất thường tim nhanh hơn so với cảnh báo ngưỡng
  • Nền tảng đô thị thông minh dự đoán giao thông và nhu cầu năng lượng từ dữ liệu cảm biến, điều chỉnh tài nguyên trước khi xảy ra đột biến
  • Nhà máy theo dõi độ rung thiết bị theo thời gian để lên lịch bảo trì trước khi sự cố xảy ra

Kiến trúc LSTM: Các cổng và ô nhớ

Phần này trình bày chi tiết nội tại của LSTM. Nếu bạn muốn nhảy thẳng đến triển khai, hãy chuyển sang phần tiếp theo. Chi tiết kiến trúc ở đây không bắt buộc để sử dụng LSTM, nhưng hữu ích khi gỡ lỗi hoặc tinh chỉnh.

Để hiểu LSTM hoạt động thế nào, chúng ta sẽ truy vết một ví dụ cụ thể: xử lý chuỗi "The clouds are dark..." để dự đoán từ tiếp theo. Mỗi thành phần sẽ dễ hiểu hơn khi bạn thấy nó làm gì với dữ liệu thực.

Trạng thái ô và trạng thái ẩn trong LSTM

Một LSTM duy trì hai vector riêng biệt chảy xuyên suốt mạng: trạng thái ô và trạng thái ẩn.

Trạng thái ô đóng vai trò lưu trữ dài hạn. Nó mang thông tin qua các bước thời gian với các điều chỉnh có kiểm soát ở mỗi bước. Khi xử lý chuỗi của chúng ta, trạng thái ô có thể lưu "xuất hiện danh từ liên quan đến thời tiết" sau khi thấy "clouds" và giữ ngữ cảnh đó qua "are", cho đến khi "dark" củng cố mẫu này.

Trạng thái ẩn là đầu ra của ô tại mỗi bước thời gian. Nó đi vào dự đoán và vào phép tính của bước tiếp theo. Sau khi xử lý "dark," trạng thái ẩn mã hóa điều gì đó như "tính từ mô tả điều kiện thời tiết" mà một tầng dự đoán có thể sử dụng.

LSTM Cell

Vì sao có hai đường riêng? Một trạng thái đơn lẻ sẽ cần đồng thời giữ ngữ cảnh dài hạn và tạo ra đầu ra hữu ích. Hai mục tiêu này mâu thuẫn: bộ nhớ ổn định cần thay đổi tối thiểu, trong khi đầu ra hữu ích cần biến đổi. Tách chúng ra cho phép mỗi đường tối ưu cho mục đích riêng.

Ở mỗi bước thời gian, ô nhận ba đầu vào và tạo ra hai đầu ra:

Đầu vào:

  • Trạng thái ô trước đó (C_{t-1}): những gì mạng đã nhớ đến hiện tại

  • Trạng thái ẩn trước đó (h_{t-1}): đầu ra của bước trước

  • Đầu vào hiện tại (x_t): token mới, như "clouds" hoặc "dark"

Đầu ra:

  • Trạng thái ô đã cập nhật (C_t): bộ nhớ sau khi xử lý token này

  • Trạng thái ẩn mới (h_t): đầu ra của bước thời gian này

Cách các cổng của LSTM tính toán

Bốn thành phần kiểm soát những gì xảy ra bên trong ô. Ba trong số đó là các cổng xuất giá trị trong khoảng 0 đến 1, hoạt động như bộ lọc. Thành phần thứ tư là ứng viên (candidate), đề xuất nội dung mới.

Khi "are" đến sau "clouds," mỗi thành phần có một nhiệm vụ:

  • Cổng quên (f_t): Chúng ta có nên giữ ngữ cảnh "clouds" không? Danh từ thời tiết theo sau động từ nối thường quan trọng, nên cổng quên xuất các giá trị gần 1 để giữ phần lớn trạng thái ô.

  • Cổng vào (i_t): "are" nên đóng góp vào bộ nhớ bao nhiêu? Động từ nối mang tín hiệu dự đoán thấp hơn, nên cổng vào xuất giá trị thấp.

  • Ứng viên (C̃_t): "are" có thể thêm thông tin gì? Ứng viên đề xuất nội dung, nhưng cổng vào sẽ giảm cường độ nó.

  • Cổng ra (o_t): Trạng thái ẩn nên hiển thị gì? Cổng ra lọc trạng thái ô để tạo h_t.

How LSTM gates compute

Mỗi thành phần tính đầu ra theo cùng một cách:

  1. Nối h_{t-1} và x_t thành một vector

  2. Nhân với một ma trận trọng số học được

  3. Cộng một hằng số độ lệch (bias)

  4. Áp dụng hàm kích hoạt

Các cổng dùng kích hoạt sigmoid, nén giá trị vào khoảng 0-1. Ứng viên dùng tanh, xuất ra các giá trị giữa -1 và 1, nên có thể đề xuất cả tăng và giảm cho trạng thái ô.

Các phương trình

Cổng quên (sigmoid → 0 đến 1):

Cổng vào (sigmoid → 0 đến 1):

Cổng ra (sigmoid → 0 đến 1):

Ứng viên (tanh → -1 đến +1):

Mỗi thành phần có ma trận trọng số riêng (W_f, W_i, W_c, W_o), nên chúng học phản hồi với các mẫu khác nhau trong đầu vào.

Cách hoạt động của lọc

Đầu ra của cổng nhân với các vector theo từng vị trí (nhân từng phần tử, ký hiệu ⊙). Mỗi chiều được lọc độc lập.

Nếu một cổng xuất 0.9 cho vị trí 1, chiều đó giữ 90% giá trị. Nếu nó xuất 0.0 cho vị trí 4, chiều đó bị xóa. Khi xử lý "dark" sau "clouds are," cổng quên có thể xuất giá trị cao cho các chiều lưu ngữ cảnh danh từ/thời tiết và giá trị thấp hơn cho các chiều chứa ngữ cảnh trước đó không liên quan.

Dòng chảy thông tin giữa các cổng quên, cộng và ra

Các thành phần kết hợp trong ba bước để cập nhật trạng thái ô và tạo ra trạng thái ẩn.

Bước 1: Quên

Cổng quên lọc trạng thái ô trước đó:

Khi xử lý "dark," cổng quên giữ ngữ cảnh thời tiết từ "clouds" đồng thời xóa các chiều theo dõi mạo từ "The."

Bước 2: Cộng

Cổng vào co giãn đề xuất của ứng viên, rồi cộng nó vào trạng thái ô đã được lọc:

Ứng viên có thể đề xuất "tính từ bổ nghĩa danh từ thời tiết," và cổng vào quyết định mức độ ghi điều đó vào bộ nhớ. Cập nhật dạng cộng (không phải nhân) này giúp gradient chảy tốt hơn trong huấn luyện.

Bước 3: Ra

Trạng thái ô đi qua tanh, sau đó cổng ra lọc phần trở thành trạng thái ẩn:

Hàm tanh giữ giá trị trong giới hạn. Cổng ra chọn những khía cạnh nào của bộ nhớ tích lũy sẽ hiển thị để dự đoán ở bước thời gian này.

Trình tự hoàn chỉnh:

Dưới đây là một truy vết giản lược với trạng thái ô có 4 chiều:

Bước

Đầu vào

Trạng thái ô (giản lược)

Điều xảy ra

1

"The"

[0.1, 0.0, 0.2, 0.0]

Lưu trữ tối thiểu; mạo từ mang ít tín hiệu dự đoán

2

"clouds"

[0.1, 0.7, 0.2, 0.3]

Cổng vào mở cho các chiều 2,4; lưu mẫu danh từ

3

"are"

[0.1, 0.6, 0.2, 0.3]

Cổng quên ≈ 0.9 giữ ngữ cảnh; động từ nối thêm rất ít

4

"dark"

[0.1, 0.8, 0.5, 0.6]

Tính từ làm mạnh tín hiệu ở các chiều 2,3,4

Đến bước thời gian 4, các chiều 2-4 đã tích lũy được một mẫu. Trạng thái ẩn h_4 đi vào một tầng dự đoán (không phải phần của LSTM), tầng này sinh ra xác suất từ vựng. Mẫu tích lũy đó giúp mô hình ưu tiên các từ như "rain" hoặc "sky."

Mạng không hiểu về thời tiết. Nó học các tương quan thống kê: một số chuỗi đầu vào có xu hướng đi trước một số đầu ra nhất định.

Dòng chảy gradient trong huấn luyện

Cấu trúc cộng của trạng thái ô cung cấp một đường dẫn gradient trực tiếp hơn so với RNN tiêu chuẩn. Nếu bạn chưa quen với cách gradient lan truyền qua mạng nơ-ron, hướng dẫn lan truyền ngược bao gồm các nền tảng cơ bản.

Trong RNN tiêu chuẩn, gradient chảy qua ma trận trọng số ở mỗi bước. Phép nhân lặp lại cộng dồn: sau 100 bước với trọng số trung bình 0.9, gradient thu nhỏ còn 0.9^100 ≈ 0.00003.

LSTM cung cấp một đường dẫn thay thế dọc theo trạng thái ô. Đạo hàm của C_t theo C_{t-1}:

∂C_t/∂C_{t-1} = f_t

Đây là giá trị cổng quên, không phải ma trận trọng số. Khi cổng quên duy trì gần 1, gradient đi qua với ít suy giảm hơn. Nếu cổng trung bình 0.95, sau 100 bước: 0.95^100 ≈ 0.006. Vẫn giảm, nhưng có thể sử dụng.

Khía cạnh

RNN

LSTM

Đường gradient

Chỉ qua ma trận trọng số

Đường bổ sung dọc theo trạng thái ô

Gradient 100 bước

~10^-5 (biến mất)

~10^-2 đến 10^-1 (dùng được)

Bộ nhớ thực tế

~10-20 bước thời gian

~50-200 bước thời gian

Ghi chú thực tiễn: Bias của cổng quên thường được khởi tạo ở 1.0 hoặc cao hơn, đẩy đầu ra ban đầu về phía "ghi nhớ mọi thứ." Điều này giúp mạng học các mẫu dài hạn sớm trong quá trình huấn luyện.

LSTM giảm thiểu vấn đề gradient biến mất chứ không loại bỏ hoàn toàn. Cải thiện đến từ cập nhật trạng thái ô dạng cộng, cung cấp một "đường cao tốc" cho gradient song song với các đường dẫn tiêu chuẩn.

Cách triển khai mô hình LSTM trong Python

Phần trước đã trình bày những gì diễn ra trong một ô LSTM: các cổng lọc thông tin, trạng thái ô giữ ngữ cảnh, và trạng thái ẩn tạo ra đầu ra. Giờ chúng ta sẽ xem nn.LSTM của PyTorch gói gọn tất cả vào một mô-đun đơn và dùng nó để phân loại đánh giá phim là tích cực hay tiêu cực.

Mẹo để chạy mã: Huấn luyện 50 epoch trên CPU sẽ rất lâu. Hãy dùng Google Colab hoặc Kaggle Notebooks với tăng tốc GPU để có kết quả nhanh hơn. Toàn bộ script có sẵn dưới dạng GitHub Gist.

Xây dựng bộ phân loại LSTM với PyTorch

Phân loại văn bản với LSTM theo một khuôn mẫu: chuyển từ thành vector, để LSTM xử lý tuần tự, sau đó dùng đầu ra cuối cùng cho dự đoán.

Nhúng từ thành vector

Mạng nơ-ron hoạt động trên các số liên tục, còn từ là ký hiệu rời rạc. Ta không thể đưa trực tiếp "terrible" vào phép nhân ma trận. Giải pháp là một tầng embedding: một bảng truy xuất nơi mỗi từ ánh xạ tới một vector có thể học được.

self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=padding_idx)

Nếu bạn chọn embedding_dim=32, mỗi từ trở thành một vector 32 chiều. Ở đầu quá trình huấn luyện, các vector này là ngẫu nhiên. Khi mô hình học, các từ có sắc thái cảm xúc tương tự sẽ xích lại gần nhau trong không gian vector này. "Terrible," "awful," và "boring" có thể ở gần nhau, trong khi "excellent" và "amazing" tụ lại nơi khác.

Tham số padding_idx báo cho tầng giữ các token đệm ở vector 0, để chúng không ảnh hưởng đến mô hình.

Định nghĩa kiến trúc LSTM

self.lstm = nn.LSTM(
	input_size=embedding_dim,
	hidden_size=hidden_size,
	num_layers=num_layers,
	batch_first=True,
	dropout=dropout if num_layers > 1 else 0,
	bidirectional=bidirectional,
)

Chỉ một dòng này tạo ra toàn bộ cơ chế cổng từ phần trước. hidden_size quyết định lượng thông tin trạng thái ô và trạng thái ẩn có thể mang. Giá trị lớn hơn cho phép mô hình theo dõi nhiều mẫu hơn nhưng tốn tính toán hơn.

Thiết lập batch_first=True nghĩa là đầu vào có dạng (batch, sequence_length, embedding_dim). LSTM xử lý từng vị trí từ trái sang phải, cập nhật trạng thái nội tại ở mỗi bước.

Xử lý chuỗi

Sau khi xử lý "This movie was absolutely terrible," LSTM đã thấy mọi từ. Trạng thái ẩn cuối cùng đã tích lũy những gì mô hình học được từ chuỗi đó. Với cảm xúc, điều này có thể mã hóa điều gì đó như "tính từ tiêu cực mạnh ở cuối, phủ định nội dung trung tính trước đó."

def forward(self, x):
	embedded = self.embedding(x)
	lstm_out, (h_n, c_n) = self.lstm(embedded)
	h_final = h_n[-1]
	return self.fc(h_final)

LSTM trả về cả đầu ra theo từng bước (lstm_out) và trạng thái cuối cùng (h_n, c_n). Với phân loại, ta chỉ cần h_n[-1], trạng thái ẩn sau token cuối. Một tầng tuyến tính sau đó ánh xạ nó thành điểm lớp.

Truy vết một đánh giá qua mô hình

Khi "This movie was terrible" đi vào bộ phân loại:

  1. Mỗi từ nhận embedding của nó: bốn vector dạng (32,)

  2. LSTM xử lý chúng tuần tự. "This" khởi tạo một phần trạng thái ô. "Movie" cập nhật nó một chút. "Was" trung tính và thay đổi ít. "Terrible" kích hoạt cổng quên để xóa ngữ cảnh không liên quan và cổng vào để ghi tín hiệu tiêu cực mạnh.

  3. Trạng thái ẩn cuối cùng nắm bắt mẫu tích lũy.

  4. Tầng tuyến tính ánh xạ nó thành hai điểm: một cho tích cực, một cho tiêu cực

Mô hình không hiểu ngôn ngữ. Nó học các mẫu thống kê: một số chuỗi từ tương quan với một số nhãn nhất định.

Lớp mô hình đầy đủ

Bên dưới là toàn bộ lớp LSTMClassifier mà chúng ta vừa tạo từng bước:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from datasets import load_dataset
from collections import Counter
import numpy as np
 
torch.manual_seed(42)
np.random.seed(42)
 
 
class LSTMClassifier(nn.Module):
    def __init__(
        Self,
        Vocab_size,
        Embedding_dim,
        Hidden_size,
        Output_size,
        num_layers=1,
        dropout=0.0,
        bidirectional=False,
        padding_idx=0,
    ):
        super().__init__()
        self.hidden_size = hidden_size
    	self.num_layers = num_layers
    	self.bidirectional = bidirectional
 
    	self.embedding = nn.Embedding(
	    vocab_size, embedding_dim, padding_idx=padding_idx
        )
    	self.lstm = nn.LSTM(
            input_size=embedding_dim,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            dropout=dropout if num_layers > 1 else 0,
            bidirectional=bidirectional,
    	)	
 
    	fc_input_size = hidden_size * 2 if bidirectional else hidden_size
    	self.fc = nn.Linear(fc_input_size, output_size)
 
    def forward(self, x):
    	embedded = self.embedding(x)
    	lstm_out, (h_n, c_n) = self.lstm(embedded)
 
    	if self.bidirectional:
            h_final = torch.cat([h_n[-2], h_n[-1]], dim=1)
    	else:
            h_final = h_n[-1]
 
    	return self.fc(h_final)

Tùy chọn hai chiều chạy hai LSTM: một từ trái sang phải, một từ phải sang trái. Trạng thái ẩn cuối của chúng được nối lại, cung cấp cho mô hình ngữ cảnh từ cả hai hướng. Điều này hữu ích khi ý nghĩa phụ thuộc vào những gì đến sau, như "not bad," nơi "not" chỉ có ý nghĩa khi có "bad."

Chuẩn bị văn bản cho mô hình

LSTM cần các chuỗi số nguyên, không phải chuỗi ký tự. Tiền xử lý chuyển văn bản thô qua ba bước: 

  1. Xây dựng từ vựng ánh xạ từ sang chỉ số
  2. Chuyển mỗi đánh giá thành một chuỗi chỉ số
  3. Đệm chuỗi về độ dài đồng nhất
def build_vocab(texts, max_vocab_size=10000):
	counter = Counter()
	for text in texts:
    		counter.update(text.lower().split())
 
	vocab = {"<pad>": 0, "<unk>": 1}
	for word, _ in counter.most_common(max_vocab_size - 2):
    		vocab[word] = len(vocab)
	return vocab

Chúng ta dành chỉ số 0 cho đệm và chỉ số 1 cho từ không biết. Giới hạn kích thước từ vựng giúp lọc các từ hiếm không có đủ ví dụ để học embedding có ý nghĩa.

def tokenize_and_pad(texts, vocab, max_length=200):
	sequences = []
	for text in texts:
    		tokens = text.lower().split()
    		ids = [vocab.get(t, vocab["<unk>"]) for t in tokens[:max_length]]
    		ids = ids + [vocab["<pad>"]] * (max_length - len(ids))
    		sequences.append(ids)
	return torch.tensor(sequences, dtype=torch.long)

Cắt ngắn ở max_length giới hạn mức độ LSTM phải nhớ ngược về sau. Chuỗi dài hơn giữ nhiều ngữ cảnh hơn nhưng tăng chi phí tính toán và bộ nhớ. Với đánh giá phim, 100-200 token thường nắm bắt được các tín hiệu cảm xúc chính.

Đệm đảm bảo mọi chuỗi có cùng độ dài, cho phép PyTorch gom chúng thành một tensor duy nhất. Các token đệm trở thành vector 0 sau khi embedding và hầu như không đóng góp vào trạng thái cuối.

def load_imdb_data(max_samples=10000, max_length=200, max_vocab=10000):
	dataset = load_dataset("imdb")
 
	train_data = dataset["train"].shuffle(seed=42)
	test_data = dataset["test"].shuffle(seed=42)
 
	train_texts = train_data["text"][:max_samples]
	train_labels = train_data["label"][:max_samples]
	test_texts = test_data["text"][: max_samples // 4]
	test_labels = test_data["label"][: max_samples // 4]
 
	vocab = build_vocab(train_texts, max_vocab)
 
	X_train = tokenize_and_pad(train_texts, vocab, max_length)
	X_test = tokenize_and_pad(test_texts, vocab, max_length)
	y_train = torch.tensor(train_labels, dtype=torch.long)
	y_test = torch.tensor(test_labels, dtype=torch.long)
 
	return X_train, y_train, X_test, y_test, vocab

Xây dựng từ vựng chỉ từ dữ liệu huấn luyện giúp ngăn rò rỉ thông tin. Nếu một từ hiếm chỉ xuất hiện trong đánh giá kiểm tra, mô hình nên coi nó là không biết thay vì đã học embedding của nó trong huấn luyện.

Huấn luyện LSTM và siêu tham số

Vòng lặp huấn luyện cập nhật trọng số mô hình dựa trên độ sai khác giữa dự đoán và giá trị đúng. Với LSTM, một chi tiết quan trọng hơn thường lệ: cắt gradient.

def train_model(model, train_loader, test_loader, n_epochs=5, lr=0.001):
	criterion = nn.CrossEntropyLoss()
	optimizer = torch.optim.Adam(model.parameters(), lr=lr)
 
	for epoch in range(n_epochs):
    		model.train()
    		total_loss, correct, total = 0, 0, 0
 
    		for batch_texts, batch_labels in train_loader:
        		optimizer.zero_grad()
        		outputs = model(batch_texts)
        		loss = criterion(outputs, batch_labels)
        		loss.backward()
            		torch.nn.utils.clip_grad_norm_(
	model.parameters(), 
	max_norm=5.0)
        		optimizer.step()
 
        		total_loss += loss.item()
        		_, predicted = outputs.max(1)
        		correct += (predicted == batch_labels).sum().item()
        		total += batch_labels.size(0)

Vì sao cắt gradient quan trọng với LSTM

Trước đó, chúng ta đã giải thích cách LSTM giảm gradient biến mất qua cập nhật cộng vào trạng thái ô. Nhưng gradient bùng nổ vẫn là rủi ro. Khi gradient quá lớn, cập nhật trọng số sẽ vượt quá và làm bất ổn huấn luyện.

clip_grad_norm_ giới hạn tổng độ lớn gradient. Nếu gradient vượt max_norm=5.0, chúng sẽ được co giãn xuống tương ứng. Điều này giữ cập nhật ổn định mà không loại bỏ hoàn toàn tín hiệu gradient.

    	model.eval()
    	test_correct, test_total = 0, 0
    	with torch.no_grad():
        	for batch_texts, batch_labels in test_loader:
            		outputs = model(batch_texts)
            		_, predicted = outputs.max(1)
            		test_correct += (predicted == batch_labels).sum().item()
            		test_total += batch_labels.size(0)
 
    	test_acc = test_correct / test_total
    	print(f"Epoch {epoch+1}: Loss={total_loss/len(train_loader):.4f}, "
          	f"Train={correct/total:.3f}, Test={test_acc:.3f}")
 
    return test_acc

model.eval() tắt dropout trong đánh giá. torch.no_grad() bỏ qua tính toán gradient vì chúng ta không cập nhật trọng số, giúp tiết kiệm bộ nhớ và tăng tốc suy luận.

Chọn siêu tham số

X_train, y_train, X_test, y_test, vocab = load_imdb_data(
	max_samples=10000, max_length=100, max_vocab=5000
)
 
train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=64, shuffle=True)
test_loader = DataLoader(TensorDataset(X_test, y_test), batch_size=64)
 
model = LSTMClassifier(
	vocab_size=len(vocab),
	embedding_dim=32,
	hidden_size=32,
	output_size=2,
	padding_idx=vocab["<pad>"],
)
 
final_acc = train_model(model, train_loader, test_loader, n_epochs=50, lr=0.001)

Các giá trị này phản ánh đánh đổi:

  • hidden_size=32: Đủ nhỏ để lặp nhanh. Mô hình sản xuất thường dùng 128-512, nhưng khi kích thước tăng, lợi ích giảm dần và huấn luyện chậm lại.

  • embedding_dim=32: Khớp với kích thước ẩn là phổ biến nhưng không bắt buộc. Embedding lớn hơn nắm bắt phân biệt từ tốt hơn với chi phí nhiều tham số hơn.

  • max_length=100: Phần lớn tín hiệu cảm xúc xuất hiện sớm trong đánh giá. Chuỗi dài hơn giúp các trường hợp tinh tế, nhưng bộ nhớ tăng gấp bốn khi max_length tăng gấp đôi.

  • batch_size=64: Lô lớn hơn cho ước lượng gradient ổn định hơn. Lô nhỏ hơn có thể giúp thoát cực tiểu cục bộ. 32-128 là điển hình.

  • lr=0.001: Mặc định của Adam hiệu quả cho hầu hết tác vụ LSTM. Nếu loss dao động, thử 0.0001. Nếu học chậm, thử 0.01.

  • n_epochs=50: Đủ để mô hình hội tụ. Theo dõi khi độ chính xác kiểm tra đi ngang hoặc giảm trong khi độ chính xác huấn luyện vẫn tăng, báo hiệu quá khớp.

Mô hình học được gì

Chạy cấu hình này cho ra đầu ra như:

...
Epoch 46: Loss=0.4126, Train=0.812, Test=0.789
Epoch 47: Loss=0.4054, Train=0.815, Test=0.791
Epoch 48: Loss=0.3998, Train=0.818, Test=0.788
Epoch 49: Loss=0.3921, Train=0.821, Test=0.793
Epoch 50: Loss=0.3867, Train=0.824, Test=0.795

Độ chính xác huấn luyện đạt khoảng 82% trong khi độ chính xác kiểm tra ổn định gần 80%. Mô hình học các mẫu từ dự đoán cảm xúc. Các tính từ mạnh gần cuối có trọng lượng. Phủ định như "not" đảo chiều sắc thái. Một số cụm từ ("waste of time," "highly recommend") trở thành tín hiệu mạnh. Không có gì trong số này được lập trình; nó xuất hiện từ tương quan trong dữ liệu huấn luyện.

Với tinh chỉnh tối thiểu và mô hình nhỏ, độ chính xác 80% là hợp lý. Khoảng cách nhỏ giữa huấn luyện và kiểm tra cho thấy mô hình khái quát tốt mà không quá khớp nghiêm trọng. Thêm dropout, dùng embedding tiền huấn luyện như GloVe, hoặc huấn luyện trên nhiều dữ liệu hơn có thể đẩy độ chính xác lên mức cuối 80%.

Cải thiện hiệu năng mô hình LSTM

Kiến trúc LSTM cơ bản ở phần trước hoạt động tốt, nhưng một số kỹ thuật có thể đẩy độ chính xác cao hơn mà không cần chuyển sang họ mô hình khác. Các cách tiếp cận này đã được kiểm chứng qua hàng nghìn triển khai sản xuất và bài báo nghiên cứu.

LSTM hai chiều

LSTM tiêu chuẩn xử lý chuỗi từ trái sang phải, chỉ xây dựng ngữ cảnh từ quá khứ. LSTM hai chiều (BiLSTM) chạy hai lượt song song: một tiến, một lùi. Biểu diễn cuối cùng tại mỗi bước kết hợp cả những gì đến trước và sau.

self.lstm = nn.LSTM(
	input_size=embedding_dim,
	hidden_size=hidden_size,
	bidirectional=True,  # Run both directions
	batch_first=True,
)
# Output size doubles: hidden_size * 2

Với phân tích cảm xúc, điều này quan trọng khi phủ định xuất hiện sau từ mà nó bổ nghĩa: "the movie was not bad" cần ngữ cảnh tiến để biết có "bad," và ngữ cảnh lùi để biết "not" đứng trước nó. 

BiLSTM thường vượt trội hơn mô hình một chiều trong nhận dạng thực thể có tên (NER), phân tích cảm xúc và nhận dạng giọng nói, với mức cải thiện điển hình 5-15%.

Đánh đổi: bạn cần toàn bộ chuỗi khi suy luận. Ứng dụng streaming nhận token từng cái một không thể dùng xử lý hai chiều.

Thêm attention

Attention cho phép mô hình gán trọng số cho các bước thời gian quan trọng nhất với một dự đoán. Thay vì nén cả chuỗi vào một trạng thái ẩn cuối cùng, attention tính một tổ hợp có trọng số của tất cả trạng thái ẩn.

class AttentionLayer(nn.Module):
	def __init__(self, hidden_size):
    		super().__init__()
    		self.attention = nn.Linear(hidden_size, 1)
	
	def forward(self, lstm_outputs):
    	# lstm_outputs: (batch, seq_len, hidden_size)
    		scores = self.attention(lstm_outputs)  # (batch, seq_len, 1)
    		weights = torch.softmax(scores, dim=1)
    		context = torch.sum(weights * lstm_outputs, dim=1)
    		return context

Trong một đánh giá như "The acting was wooden, the plot made no sense, but the cinematography was stunning," attention học cách tập trung vào các cụm mang cảm xúc thay vì coi mọi từ là ngang nhau. Mô hình gán trọng số cao hơn cho "wooden," "no sense," và "stunning" trong khi giảm trọng số cho "the" và "was."

Attention thêm chi phí tính toán tối thiểu và thường mang lại cải thiện 2-5% độ chính xác cho các tác vụ phân loại. Với các bài toán chuỗi-đến-chuỗi như dịch máy, attention giữa bộ mã hóa và giải mã gần như bắt buộc để đạt hiệu năng tốt.

Xếp chồng các tầng LSTM

Một tầng LSTM đơn nắm bắt các mẫu ở một mức trừu tượng. Xếp chồng nhiều tầng cho phép mạng học đặc trưng phân cấp: tầng đầu có thể phát hiện mẫu cụm từ cục bộ, còn các tầng cao hơn kết hợp chúng thành hiểu biết cấp tài liệu.

self.lstm = nn.LSTM(
	input_size=embedding_dim,
	hidden_size=hidden_size,
	num_layers=3,  # Stack three layers
	dropout=0.3,   # Dropout between layers (not within)
	batch_first=True,
)

Hướng dẫn về độ sâu:

  •  1 tầng: Tác vụ đơn giản, dữ liệu nhỏ, lặp nhanh
  • 2 tầng: Đa số ứng dụng sản xuất
  • 3-4 tầng: Phụ thuộc thời gian phức tạp, dữ liệu lớn
  • Trên 4: Lợi ích giảm dần, huấn luyện kém ổn định

Mỗi tầng bổ sung gần như nhân đôi thời gian huấn luyện. Bắt đầu nông và chỉ thêm độ sâu nếu chỉ số xác thực đi ngang.

Kỹ thuật điều chuẩn

LSTM dễ quá khớp, đặc biệt trên dữ liệu nhỏ. Ba kỹ thuật điều chuẩn hoạt động tốt cùng nhau:

  • Dropout
  • Cắt gradient
  • Dừng sớm

Dropout

Dropout ngẫu nhiên đưa một số nơ-ron về 0 trong huấn luyện, buộc mạng phân tán việc học qua nhiều đường dẫn. Áp dụng dropout giữa các tầng LSTM, không phải bên trong kết nối hồi quy (vì có thể làm gián đoạn học theo thời gian).

# PyTorch applies dropout between stacked layers automatically
self.lstm = nn.LSTM(..., num_layers=2, dropout=0.3)
 
# For single-layer LSTMs, add dropout after
self.dropout = nn.Dropout(0.3)

Tỷ lệ dropout 0.2-0.5 hiệu quả cho hầu hết tác vụ. Giá trị cao hơn giúp khi quá khớp nghiêm trọng nhưng làm chậm hội tụ.

Cắt gradient

Cắt gradient giới hạn độ lớn gradient trong lan truyền ngược. Dù LSTM giảm gradient biến mất, gradient bùng nổ vẫn xảy ra, đặc biệt với chuỗi dài hoặc mạng sâu.

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)

Giá trị giữa 1.0 và 5.0 là điển hình. Nếu loss huấn luyện tăng đột biến hoặc cho ra giá trị NaN, hãy giảm ngưỡng.

Dừng sớm

Dừng sớm theo dõi loss xác thực và dừng huấn luyện khi nó ngừng cải thiện. Điều này ngăn mô hình ghi nhớ dữ liệu huấn luyện làm giảm khả năng khái quát.

patience = 5
best_val_loss = float('inf')
epochs_without_improvement = 0
 
for epoch in range(max_epochs):
	train_loss = train_one_epoch(model, train_loader)
	val_loss = evaluate(model, val_loader)
	
	if val_loss < best_val_loss:
    		best_val_loss = val_loss
    		epochs_without_improvement = 0
    		torch.save(model.state_dict(), 'best_model.pt')
	else:
    		epochs_without_improvement += 1
	if epochs_without_improvement >= patience:
        	break

Để xem hướng dẫn đầy đủ về vòng lặp huấn luyện, chỉ số đánh giá và gỡ lỗi các vấn đề thường gặp trong LSTM, hãy xem hướng dẫn NLP với PyTorch.

Embedding tiền huấn luyện

Khởi tạo embedding ngẫu nhiên hoạt động được, nhưng các embedding tiền huấn luyện như GloVe hoặc Word2Vec cho mô hình một xuất phát điểm tốt. Các embedding này mã hóa quan hệ ngữ nghĩa học từ hàng tỷ từ: "king" và "queen" ở gần nhau, cũng như "terrible" và "awful."

import numpy as np
 
def load_glove_embeddings(glove_path, vocab, embedding_dim):
	embeddings = np.random.randn(len(vocab), embedding_dim) * 0.01
	
	with open(glove_path, 'r') as f:
    	for line in f:
        	parts = line.split()
        	word = parts[0]
        	if word in vocab:
            		embeddings[vocab[word]] = np.array(parts[1:], dtype=np.float32)
	
	return torch.tensor(embeddings, dtype=torch.float32)
 
# Load and freeze or fine-tune
pretrained = load_glove_embeddings('glove.6B.100d.txt', vocab, 100)
model.embedding.weight.data.copy_(pretrained)
model.embedding.weight.requires_grad = True  # Fine-tune during training

Embedding tiền huấn luyện hữu ích nhất khi dữ liệu huấn luyện hạn chế. Với hàng triệu ví dụ, embedding khởi tạo ngẫu nhiên cuối cùng sẽ bắt kịp. Với hầu hết ứng dụng thực tế có 10k-100k mẫu, kỳ vọng tăng 3-8% độ chính xác nhờ khởi tạo tiền huấn luyện.

Kết hợp các kỹ thuật

Các cách tiếp cận này có thể xếp chồng. Một bộ phân loại văn bản cấp sản xuất có thể dùng:

  • BiLSTM để có ngữ cảnh hai chiều
  • 2 tầng xếp chồng để có đặc trưng phân cấp
  • Attention pooling thay cho trạng thái ẩn cuối
  • Embedding GloVe được tinh chỉnh trong huấn luyện
  • Dropout 0.3 giữa các tầng
  • Cắt gradient ở 5.0
  • Dừng sớm với patience 5 epoch

Mỗi bổ sung làm tăng độ phức tạp và thời gian huấn luyện. Bắt đầu với LSTM một tầng cơ bản, đo hiệu năng, sau đó thêm từng kỹ thuật một để xem cái nào mang lại lợi ích cho tác vụ cụ thể của bạn.

LSTM so với Transformer: Chọn kiến trúc phù hợp

LSTM thống trị mô hình hóa chuỗi từ cuối những năm 1990 đến giữa những năm 2010. Bài báo 2017 "Attention Is All You Need" đã thay đổi điều đó. Transformer giờ đang vận hành hầu hết các mô hình ngôn ngữ lớn, nhưng LSTM chưa biến mất. Chúng phù hợp với những ngách khác nhau.

Hạn chế của LSTM

Các phần trước đã nói về cách LSTM xử lý chuỗi từng token một và vì sao cơ chế cổng đó hiệu quả. Ở quy mô lớn, sự phụ thuộc tuần tự này trở thành nút thắt cổ chai. GPU của bạn có thể có hàng nghìn lõi, nhưng phần lớn nhàn rỗi trong khi chuỗi trạng thái ẩn hoàn tất. Một đầu vào 500 token nghĩa là 500 phép toán nối tiếp.

Transformer không có ràng buộc này. Self-attention tính mối quan hệ giữa mọi vị trí đồng thời, nên cùng một chuỗi 500 token được xử lý song song. Trên phần cứng hiện đại với kích thước batch lớn, điều này chuyển thành tăng tốc huấn luyện 10-100 lần.

Giới hạn độ dài chuỗi cũng quan trọng. Ta đã thấy LSTM giữ gradient tốt hơn RNN nguyên bản, nhưng "tốt hơn" không có nghĩa là "hoàn hảo." Hiệu năng vẫn giảm ở khoảng 200-500 token, tùy tác vụ. 

Transformer tính attention giữa mọi cặp vị trí trực tiếp, nên token 1 chú ý đến token 1000 một cách dễ dàng như nhau. Đánh đổi là bộ nhớ: ma trận attention tăng theo bình phương độ dài chuỗi.

Kích hoạt cổng cũng khiến việc diễn giải trở nên khó khăn. Giá trị trong khoảng 0 đến 1 mà không có ranh giới rõ ràng. Trọng số attention trong Transformer cũng không phải lời giải thích hoàn hảo, nhưng chúng cho tín hiệu rõ ràng hơn về token đầu vào nào quan trọng với mỗi đầu ra.

Khi nào dùng LSTM so với Transformer

Transformer hợp lý khi:

  • Dữ liệu huấn luyện vượt 100K ví dụ, và bạn có GPU
  • Tác vụ liên quan đến phụ thuộc trải dài hàng trăm token
  • Tốc độ huấn luyện quan trọng cho vòng lặp lặp lại
  • Có thể tinh chỉnh các mô hình tiền huấn luyện như GPT cho trường hợp của bạn

LSTM vẫn phù hợp khi:

  • Thiết bị mục tiêu bị giới hạn bộ nhớ (edge, nhúng, di động)
  • Dữ liệu khan hiếm (dưới 10K ví dụ), và Transformer dễ quá khớp
  • Dự đoán cần diễn ra trên chuỗi một phần trước khi nhận đủ đầu vào
  • Yêu cầu độ trễ buộc phải dùng mô hình gọn nhẹ

Dự báo chuỗi thời gian nằm đâu đó ở giữa. Các benchmark gần đây cho thấy cả hai kiến trúc có hiệu năng tương tự trên dữ liệu đơn biến với mẫu đều đặn. Transformer vượt trội khi nhiều đặc trưng tương tác phức tạp, còn LSTM trụ vững khi dữ liệu khan hiếm hoặc tài nguyên tính toán hạn hẹp.

Thiết kế lai pha trộn hai cách tiếp cận. Một số kiến trúc xếp chồng tầng LSTM với cơ chế attention. xLSTM, công bố năm 2024, xem xét lại thiết kế gốc với cổng hàm mũ và bộ nhớ ma trận, cho kết quả cạnh tranh với Transformer ở quy mô nhỏ hơn.

Khía cạnh

LSTM

Transformer

Xử lý

Tuần tự (từng token)

Song song (mọi vị trí cùng lúc)

Tốc độ huấn luyện

Chậm hơn (hạn chế song song hóa)

Nhanh hơn trên GPU

Phụ thuộc tầm xa

Suy giảm sau 200-500 token

Xử lý hàng nghìn token

Tỷ lệ bộ nhớ

O(n) theo độ dài chuỗi

O(n²) cho ma trận attention

Hiệu năng với dữ liệu nhỏ

Tốt (ít tham số hơn)

Dễ quá khớp

Triển khai biên

Phù hợp (dấu chân bộ nhớ thấp)

Thách thức (mô hình lớn)

Streaming/thời gian thực

Hỗ trợ tự nhiên

Cần toàn bộ chuỗi

Hệ sinh thái tiền huấn luyện

Hạn chế

Rộng lớn (BERT, GPT, v.v.)

Quyết định thường phụ thuộc vào ràng buộc. Tài nguyên tính toán và dữ liệu không giới hạn? Transformer thắng ở hầu hết tác vụ NLP. Vi điều khiển với 10K ví dụ huấn luyện? LSTM vẫn thực tế.

Kết luận

LSTM đã giải một vấn đề thực khi ra đời năm 1997. Cơ chế cổng do Hochreiter và Schmidhuber giới thiệu cho phép mạng cuối cùng học từ các chuỗi dài hơn vài chục token. Kiến trúc đó đã thúc đẩy dịch máy, nhận dạng giọng nói và dự báo chuỗi thời gian trong gần hai thập kỷ.

Transformer hiện xử lý hầu hết tác vụ NLP quy mô lớn, nhưng LSTM chưa lỗi thời. Chúng vẫn hợp lý cho triển khai biên, ứng dụng streaming và tình huống dữ liệu huấn luyện hạn chế.

Nếu bạn bắt đầu một dự án mô hình hóa chuỗi mới, hãy thử một LSTM đơn giản trước. Đo lường hiệu năng, hiểu các chế độ thất bại của nó, rồi quyết định liệu vấn đề có thực sự cần Transformer hay không. Các khái niệm từ hướng dẫn này (cổng, trạng thái ô, dòng chảy gradient) chuyển giao trực tiếp sang việc hiểu các kiến trúc phức tạp hơn.

Để luyện tập thực hành, tôi khuyên bạn tham gia khóa học Mạng nơ-ron hồi quy, khóa học này hướng dẫn thêm về các ứng dụng LSTM với bài tập có hướng dẫn.

Câu hỏi thường gặp về LSTM

Sự khác biệt giữa LSTM và RNN thông thường là gì?

RNN thông thường gặp khó với chuỗi dài vì gradient thu nhỏ trong quá trình lan truyền ngược, làm mất khả năng học từ ngữ cảnh xa. LSTM thêm trạng thái ô với cơ chế cổng kiểm soát luồng thông tin, cho phép mạng giữ ngữ cảnh liên quan qua hàng trăm bước thời gian thay vì chỉ 20-30.

Khi nào tôi nên dùng LSTM thay vì Transformer?

LSTM hoạt động tốt hơn khi bạn có dữ liệu huấn luyện hạn chế (dưới 10K ví dụ), cần chạy trên thiết bị hạn chế bộ nhớ như di động hoặc hệ nhúng, yêu cầu dự đoán thời gian thực trên dữ liệu streaming trước khi nhận đủ chuỗi, hoặc có yêu cầu độ trễ nghiêm ngặt khiến các mô hình lớn bị loại trừ.

hidden_size trong PyTorch LSTM có ý nghĩa gì?

Tham số hidden_size xác định số chiều của cả vector trạng thái ẩn và trạng thái ô. Giá trị lớn hơn cho phép mô hình theo dõi nhiều mẫu hơn, nhưng tăng chi phí tính toán và bộ nhớ. Phổ biến là 32-512, với 128-256 thường dùng trong ứng dụng văn bản sản xuất.

Làm sao để ngăn mô hình LSTM bị quá khớp?

Dùng dropout giữa các tầng LSTM (tỷ lệ 0.2-0.5), áp dụng cắt gradient để ngăn gradient bùng nổ, triển khai dừng sớm để dừng huấn luyện khi loss xác thực ngừng cải thiện, và cân nhắc dùng embedding tiền huấn luyện như GloVe nếu bộ dữ liệu nhỏ.

LSTM hai chiều là gì và khi nào nên dùng?

LSTM hai chiều chạy hai lượt qua chuỗi: một tiến và một lùi. Điều này cho mỗi vị trí ngữ cảnh từ cả hai hướng, hữu ích khi ý nghĩa phụ thuộc vào các token tương lai (như "not bad" nơi "not" cần thấy "bad"). Hãy dùng cho các tác vụ phân loại và gán nhãn nơi bạn có toàn bộ chuỗi khi suy luận, nhưng tránh dùng cho ứng dụng streaming.

Bexruz (Bex) Tuychiev

Tác giả

Bexruz (Bex) Tuychiev

Tôi là người sáng tạo nội dung về khoa học dữ liệu với hơn 2 năm kinh nghiệm và là một trong những tài khoản có lượng người theo dõi lớn nhất trên Medium. Tôi thích viết các bài chuyên sâu về AI và ML với chút giọng điệu mỉa mai, vì bạn cũng phải làm gì đó để chúng bớt khô khan. Tôi đã xuất bản hơn 130 bài viết và một khóa học trên DataCamp, cùng với một khóa nữa đang được thực hiện. Nội dung của tôi đã được hơn 5 triệu lượt xem, trong đó 20 nghìn người đã trở thành người theo dõi trên cả Medium và LinkedIn. 

Chủ đề

Liên quan