Skip to content

Mô hình chuỗi sang chuỗi (Seq2Seq)

This content is not available in your language yet.

Sau bài này, bạn sẽ:

  • Hiểu bài toán chuỗi sang chuỗi (sequence-to-sequence, Seq2Seq), khung encoder–decoder, và vì sao nén cả câu nguồn vào một vectơ cố định là một nút thắt.
  • Giải thích được attention của Bahdanau và cross-attention trong Transformer: truy vấn từ decoder, khóa và giá trị từ encoder.
  • Phân biệt teacher forcing khi huấn luyện với sinh tự do khi dùng, và nhận ra exposure bias.
  • Cài đặt được beam search có phạt độ dài (length penalty) bằng NumPy, và đọc hiểu một Transformer encoder–decoder nhỏ viết bằng PyTorch.
  • LSTM and BiLSTM, in Deep Learning(Not translated yet) (Learned)
  • Transformers, in Deep Learning(Not translated yet) (Learned)

Hãy dịch hai câu tiếng Anh sang tiếng Việt:

Câu nguồnCâu đíchSố từ
Good morningChào buổi sáng2 → 3
the black catcon mèo đen3 → 3, nhưng “black cat” thành “mèo đen”

Hai điều làm việc này khác hẳn phân loại. Độ dài khác nhau: câu ra có thể dài hơn hay ngắn hơn câu vào, và ta không biết trước nó dài bao nhiêu. Trật tự khác nhau: từ thứ hai của câu ra không nhất thiết ứng với từ thứ hai của câu vào. Vì vậy không thể gắn cho mỗi từ nguồn một nhãn đích, như khi gán nhãn từ loại.

Dịch máy chỉ là một ví dụ. Tóm tắt (văn bản dài vào, đoạn ngắn ra), hỏi đáp có sinh câu trả lời, hay sửa lỗi chính tả đều có cùng dạng: một chuỗi vào, một chuỗi ra, độ dài và trật tự của hai bên tự do.

Cách làm đầu tiên giống một người phiên dịch nghe hết câu, ghi ý vào một mẩu giấy nhỏ, rồi úp bản gốc xuống và dịch chỉ dựa vào mẩu giấy. Phần nghe là encoder (bộ mã hóa), phần nói là decoder (bộ giải mã). Câu ngắn thì ổn. Câu năm mươi từ thì mẩu giấy vẫn chỉ to chừng ấy, nên chi tiết rơi rớt.

Cách tốt hơn: để bản gốc nằm trên bàn. Mỗi khi viết một từ, người phiên dịch liếc lại đúng chỗ cần nhìn trong câu gốc. Viết “mèo” thì nhìn “cat”, viết “đen” thì nhìn “black”. Đó là ý tưởng của attention, và cũng là việc của cross-attention trong Transformer.

Còn một chuyện nữa: mỗi bước, mô hình có nhiều từ có thể viết tiếp. Chọn từ “tốt nhất” ở từng bước chưa chắc ra câu tốt nhất, vì một lựa chọn ban đầu hơi kém có khi mở ra phần sau rất tốt.

Bài toán của bài này là: từ các cặp câu nguồn và câu đích, học cách sinh câu đích từng token một, mỗi bước nhìn lại câu nguồn; rồi khi chỉ có câu nguồn, tìm một câu đích có xác suất cao.

Gọi chuỗi nguồn là x=(x1,…,xLx)\mathbf{x} = (x_1, \dots, x_{L_x}) và chuỗi đích là y=(y1,…,yLy)\mathbf{y} = (y_1, \dots, y_{L_y}), với LxL_x, LyL_y là độ dài của hai chuỗi. Token cuối yLyy_{L_y} luôn là <EOS> (end of sequence): mô hình tự quyết định khi nào dừng bằng cách sinh ra token này, nên LyL_y tính cả <EOS>.

Như bài Xây dựng LLM đầu tiên, ta tách xác suất của cả chuỗi bằng quy tắc nhân. Khác biệt duy nhất là mọi thừa số đều có thêm điều kiện x\mathbf{x}:

p(y∣x)=∏i=1Lyp(yi∣y<i,x)p(\mathbf{y} \mid \mathbf{x}) = \prod_{i=1}^{L_y} p(y_i \mid y_{<i}, \mathbf{x})

Ở đây y<iy_{<i} là các token đích đứng trước vị trí ii. Vậy mô hình seq2seq là một mô hình ngôn ngữ có điều kiện: nó đoán token kế tiếp của câu đích, khi biết các token đích đã có và cả câu nguồn.

Sutskever, Vinyals và Le (2014) và Cho cùng cộng sự (2014) đưa ra khung encoder–decoder bằng hai mạng hồi tiếp:

  1. Encoder đọc x1,…,xLxx_1, \dots, x_{L_x}. Trạng thái ẩn cuối cùng là một vectơ c\mathbf{c} có số chiều cố định, đại diện cho cả câu nguồn.
  2. Decoder sinh y1,y2,…y_1, y_2, \dots dựa trên c\mathbf{c}, từng token một, mỗi bước nhận token vừa sinh làm đầu vào, cho tới khi sinh <EOS>.

Hai mạng được huấn luyện cùng nhau để làm lớn ln⁡p(y∣x)\ln p(\mathbf{y} \mid \mathbf{x}) trên các cặp câu. Mô hình của Sutskever và cộng sự dùng LSTM 4 lớp, mỗi lớp 1000 ô. Trên tập dịch Anh–Pháp WMT’14, tổ hợp 5 mô hình (ensemble) của họ đạt điểm BLEU 34.8, so với 33.3 của một hệ thống dịch dựa trên cụm từ. BLEU đo mức trùng các cụm từ ngắn (n-gram) giữa bản dịch máy và bản dịch tham chiếu của người; cao hơn là tốt hơn. Họ còn gặp một mẹo lạ: đảo ngược trật tự câu nguồn làm BLEU tăng từ 25.9 lên 30.6. Họ thừa nhận chưa giải thích được đầy đủ, và cho rằng khi đảo, nhiều cặp từ tương ứng giữa hai câu nằm gần nhau hơn, nên bài toán tối ưu dễ hơn.

Mẹo đó là một dấu hiệu: mọi thông tin của câu nguồn phải đi qua đúng một vectơ c\mathbf{c}. Câu 5 từ hay 50 từ đều được nén vào cùng một số chiều. Bahdanau, Cho và Bengio (2015) đặt giả thuyết rằng chính vectơ cố định này là nút thắt (bottleneck), và đo được bằng chứng: mô hình encoder–decoder cơ bản tụt điểm mạnh khi câu dài ra, còn bản tốt nhất có attention của họ không tụt, kể cả với câu từ 50 từ trở lên.

Bahdanau và cộng sự bỏ yêu cầu “một vectơ cho cả câu”. Encoder là một RNN hai chiều, cho mỗi từ nguồn jj một vectơ hj\mathbf{h}_j mang thông tin của từ đó và các từ xung quanh. Gọi si−1\mathbf{s}_{i-1} là trạng thái của decoder ngay trước khi sinh yiy_i. Ở mỗi bước ii, decoder làm ba việc:

eij=a(si−1,hj),αij=exp⁡(eij)∑k=1Lxexp⁡(eik),ci=∑j=1Lxαijhje_{ij} = a(\mathbf{s}_{i-1}, \mathbf{h}_j), \qquad \alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{L_x} \exp(e_{ik})}, \qquad \mathbf{c}_i = \sum_{j=1}^{L_x} \alpha_{ij} \mathbf{h}_j
  • Chấm điểm. eije_{ij} đo “để sinh từ đích thứ ii, nên nhìn từ nguồn thứ jj bao nhiêu”. Hàm aa là một mạng nhỏ, học cùng cả mô hình.
  • Chuẩn hóa. Softmax trên jj biến các điểm thành trọng số αij\alpha_{ij} dương, cộng lại bằng 1 trên mọi từ nguồn.
  • Trộn. ci\mathbf{c}_i là trung bình có trọng số của các hj\mathbf{h}_j. Đây là bản tóm tắt câu nguồn riêng cho bước ii, thay cho vectơ c\mathbf{c} chung.

Encoder vẫn tạo vectơ cho câu nguồn; điều thay đổi là decoder không còn bị buộc chỉ dùng một vectơ. Bảng trọng số α\alpha còn đọc được bằng mắt. Với “the black cat” sang “con mèo đen”, một mô hình học tốt có thể cho bảng như dưới (số tự đặt để minh họa, không phải số của mô hình thật):

Từ đíchnhìn “the”nhìn “black”nhìn “cat”
con0.800.050.15
mèo0.050.100.85
đen0.020.900.08

Mỗi hàng cộng lại bằng 1. Hai hàng cuối đổi chỗ so với đường chéo, đúng chỗ tiếng Việt đặt tính từ sau danh từ. Bahdanau và cộng sự cho thấy các bản đồ như vậy từ mô hình thật trên câu Anh–Pháp: gần như theo đường chéo, lệch ra ở chỗ hai ngôn ngữ đổi trật tự.

Transformer (Vaswani và cộng sự, 2017) giữ khung encoder–decoder nhưng bỏ RNN, chỉ dùng attention. Bản gốc có 6 lớp encoder và 6 lớp decoder:

  • Một lớp encoder gồm self-attention rồi mạng truyền thẳng, mỗi phần có kết nối tắt và chuẩn hóa lớp. Self-attention ở đây không có mặt nạ: mỗi từ nguồn nhìn được mọi từ nguồn, cả trước lẫn sau.
  • Một lớp decoder có ba phần: self-attention có mặt nạ nhân quả trên các token đích, rồi cross-attention nhìn sang encoder, rồi mạng truyền thẳng.

Cross-attention dùng đúng công thức attention mà bạn đã gặp. Khác biệt nằm ở nguồn của ba ma trận. Gọi Henc∈RLx×D\mathbf{H}_{\text{enc}} \in \mathbb{R}^{L_x \times D} là đầu ra của lớp encoder cuối, mỗi hàng một vị trí nguồn, và S∈RLy×D\mathbf{S} \in \mathbb{R}^{L_y \times D} là trạng thái của decoder (đầu ra của phần self-attention ngay trước), mỗi hàng một vị trí đích. Với các ma trận chiếu học được WQ\mathbf{W}^Q, WK\mathbf{W}^K, WV\mathbf{W}^V:

Q=SWQ,K=HencWK,V=HencWV,Attn=softmax ⁣(QK⊤d)V\mathbf{Q} = \mathbf{S}\mathbf{W}^Q, \quad \mathbf{K} = \mathbf{H}_{\text{enc}}\mathbf{W}^K, \quad \mathbf{V} = \mathbf{H}_{\text{enc}}\mathbf{W}^V, \qquad \mathrm{Attn} = \mathrm{softmax}\!\left(\frac{\mathbf{Q}\mathbf{K}^\top}{\sqrt{d}}\right)\mathbf{V}
  • Truy vấn từ decoder, khóa và giá trị từ encoder. Mỗi vị trí đích “hỏi”, các vị trí nguồn “trả lời”.
  • Ma trận điểm QK⊤\mathbf{Q}\mathbf{K}^\top có kích thước Ly×LxL_y \times L_x. Hàng ii là các điểm của vị trí đích ii trên mọi vị trí nguồn; softmax theo hàng cho đúng vai trò của αij\alpha_{ij} ở trên.
  • Không có mặt nạ nhân quả. Câu nguồn đã có đủ từ đầu, không có vị trí nào là “tương lai”. Chỉ self-attention của decoder mới cần mặt nạ.
  • Mọi lớp decoder dùng cùng Henc\mathbf{H}_{\text{enc}}, nhưng mỗi lớp có ma trận chiếu riêng. Công thức trên là một đầu; Transformer dùng nhiều đầu song song. Với HH đầu, mỗi đầu chiếu xuống d=D/Hd = D/H chiều và chia cho d\sqrt{d}.

Vaswani và cộng sự viết rằng cross-attention bắt chước cơ chế attention encoder–decoder của các mô hình seq2seq trước đó. Hai khác biệt chính: điểm là tích vô hướng thay cho mạng nhỏ aa, và mọi vị trí được tính song song thay vì từng bước như RNN. Bản lớn của họ đạt BLEU 28.4 trên tập dịch Anh–Đức WMT 2014.

Khi huấn luyện, ta đã có câu đích thật. Vậy đầu vào của decoder là chính câu đích, dịch sang phải một vị trí (shift right), bắt đầu bằng token mở đầu <BOS>; còn nhãn là câu đích không dịch:

Vị trí1234
Đầu vào decoder<BOS>conmèođen
Nhãn cần đoánconmèođen<EOS>

Cách đưa đáp án thật làm đầu vào này gọi là teacher forcing. Nhờ mặt nạ nhân quả, vị trí ii chỉ thấy <BOS> và y1,…,yi−1y_1, \dots, y_{i-1}, nên một lần chạy cho cả LyL_y dự đoán, như với GPT. Hàm mất mát là cross-entropy trung bình trên các vị trí:

L(θ)=−1Ly∑i=1Lyln⁡pθ(yi∣y<i,x)\mathcal{L}(\boldsymbol{\theta}) = -\frac{1}{L_y} \sum_{i=1}^{L_y} \ln p_{\boldsymbol{\theta}}(y_i \mid y_{<i}, \mathbf{x})

Trong công thức này, y<iy_{<i} là token thật. Khi lô có nhiều cặp câu, ta lấy trung bình thêm trên các cặp.

Exposure bias: lúc học và lúc dùng không giống nhau

Phần tiêu đề “Exposure bias: lúc học và lúc dùng không giống nhau”

Khi dùng mô hình, không có câu đích thật. Decoder phải ăn lại token do chính nó sinh ra: y<iy_{<i} trong công thức giờ là các token mô hình đã chọn. Ranzato và cộng sự (2016) gọi sự lệch này là exposure bias: lúc huấn luyện, mô hình chỉ được thấy tiền tố đúng, chưa từng tập tiếp tục từ một tiền tố có lỗi. Một token sai sớm làm tiền tố khác mọi thứ nó đã thấy, và lỗi có thể dồn lên nhau.

Teacher forcing không phải lỗi: nó nhanh, song song và ổn định. Vấn đề là khoảng cách giữa hai chế độ. Một hướng xử lý là scheduled sampling (Bengio và cộng sự, 2015): trong lúc huấn luyện, thỉnh thoảng thay token thật bằng token mô hình tự sinh, tỉ lệ thay tăng dần. Mức nặng nhẹ của exposure bias tùy bài toán: ở ô PyTorch bên dưới, tác vụ quá dễ nên không thấy nó.

Khi đã có mô hình, ta muốn câu y\mathbf{y} có p(y∣x)p(\mathbf{y} \mid \mathbf{x}) lớn nhất. Thử hết thì không thể: với từ vựng 32 000 token và câu 20 token, có 32 00020≈109032\,000^{20} \approx 10^{90} chuỗi (tự tính).

Giải mã tham lam (greedy decoding) chọn token xác suất cao nhất ở mỗi bước. Nhanh, nhưng một lựa chọn sớm không bao giờ được xem lại.

Beam search giữ BB giả thuyết dở dang tốt nhất (gọi là chùm, beam), BB là độ rộng chùm (beam width). Điểm của một giả thuyết là tổng log-xác suất của các token của nó:

score(y)=∑i=1Lyln⁡p(yi∣y<i,x)=ln⁡p(y∣x)\mathrm{score}(\mathbf{y}) = \sum_{i=1}^{L_y} \ln p(y_i \mid y_{<i}, \mathbf{x}) = \ln p(\mathbf{y} \mid \mathbf{x})

Đẳng thức thứ hai có được vì logarit của tích bằng tổng các logarit. Mỗi bước:

  1. Nối mỗi giả thuyết trong chùm với mọi token có thể, cộng thêm log-xác suất của token đó.
  2. Trong mọi ứng viên, giữ BB ứng viên điểm cao nhất.
  3. Ứng viên nào vừa sinh <EOS> thì tách ra danh sách “đã xong”. Dừng khi chùm rỗng, hoặc khi đủ độ dài tối đa.

B=1B = 1 chính là giải mã tham lam. Sutskever và cộng sự thấy B=1B = 1 đã khá tốt và B=2B = 2 lấy được phần lớn lợi ích của beam search; Vaswani và cộng sự dùng B=4B = 4. Beam search vẫn chỉ là xấp xỉ: không đảm bảo tìm ra câu có xác suất cao nhất.

Phạt độ dài. Mỗi token cộng thêm một số ln⁡p≤0\ln p \le 0 vào điểm, nên câu càng dài điểm càng âm. Beam search thô vì vậy thiên về câu ngắn, kể cả câu cắt cụt. Wu và cộng sự (2016) chia điểm cho một hàm tăng theo độ dài:

s(y)=ln⁡p(y∣x)lp(y),lp(y)=(5+∣y∣5+1)αs(\mathbf{y}) = \frac{\ln p(\mathbf{y} \mid \mathbf{x})}{\mathrm{lp}(\mathbf{y})}, \qquad \mathrm{lp}(\mathbf{y}) = \left(\frac{5 + |\mathbf{y}|}{5 + 1}\right)^{\alpha}
  • ∣y∣|\mathbf{y}| là số token của câu đích. Bài này tính cả <EOS>; bài báo không nói rõ điểm này, và các thư viện có thể đếm khác nhau.
  • α=0\alpha = 0 cho lp=1\mathrm{lp} = 1: quay về beam search thô. α\alpha càng lớn, câu dài càng được bù nhiều.
  • Không có α\alpha “đúng” chung. Nó được chỉnh trên tập kiểm định (validation set): Wu và cộng sự dùng α=0.2\alpha = 0.2 cùng một khoản phạt khác tên là coverage penalty, Vaswani và cộng sự dùng α=0.6\alpha = 0.6.

Raffel và cộng sự (2020) đẩy ý seq2seq tới cùng với T5: mọi tác vụ đều viết thành “văn bản vào, văn bản ra”. Chỉ cần thêm một tiền tố tác vụ (task prefix) vào đầu câu vào. Các ví dụ trong bài báo:

Đầu vàoĐầu ra
translate English to German: That is good.Das ist gut.
cola sentence: The course is jumping well.not acceptable
stsb sentence1: The rhino grazed on the grass. sentence2: A rhino is grazing in a field.3.8

Phân loại (câu có đúng ngữ pháp không) cho ra chữ “not acceptable”; chấm độ giống nhau của hai câu cho ra chuỗi “3.8”. Cùng một mô hình, cùng hàm mất mát, cùng cách giải mã cho mọi việc. Ý “nói việc cần làm bằng chữ ở đầu đầu vào” là họ hàng gần của prompt chỉ dẫn ở bài Prompt Engineering.

T5 dùng Transformer encoder–decoder chuẩn, huấn luyện bằng teacher forcing. Bản cơ sở có 12 khối ở mỗi bên, khoảng 220 triệu tham số. Trong thí nghiệm so kiến trúc của họ, với mục tiêu khử nhiễu (denoising) mà T5 chọn, bản encoder–decoder cho điểm cao nhất trên mọi tác vụ, hơn các biến thể decoder-only (chỉ có decoder) cùng chi phí tính toán (dù bản encoder–decoder có gấp đôi số tham số).

GPT và các họ mô hình như LLaMA là decoder-only: không có encoder, không có cross-attention. Câu vào và câu ra nằm nối nhau trong một chuỗi, và mọi vị trí đều dùng mặt nạ nhân quả. Giữa hai kiểu có prefix LM: vẫn một chồng khối, nhưng phần câu vào nhìn đầy đủ hai chiều, chỉ phần câu ra là nhân quả.

Tiêu chíEncoder–decoder (T5)Prefix LMDecoder-only (GPT)
Câu vào nhìnCả hai chiều, trong encoderCả hai chiềuChỉ về trước (nhân quả)
Câu ra nhìnNhân quả, cộng cross-attention sang câu vàoNhân quả, cả câu vàoNhân quả, cả câu vào
Số chồng khốiHaiMộtMột

Decoder-only làm được việc seq2seq bằng cách đặt câu vào làm phần đầu của chuỗi, như khi bạn gửi prompt cho một LLM. Encoder–decoder vẫn là lựa chọn tự nhiên cho dịch và tóm tắt, nơi câu vào và câu ra tách bạch.

  • Beam search tìm câu xác suất cao, không phải câu hay. Với sinh văn bản mở, Holtzman và cộng sự (2020) cho rằng giải mã theo xác suất cao nhất làm văn nhạt và lặp lạ thường; họ đề xuất top-p (bài Sử dụng API văn bản).
  • Văn bản nguồn rất dài vẫn đắt. Self-attention của encoder tốn Lx×LxL_x \times L_x điểm, cross-attention tốn Ly×LxL_y \times L_x, cho mỗi đầu ở mỗi lớp.

Hình dưới đặt encoder và decoder cạnh nhau. Hãy nhìn các mũi tên đi vào khối cross-attention: truy vấn đến từ phía decoder, còn khóa và giá trị đến từ đầu ra của encoder.

DecoderNext token: đen×NlayersFeed-forwardCross-attentionQSelf-attentionmaskedEncoderEncoder outputKV×NlayersFeed-forwardSelf-attentionno masktheblackcat<BOS>conmèoappendedwhilegenerating
The score matrix of cross-attention is Ly×LxL_y \times L_x and has no causal mask; only the self-attention of the decoder is masked. The original Transformer has N=6N = 6, the lesson's PyTorch cell N=2N = 2.

Bảng dưới tóm lại ba kiểu attention trong một Transformer encoder–decoder. Hàng cuối là chỗ hai nửa gặp nhau.

Kiểu attentionỞ đâuQ\mathbf{Q} từK\mathbf{K}, V\mathbf{V} từMặt nạ nhân quảKích thước ma trận điểm
Self-attentionEncoderCâu nguồnCâu nguồnKhôngLx×LxL_x \times L_x
Self-attention có mặt nạDecoderCâu đíchCâu đíchCóLy×LyL_y \times L_y
Cross-attentionDecoderCâu đíchĐầu ra encoderKhôngLy×LxL_y \times L_x

Code trong bài chạy thử lần cuối tháng 10/2026: ô beam search với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), ô Transformer với Python 3.14 và PyTorch 2.14 trên CPU. Ô beam search chạy ngay trên trang. Ô Transformer cần PyTorch nên chạy trên máy bạn.

Để thấy cơ chế mà không cần huấn luyện gì, ô dưới dùng một “decoder giả” với bảng xác suất tự đặt. Hãy coi như decoder đã đọc xong câu nguồn. Để code ngắn, xác suất token kế chỉ phụ thuộc token ngay trước; decoder thật dùng cả câu nguồn và mọi token đã sinh.

Token trướcToken kế và xác suất
<s> (bắt đầu)con 0.5, đàn 0.4, <eos> 0.1
conmèo 0.34, chó 0.33, chim 0.33
đànchim 0.9, mèo 0.1
mèo, chó, bay<eos> 1.0
chim<eos> 0.52, bay 0.48

Trước khi chạy, hãy tự tính: câu “con mèo” có xác suất 0.5×0.34×1.00.5 \times 0.34 \times 1.0, còn câu “đàn chim” có 0.4×0.9×0.520.4 \times 0.9 \times 0.52. Câu nào lớn hơn, và giải mã tham lam sẽ chọn câu nào?

Ô chạy ba cấu hình: tham lam (B=1B = 1), beam search B=2B = 2 không phạt độ dài, và B=2B = 2 với α=0.6\alpha = 0.6. Mỗi dòng in câu được chọn, xác suất của cả câu p, và điểm s đã chia cho lp\mathrm{lp}.

Tham lam và beam search trên bảng xác suất giả lập

# Giải mã tham lam và beam search trên một bảng xác suất giả lập (số tự đặt)
import numpy as np
# P[trước][kế] = p(token kế | token trước), như thể decoder đã đọc câu nguồn
P = {
"<s>": {"con": 0.5, "đàn": 0.4, "<eos>": 0.1},
"con": {"mèo": 0.34, "chó": 0.33, "chim": 0.33},
"đàn": {"chim": 0.9, "mèo": 0.1},
"mèo": {"<eos>": 1.0},
"chó": {"<eos>": 1.0},
"chim": {"<eos>": 0.52, "bay": 0.48},
"bay": {"<eos>": 1.0},
}
def lp(n, alpha): # phạt độ dài của Wu và cộng sự (2016)
return ((5 + n) / 6) ** alpha
def beam_search(width, alpha=0.0, max_len=6):
beams, done = [(0.0, ["<s>"])], [] # (tổng log-xác suất, chuỗi)
for _ in range(max_len):
cand = [(score + np.log(p), seq + [tok]) # nối mọi token có thể
for score, seq in beams
for tok, p in P[seq[-1]].items()]
cand.sort(key=lambda c: -c[0])
beams = []
for score, seq in cand[:width]: # chỉ giữ width ứng viên tốt nhất
(done if seq[-1] == "<eos>" else beams).append((score, seq))
if not beams:
break
# chọn theo điểm đã chia lp; độ dài n tính cả <eos>, không tính <s>
score, seq = max(done, key=lambda c: c[0] / lp(len(c[1]) - 1, alpha))
return " ".join(seq[1:-1]), score, score / lp(len(seq) - 1, alpha)
for width, alpha in [(1, 0.0), (2, 0.0), (2, 0.6)]:
text, score, s = beam_search(width, alpha)
print(f"B={width} alpha={alpha}: '{text}' p={np.exp(score):.3f} s={s:.3f}")

The first run downloads Python, about 10 MB.

Đọc kết quả:

  • Tham lam ra “con mèo”, p=0.170p = 0.170. Bước đầu nó chọn “con” (0.5 > 0.4), rồi gặp một nhánh phẳng: “mèo”, “chó”, “chim” gần như ngang nhau, nên tích chỉ còn 0.5×0.34=0.1700.5 \times 0.34 = 0.170.
  • Beam B=2B = 2 ra “đàn chim”, p=0.187p = 0.187. Nó giữ cả “đàn” ở bước đầu, và nhánh đó có “chim” chắc tới 0.9. Đây là câu có xác suất cao hơn, mà tham lam bỏ lỡ chỉ vì một lựa chọn sớm.
  • Thêm α=0.6\alpha = 0.6, beam đổi sang “đàn chim bay”, p=0.173p = 0.173, dù xác suất thô thấp hơn “đàn chim” (0.187). Tính tay với độ dài tính cả <eos>: “đàn chim” có p=0.1872p = 0.1872, ln⁡p=−1.676\ln p = -1.676, chia lp=(8/6)0.6≈1.188\mathrm{lp} = (8/6)^{0.6} \approx 1.188 được −1.410-1.410; “đàn chim bay” có p=0.1728p = 0.1728, ln⁡p=−1.756\ln p = -1.756, chia (9/6)0.6≈1.275(9/6)^{0.6} \approx 1.275 được −1.376-1.376, cao hơn.

Bảng này tự đặt để thấy cơ chế, không nói gì về việc phạt độ dài tốt hay xấu cho một bài dịch thật. Một chi tiết nữa của code: việc cắt chùm dùng log-xác suất thô, chỉ bước chọn cuối mới chia cho lp\mathrm{lp}. Như vậy beam search không đảm bảo tìm ra câu có điểm ss cao nhất.

Transformer encoder–decoder nhỏ bằng PyTorch

Phần tiêu đề “Transformer encoder–decoder nhỏ bằng PyTorch”

File dưới đây huấn luyện một Transformer encoder–decoder để đảo ngược dãy 6 chữ số: nguồn 9 7 9 6 0 9, đích 9 0 6 9 7 9. Tác vụ đồ chơi này có đủ mọi phần của seq2seq, và câu đích phụ thuộc vào cả câu nguồn: token đích đầu tiên là token nguồn cuối cùng, nên decoder buộc phải nhìn sang encoder.

  • Mô hình: nn.Transformer có 2 lớp encoder, 2 lớp decoder, D=64D = 64, 4 đầu, mạng truyền thẳng 128 chiều, cộng một lớp nhúng token và một lớp nhúng vị trí học được. Từ vựng có 12 token: <BOS>, <EOS> và 10 chữ số.
  • Mặt nạ: generate_square_subsequent_mask trả về ma trận số thực, −∞-\infty ở các ô phía trên đường chéo, được cộng thẳng vào điểm của self-attention trong decoder. Encoder và cross-attention không nhận mặt nạ nào.
  • Teacher forcing: hàm batch tạo đầu vào decoder là <BOS> cộng đáp án, nhãn là đáp án cộng <EOS>, đúng như bảng ở mục Lý thuyết. Mỗi bước sinh 64 dãy ngẫu nhiên mới; vòng lặp chạy 301 bước bằng AdamW.
  • Đánh giá trên 500 dãy mới, theo hai cách: tỉ lệ khớp hoàn toàn cả 7 token khi được đưa đáp án (teacher forcing), và khi tự sinh tham lam từ <BOS>, mỗi bước nối token vừa chọn vào.

Cách cài PyTorch bản chỉ chạy CPU có ở bài Xây dựng LLM đầu tiên.

Run on your machine

Transformer encoder–decoder đảo ngược dãy số

Install
pip install torch
Hardware
CPU là đủ, không cần GPU. Khoảng 20 tới 40 giây trên máy phát triển, có thể vài phút trên máy chậm hoặc đang bận
# Transformer encoder-decoder nhỏ học đảo ngược một dãy 6 chữ số
import torch
from torch import nn
torch.manual_seed(0)
BOS, EOS, L, D = 0, 1, 6, 64 # L: độ dài dãy, D: số chiều
TOK = ["<BOS>", "<EOS>"] + list("0123456789") # chữ số mang id 2..11
V = len(TOK) # từ vựng 12 token
CAUSAL = True # đổi thành False để thử bỏ mặt nạ của decoder
def batch(n):
src = torch.randint(2, V, (n, L))
tgt = src.flip(1) # đáp án: câu nguồn viết ngược
dec_in = torch.cat([torch.full((n, 1), BOS), tgt], 1) # dịch phải
dec_out = torch.cat([tgt, torch.full((n, 1), EOS)], 1) # nhãn + <EOS>
return src, dec_in, dec_out
class Seq2Seq(nn.Module):
def __init__(self):
super().__init__()
self.emb, self.pos = nn.Embedding(V, D), nn.Embedding(L + 1, D)
self.tf = nn.Transformer(D, nhead=4, num_encoder_layers=2,
num_decoder_layers=2, dim_feedforward=128,
dropout=0.0, batch_first=True)
self.out = nn.Linear(D, V)
def embed(self, x): # nhúng token cộng nhúng vị trí
return self.emb(x) + self.pos(torch.arange(x.size(1)))
def forward(self, src, dec_in):
n = dec_in.size(1)
mask = self.tf.generate_square_subsequent_mask(n) if CAUSAL else None
h = self.tf(self.embed(src), self.embed(dec_in), tgt_mask=mask,
tgt_is_causal=CAUSAL)
return self.out(h) # (lô, n, V) logit
model = Seq2Seq()
opt = torch.optim.AdamW(model.parameters(), lr=3e-3)
for step in range(301): # teacher forcing: decoder ăn đáp án thật
src, dec_in, dec_out = batch(64)
z = model(src, dec_in).transpose(1, 2) # cross_entropy cần lớp ở chiều 1
loss = nn.functional.cross_entropy(z, dec_out)
opt.zero_grad()
loss.backward()
opt.step()
if step % 100 == 0:
print(f"step {step:3d} loss {loss.item():.3f}")
model.eval()
with torch.no_grad():
src, dec_in, dec_out = batch(500) # 500 dãy mới
forced = (model(src, dec_in).argmax(-1) == dec_out).all(1).float().mean()
seq = torch.full((500, 1), BOS) # sinh tự do: ăn lại token của chính nó
for _ in range(L + 1):
nxt = model(src, seq)[:, -1].argmax(-1, keepdim=True) # tham lam
seq = torch.cat([seq, nxt], dim=1)
free = (seq[:, 1:] == dec_out).all(1).float().mean()
print(f"exact match, teacher forcing: {forced:.3f} greedy: {free:.3f}")
print("src:", *[TOK[i] for i in src[0].tolist()])
print("out:", *[TOK[i] for i in seq[0, 1:].tolist()])

Sample output

From one real run.

step 0 loss 2.608
step 100 loss 0.003
step 200 loss 0.001
step 300 loss 0.001
exact match, teacher forcing: 1.000 greedy: 1.000
src: 9 7 9 6 0 9
out: 9 0 6 9 7 9 <EOS>

Đọc kết quả:

  • Bước 0: loss 2.608, gần ln⁡12≈2.485\ln 12 \approx 2.485, mức của mô hình đoán đều trên 12 token.
  • Bước 100: loss đã xuống 0.003. Đảo dãy là việc rất hợp với cross-attention: vị trí đích ii chỉ cần nhìn đúng vị trí nguồn Lx+1−iL_x + 1 - i.
  • Cả hai cách đo đều khớp hoàn toàn 1.000 trên 500 dãy chưa thấy, và dãy mẫu được đảo đúng, kết thúc bằng <EOS>. Ở đây sinh tự do tốt ngang teacher forcing, nên không thấy exposure bias: tác vụ quá dễ để mô hình mắc lỗi đầu tiên. Đừng lấy kết quả này để kết luận exposure bias không có thật.

Seed cố định nên mỗi lần chạy trên cùng máy cho cùng kết quả; trên máy khác hoặc bản PyTorch khác, số có thể lệch đôi chút. Ở dòng 9, CAUSAL = True bật mặt nạ của decoder. Hộp đầu tiên ở mục dưới cho thấy chuyện gì xảy ra khi đổi nó thành False.

Question 1 of 4 Câu nguồn có 12 token, decoder đã có 5 token. Trong một lớp cross-attention (xét một đầu), ma trận điểm QK⊤\mathbf{Q}\mathbf{K}^\top có kích thước bao nhiêu, và có cần mặt nạ nhân quả không?
Question 2 of 4 Bạn huấn luyện một Transformer encoder–decoder và quên truyền mặt nạ cho self-attention của decoder. Loss huấn luyện về 0.0010.001. Cách đo nào phát hiện được lỗi này?
Question 3 of 4 Beam search kết thúc với hai giả thuyết. A có 3 token (tính cả <EOS>), tổng log-xác suất −1.8-1.8. B có 7 token, tổng log-xác suất −2.4-2.4. Dùng lp(y)=((5+∣y∣)/6)α\mathrm{lp}(\mathbf{y}) = ((5 + |\mathbf{y}|)/6)^{\alpha}. Câu nào được chọn khi α=0\alpha = 0 và khi α=1\alpha = 1?
Question 4 of 4 Một mô hình tóm tắt đạt độ chính xác từng token rất cao khi đo bằng teacher forcing trên tập kiểm định. Nhưng khi tự sinh, bản tóm tắt thường đúng ở đầu rồi lạc đề dần từ giữa đoạn. Giải thích nào hợp nhất?

Ý chính:

  • Seq2seq học p(y∣x)=∏ip(yi∣y<i,x)p(\mathbf{y} \mid \mathbf{x}) = \prod_i p(y_i \mid y_{<i}, \mathbf{x}): một mô hình ngôn ngữ có điều kiện theo câu nguồn, cho câu ra có độ dài và trật tự tự do, dừng khi sinh <EOS>.
  • Encoder–decoder RNN nén câu nguồn vào một vectơ cố định, và tụt điểm với câu dài. Attention cho mỗi bước một trung bình có trọng số riêng của câu nguồn. Trong Transformer, đó là cross-attention: Q\mathbf{Q} từ decoder, K\mathbf{K} và V\mathbf{V} từ encoder, ma trận điểm Ly×LxL_y \times L_x, không có mặt nạ nhân quả.
  • Huấn luyện bằng teacher forcing (đầu vào là đáp án dịch phải, một lần chạy cho mọi vị trí), dùng bằng sinh tự do. Khoảng cách giữa hai chế độ là exposure bias; luôn đánh giá bằng cách cho mô hình tự sinh.
  • Beam search giữ BB giả thuyết theo tổng log-xác suất, tốt hơn tham lam nhưng vẫn là xấp xỉ, và cần chia cho lp(y)=((5+∣y∣)/6)α\mathrm{lp}(\mathbf{y}) = ((5 + |\mathbf{y}|)/6)^{\alpha} để không thiên vị câu ngắn. T5 biến mọi tác vụ thành văn bản sang văn bản bằng tiền tố tác vụ.

Đọc thêm:

Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About