Họ GPT và fine-tune đa nhiệm
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Kể lại được con đường từ GPT-1 tới GPT-3: fine-tune riêng từng tác vụ, rồi nhắc tác vụ bằng văn bản (zero-shot, không ví dụ), rồi học trong ngữ cảnh (in-context learning). Phân biệt được cách nào đổi tham số, cách nào không.
- Hiểu instruction tuning (fine-tune theo chỉ dẫn): fine-tune có giám sát (supervised fine-tuning, SFT) trên nhiều tác vụ viết thành lời, như FLAN, T0 và bước đầu của InstructGPT.
- Đưa được năm tác vụ QA, sinh code, tóm tắt, truy hồi, dịch về cùng một dạng cặp văn bản vào, văn bản ra, và hiểu chat template dùng để làm gì.
- Tự dẫn được loss SFT chỉ tính trên phần trả lời. Đọc hiểu và viết được cách cài đặt nó bằng nhãn trong PyTorch.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Xây dựng LLM đầu tiên, thuộc phần Mô hình ngôn ngữ lớn (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Biết viết tiếp, chưa biết nghe lời
Phần tiêu đề “Biết viết tiếp, chưa biết nghe lời”Giả sử bạn có một GPT đã pre-train trên rất nhiều trang web, như GPT nhỏ ở bài trước nhưng lớn hơn rất nhiều. Bạn gõ:
Dịch sang tiếng Anh: Chào buổi sáng.
Mô hình chỉ biết một việc: viết tiếp sao cho giống văn bản nó từng đọc. Trên mạng, một dòng như thế thường nằm trong một bài tập có nhiều dòng giống nhau. Nên việc viết tiếp bằng “Dịch sang tiếng Anh: Hẹn gặp lại.” cũng hợp lý không kém việc trả lời “Good morning.”. Mô hình không sai. Nó chỉ đang làm đúng việc được dạy: viết tiếp văn bản, không phải trả lời người hỏi.
Từ 2018 tới 2022, các nhóm nghiên cứu thử ba cách để mô hình làm đúng việc mình muốn. Hình dung một người thợ đã đọc rất nhiều sách:
- Dạy riêng từng nghề (GPT-1, 2018). Mỗi tác vụ, bạn kèm người thợ trên vài nghìn ví dụ có đáp án. Kết quả là mỗi tác vụ một bản mô hình riêng.
- Dặn bằng lời, hoặc làm mẫu vài lần (GPT-2, 2019 và GPT-3, 2020). Không dạy thêm gì. Bạn viết yêu cầu, có thể kèm vài ví dụ, ngay trong văn bản đưa vào, rồi để mô hình viết tiếp.
- Tập cho quen nghe lời dặn (FLAN, T0, InstructGPT, 2021–2022). Dạy thêm một lần, trên hàng chục loại việc, mỗi loại dặn theo nhiều cách khác nhau. Lần sau gặp một lời dặn lạ, người thợ vẫn hiểu phải làm gì.
Cách thứ ba là bước đầu tiên khi huấn luyện InstructGPT và nhiều mô hình chat ra đời sau đó. Nó cần hai thứ mà bài này sẽ làm rõ: dữ liệu viết thế nào, và khi huấn luyện thì chấm điểm phần nào.
Bài toán của bài này là: từ một mô hình chỉ biết viết tiếp văn bản, làm sao có một mô hình làm theo yêu cầu viết bằng lời, cho nhiều loại tác vụ, kể cả loại nó chưa gặp khi huấn luyện? Và khi huấn luyện, phần nào của chuỗi được tính loss?
Lý thuyết
Phần tiêu đề “Lý thuyết”GPT-1: pre-train, rồi fine-tune từng tác vụ
Phần tiêu đề “GPT-1: pre-train, rồi fine-tune từng tác vụ”GPT-1 (Radford và cộng sự, 2018) có hai giai đoạn. Giai đoạn một là pre-train bằng dự đoán token kế tiếp trên BooksCorpus, hơn 7 000 cuốn sách. Kiến trúc cùng kiểu với GPT ở bài trước, nhưng lớn hơn: 12 khối, , ngữ cảnh 512 token. Giai đoạn hai là fine-tune có giám sát trên từng tác vụ, ví dụ phân loại hay suy diễn ngôn ngữ. Với mỗi tác vụ, họ thêm một lớp tuyến tính và softmax trên biểu diễn ở token cuối, để cho xác suất của nhãn .
Viết thành hàm mất mát, với đầu vào và nhãn :
Điểm đáng chú ý là họ cộng thêm một mục tiêu phụ (auxiliary objective): loss mô hình ngôn ngữ , tính trên chính văn bản đầu vào của tập fine-tune:
Paper viết công thức này dưới dạng log-likelihood cần làm lớn nhất (). Ở đây đổi dấu để có hàm mất mát cần làm nhỏ nhất, như ở bài trước. Tác giả báo mục tiêu phụ giúp tổng quát hóa tốt hơn và hội tụ nhanh hơn. Thí nghiệm bỏ nó đi cho thấy nó có ích ở tập dữ liệu lớn, còn ở tập nhỏ thì không.
Chi tiết thứ hai là cách GPT-1 đưa đầu vào có cấu trúc vào mô hình. Thay vì làm kiến trúc riêng cho mỗi tác vụ, họ xếp mọi thứ thành một chuỗi token, có token bắt đầu, token kết thúc và một token phân cách ở giữa. Với suy diễn ngôn ngữ, chuỗi là tiền đề, token phân cách, giả thuyết. Với hỏi đáp trắc nghiệm, mỗi đáp án được ghép với ngữ cảnh và câu hỏi thành một chuỗi riêng. Ý “mọi tác vụ thành một chuỗi” này sẽ quay lại ở FLAN và chat template.
GPT-1 cải thiện kết quả tốt nhất lúc đó trên 9 trong 12 tập dữ liệu. Nhưng mỗi tác vụ cần dữ liệu có nhãn riêng, một lớp đầu ra riêng và một bản mô hình đã fine-tune riêng.
GPT-2: ghi tác vụ ngay trong văn bản
Phần tiêu đề “GPT-2: ghi tác vụ ngay trong văn bản”GPT-2 (Radford và cộng sự, 2019) đặt câu hỏi: có cần fine-tune không? Học một tác vụ là ước lượng . Một hệ thống làm được nhiều tác vụ thì phải biết đang làm tác vụ nào, tức ước lượng
Điều kiện “tác vụ” không cần một kiến trúc riêng. Tác vụ, đầu vào và đầu ra đều là văn bản, nên có thể viết cả ba thành một chuỗi. Chính paper đưa ví dụ: một mẫu dịch là chuỗi (dịch sang tiếng Pháp, câu tiếng Anh, câu tiếng Pháp). Một mô hình ngôn ngữ đủ lớn, đọc đủ nhiều, có thể đã gặp những chuỗi như vậy trong văn bản tự nhiên.
Họ huấn luyện mô hình lớn nhất 1.5 tỉ tham số trên WebText, khoảng 40 GB văn bản, rồi đo ở chế độ zero-shot: không cập nhật tham số nào, chỉ viết tác vụ vào đầu vào. Vài cách nhắc tác vụ trong paper:
- Tóm tắt: thêm
TL;DR:sau bài báo rồi cho mô hình viết tiếp. Bỏ gợi ý này thì điểm tụt 6.4 điểm. Vậy một chuỗi chữ ngắn đã đủ gọi ra hành vi “tóm tắt”. - Dịch: đặt vài cặp “câu tiếng Anh = câu tiếng Pháp” vào ngữ cảnh, rồi “câu tiếng Anh =” và để mô hình viết tiếp.
- Hỏi đáp: đặt vài cặp hỏi đáp vào ngữ cảnh để mô hình bắt chước kiểu trả lời ngắn.
Paper GPT-2 vẫn gọi hai cách này là zero-shot vì không cập nhật tham số. GPT-3 về sau tách riêng: có ví dụ trong ngữ cảnh thì gọi là one-shot hay few-shot.
Kết quả đáng chú ý ở mô hình ngôn ngữ: tốt nhất lúc đó trên 7 trong 8 tập. Nhưng các tác vụ khác thì còn yếu, và chính tác giả nói rõ. Tóm tắt chỉ vừa nhỉnh hơn việc chọn ngẫu nhiên 3 câu trong bài theo điểm ROUGE. Dịch Anh sang Pháp được 5 BLEU. Hỏi đáp trả lời đúng 4.1% câu hỏi của Natural Questions. Tác giả viết rằng hiệu năng zero-shot của GPT-2 “còn xa” mức dùng được trong thực tế. Chỉ nhắc bằng lời là chưa đủ.
GPT-3: học trong ngữ cảnh
Phần tiêu đề “GPT-3: học trong ngữ cảnh”GPT-3 (Brown và cộng sự, 2020) giữ ý tưởng của GPT-2 nhưng tăng cỡ lên 175 tỉ tham số, ngữ cảnh 2048 token. Paper đo ba thiết lập, cả ba đều không cập nhật tham số:
- Zero-shot: chỉ có lời mô tả tác vụ, ví dụ “Dịch tiếng Anh sang tiếng Pháp:” rồi “cheese =>”.
- One-shot: thêm đúng một ví dụ, như “sea otter => loutre de mer”, trước dòng cần dịch.
- Few-shot: thêm vài ví dụ, thường 10 tới 100, tùy ngữ cảnh chứa được bao nhiêu.
Paper gọi khả năng làm theo các ví dụ trong ngữ cảnh là học trong ngữ cảnh. Bài Prompt Engineering đã dùng nó để viết prompt few-shot. Ở đây chỉ cần nhớ điểm khác với fine-tune: fine-tune đổi tham số, nên cái học được còn lại ở mọi lần gọi sau. Học trong ngữ cảnh không đổi gì trong mô hình. Prompt sau không có ví dụ thì mô hình quay về như cũ. Paper cũng nói họ không fine-tune GPT-3, và coi đó là hướng cho sau này.
Instruction tuning: SFT trên nhiều tác vụ
Phần tiêu đề “Instruction tuning: SFT trên nhiều tác vụ”GPT-3 cho thấy mô hình lớn làm được nhiều việc nếu prompt viết khéo. Bước tiếp theo đảo lại câu hỏi: thay vì chiều theo mô hình bằng prompt, hãy dạy mô hình hiểu lời dặn. Cách làm là fine-tune có giám sát, như GPT-1, nhưng trên nhiều tác vụ cùng lúc, mỗi tác vụ viết thành lời chỉ dẫn bằng nhiều cách. Gọi chung là instruction tuning. Mục tiêu không phải giỏi một tác vụ, mà là làm tốt cả những tác vụ chưa gặp khi huấn luyện.
FLAN (Wei và cộng sự, 2021) lấy một mô hình decoder-only 137 tỉ tham số đã pre-train, rồi fine-tune trên 62 tập dữ liệu NLP, chia thành 12 cụm theo loại tác vụ. Mỗi tập có 10 mẫu lời chỉ dẫn viết tay, vài mẫu còn đảo ngược tác vụ (ví dụ thay vì phân loại cảm xúc của bài đánh giá phim, yêu cầu viết một bài đánh giá phim). Cách đo “chưa gặp” rất chặt: muốn đo trên suy diễn ngôn ngữ thì bỏ cả cụm suy diễn ngôn ngữ ra khỏi dữ liệu huấn luyện. Kết quả: trên tác vụ chưa gặp, FLAN zero-shot hơn GPT-3 175 tỉ zero-shot ở 20 trong 25 tập. Paper cũng nêu hai giới hạn. Instruction tuning giúp ít với tác vụ vốn đã có dạng “viết tiếp câu”, như suy luận thường thức. Và với mô hình nhỏ, khoảng 8 tỉ tham số trở xuống, nó còn làm kết quả trên tác vụ chưa gặp tệ đi. Tác giả đoán là vì mô hình nhỏ dùng hết dung lượng cho các tác vụ huấn luyện.
T0 (Sanh và cộng sự, 2021, ICLR 2022) làm cùng ý với thiết kế khác: mô hình encoder-decoder T5 11 tỉ tham số, huấn luyện trên hỗn hợp nhiều tập có giám sát, mỗi tập viết lại bằng nhiều mẫu prompt lấy từ kho công khai P3. Tóm tắt của paper nêu T0 thường hơn những mô hình lớn gấp tới 16 lần trên một số tập chuẩn. Hai paper đo theo cách khác nhau, nên đừng so số giữa chúng. Điểm chung mới là bài học: nhiều tác vụ, mỗi tác vụ nhiều cách diễn đạt, tất cả là cặp văn bản vào, văn bản ra.
InstructGPT (Ouyang và cộng sự, 2022) đi thêm một bước. Bước đầu tiên của họ là SFT: fine-tune GPT-3 trên lời trình diễn (demonstration), tức câu trả lời mẫu do người gán nhãn viết, cho khoảng 13 nghìn prompt lấy từ API và do chính người gán nhãn viết. Prompt từ API phần lớn là việc mở: theo Bảng 1 của paper (thống kê trên bộ prompt API), sinh nội dung chiếm 45.6%, hỏi đáp mở 12.4%, brainstorming 11.2%, trong khi phân loại chỉ 3.5% và hỏi đáp đóng 2.6%. Một chi tiết đáng nhớ: họ huấn luyện 16 epoch, loss trên tập kiểm định bắt đầu tăng sau epoch đầu, nhưng huấn luyện thêm vẫn làm điểm đánh giá của người tốt lên. Họ chọn mô hình cuối theo điểm của một mô hình phần thưởng, không theo loss. Đây là quan sát trong thiết lập của họ, không phải luật chung, nhưng nó nhắc rằng loss kiểm định (bài Overfitting và Underfitting) chưa chắc đo đúng điều bạn quan tâm. Hai bước sau của InstructGPT, mô hình phần thưởng và học tăng cường, thuộc bài RLHF.
Về mặt toán, cả ba đều dùng chung một hàm mất mát với fine-tune một tác vụ. Chỗ khác nằm ở dữ liệu: trộn nhiều tác vụ và nhiều cách diễn đạt vào cùng một lần huấn luyện.
Định dạng dữ liệu: mọi tác vụ thành cặp văn bản
Phần tiêu đề “Định dạng dữ liệu: mọi tác vụ thành cặp văn bản”Để trộn được nhiều tác vụ, mọi mẫu phải cùng một dạng: một prompt (đầu vào, có lời chỉ dẫn) và một completion (đầu ra mong muốn). Bảng dưới đưa năm tác vụ về dạng đó. Đây là cách minh họa của bài. Riêng dòng truy hồi, bài tự đặt ra để cho thấy một tác vụ phân loại cũng viết được thành văn bản; truy hồi theo nghĩa tìm tài liệu bằng embedding là chuyện của bài RAG và chatbot hiện đại.
| Tác vụ | Prompt | Completion |
|---|---|---|
| Hỏi đáp (QA) | Đọc đoạn sau rồi trả lời. Đoạn: “Hà Nội là thủ đô của Việt Nam…” Câu hỏi: Thủ đô của Việt Nam là gì? | Hà Nội. |
| Sinh code | Viết hàm Python trả về bình phương của x. | def sq(x): return x * x |
| Tóm tắt | Tóm tắt trong một câu: “Trời mưa to nên trận bóng chiều nay bị hoãn…” | Trận bóng bị hoãn vì mưa. |
| Truy hồi | Đoạn văn sau có trả lời được câu hỏi “Khi nào trận bóng diễn ra?” không? Đoạn: ”…” Trả lời có hoặc không. | không |
| Dịch | Dịch sang tiếng Anh: Chào buổi sáng. | Good morning. |
Hai điều cần thêm khi làm dữ liệu thật. Một là viết nhiều mẫu lời chỉ dẫn cho mỗi tác vụ, như 10 mẫu mỗi tập của FLAN, để mô hình học ý của lời dặn chứ không học thuộc một câu cố định. Hai là chọn cấu trúc lưu. Thư viện TRL của Hugging Face nhận cặp {"prompt": ..., "completion": ...}, hoặc dạng hội thoại là một danh sách messages như ở API chat:
{ "messages": [ {"role": "system", "content": "Trả lời ngắn."}, {"role": "user", "content": "Dịch sang tiếng Anh: Chào buổi sáng."}, {"role": "assistant", "content": "Good morning."} ]}Chat template: biến hội thoại thành một chuỗi token
Phần tiêu đề “Chat template: biến hội thoại thành một chuỗi token”Mô hình chỉ đọc một dãy token. Danh sách messages phải được ghép thành một chuỗi, có các token điều khiển đánh dấu ai đang nói. Quy tắc ghép đó gọi là chat template. Template đồ chơi của bài viết mỗi lượt thành <|vai trò|> nội dung <|end|>:
<|system|> Trả lời ngắn. <|end|> <|user|> Dịch sang tiếng Anh: Chào buổi sáng. <|end|> <|assistant|> Good morning. <|end|>Ba điều cần nhớ:
- Mỗi mô hình có template riêng. Tài liệu Transformers đưa ví dụ hai bản fine-tune từ cùng một mô hình nền, một bản dùng
[INST] … [/INST], bản kia dùng<|user|>và<|assistant|>. Dùng template của mô hình này cho mô hình kia thì chất lượng tụt rõ. Trong thư việntransformers, hàmapply_chat_templatecủa tokenizer làm việc ghép này theo đúng template của mô hình. - Khi sinh thì mở sẵn lượt của assistant, khi huấn luyện thì không. Lúc dùng, chuỗi đưa vào kết thúc bằng
<|assistant|>để mô hình viết câu trả lời, thay vì viết tiếp lời người dùng. Trongapply_chat_template, đó là tham sốadd_generation_prompt=True. Lúc huấn luyện, chuỗi đã chứa sẵn cả câu trả lời, nên không thêm. - Token kết thúc lượt là cách mô hình biết dừng. Nếu mô hình chưa từng học sinh
<|end|>(hay token EOS, end-of-sequence, tùy template), nó sẽ viết mãi.
Chỉ tính loss trên phần trả lời
Phần tiêu đề “Chỉ tính loss trên phần trả lời”Một mẫu SFT là prompt ghép với đáp án thành một chuỗi . Ta muốn mô hình học : đọc prompt, viết đáp án. Prompt thì lúc dùng thật luôn có sẵn, do người dùng gõ.
Vậy có nên tính loss ở các vị trí thuộc prompt không? TRL mặc định là không với dữ liệu dạng prompt-completion. Lý do sau đây là lập luận của bài, tài liệu TRL chỉ nêu mặc định. Thứ nhất, đoán lại lời người dùng không phải việc ta cần. Thứ hai, prompt thường dài hơn đáp án nhiều: một bài báo 950 token kèm bản tóm tắt 50 token thì đáp án chỉ chiếm khoảng 5% số vị trí, và loss trung bình gần như chỉ đo việc đoán bài báo. Đây là một lựa chọn thiết kế, không phải luật: GPT-1 cố ý cộng loss trên văn bản đầu vào và thấy có lợi ở tập lớn, và TRL cũng cho bật tính loss trên cả chuỗi. Riêng dạng messages thì ngược lại: TRL mặc định tính loss trên cả chuỗi. Muốn chỉ chấm lời assistant thì đặt assistant_only_loss=True, và template phải có cặp từ khóa {% generation %} và {% endgeneration %} bao quanh lời của assistant.
Gọi là tập các vị trí thuộc đáp án, kể cả token kết thúc. Loss SFT là:
Đọc từng phần:
- đúng là số hạng của loss pre-train ở bài trước. Mô hình vẫn đọc cả chuỗi, kể cả prompt, nên chứa toàn bộ prompt.
- chỉ khác pre-train ở chỗ tổng chạy trên các vị trí của đáp án, không phải mọi vị trí.
- Chia , số vị trí được tính, để loss là trung bình trên token đáp án, không phụ thuộc prompt dài hay ngắn. Với một batch, gồm mọi vị trí đáp án của mọi mẫu, nên mẫu có đáp án dài đóng góp nhiều hơn. Tài liệu TRL viết công thức dạng tổng, còn log của trình huấn luyện ghi trung bình trên các token không bị che. Bài chọn dạng trung bình.
Cài đặt bằng nhãn . Trong PyTorch, ta tạo mảng labels cùng cỡ với chuỗi token. Ở vị trí thuộc đáp án, nhãn là chính token đó. Ở vị trí thuộc prompt và phần đệm (padding, token thêm vào cuối để các chuỗi trong một batch dài bằng nhau), nhãn là . Hàm F.cross_entropy có tham số ignore_index, mặc định là : vị trí có nhãn này bị bỏ khỏi cả tổng lẫn mẫu số (với reduction='mean', mặc định), đúng như . Còn một bước nữa, giống bài trước: dịch một vị trí, vì logit ở vị trí đoán token ở vị trí . Ba dòng này lấy từ ô PyTorch ở mục Ví dụ code:
z = logits[:, :-1].reshape(-1, V)y = labels[:, 1:].reshape(-1)keep = y != -100logits[:, :-1] bỏ logit ở vị trí cuối (không còn token nào sau nó để đoán), labels[:, 1:] bỏ nhãn đầu tiên (không có vị trí nào trước nó). Sau đó F.cross_entropy(z, y) cho đúng , với là mọi vị trí đáp án của cả batch.
Đừng nhầm nhãn với mặt nạ nhân quả ở bài trước. Mặt nạ nhân quả che đầu vào của attention: vị trí không được nhìn về sau. Nhãn che đầu ra của loss: vị trí đó vẫn được đọc, chỉ không bị chấm điểm. Giống chấm bài kiểm tra: học sinh đọc đề, nhưng chỉ phần học sinh viết mới được chấm.
Giới hạn
Phần tiêu đề “Giới hạn”- SFT chỉ bắt chước đáp án mẫu. Nó không cho mô hình biết câu trả lời nào tốt hơn câu nào, chỉ đẩy xác suất về phía câu trả lời mẫu. Thêm tín hiệu ưa thích của người là việc của bài RLHF.
- Instruction tuning dạy cách làm theo yêu cầu, không chắc thêm được kiến thức. FLAN giúp ít với tác vụ dạng viết tiếp câu, và còn làm hại mô hình nhỏ. Đừng kỳ vọng một lần SFT biến mô hình yếu thành mạnh.
- Fine-tune toàn bộ tham số tốn bộ nhớ GPU. Cách chỉ học một phần nhỏ tham số, như LoRA, ở bài Đánh giá mô hình và PEFT.
Minh họa
Phần tiêu đề “Minh họa”Bảng dưới đặt bốn cách làm cạnh nhau. Hãy nhìn cột thứ hai: chỉ dòng đầu và dòng cuối là có cập nhật tham số. Dòng cuối dạy một lần cho nhiều tác vụ, còn dòng đầu dạy lại cho mỗi tác vụ. Hai dòng giữa không dạy gì thêm, chỉ đổi văn bản đầu vào.
| Cách làm | Cập nhật tham số cho tác vụ? | Dữ liệu có nhãn | Cách báo cho mô hình biết tác vụ |
|---|---|---|---|
| GPT-1 (2018): fine-tune từng tác vụ | Có, mỗi tác vụ một bản mô hình | Hàng nghìn tới hàng trăm nghìn mẫu cho mỗi tác vụ | Lớp đầu ra riêng, đầu vào xếp thành chuỗi có token phân cách |
| GPT-2 (2019): zero-shot | Không | Không cần (dịch, hỏi đáp có kèm vài cặp ví dụ) | Gợi ý bằng chữ trong đầu vào, như TL;DR: |
| GPT-3 (2020): one-shot, few-shot | Không | Vài ví dụ, đặt trong ngữ cảnh | Lời mô tả và ví dụ trong ngữ cảnh |
| FLAN, T0, bước SFT của InstructGPT (2021–2022) | Có, một lần cho nhiều tác vụ | Nhiều tập dữ liệu, viết bằng nhiều mẫu lời chỉ dẫn; hoặc câu trả lời mẫu do người viết | Lời chỉ dẫn bằng ngôn ngữ tự nhiên, kể cả cho tác vụ chưa gặp |
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 10/2026: ô chat template với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), ô loss có mask với Python 3.14 và PyTorch 2.14 trên CPU. Ô đầu chạy ngay trên trang. Ô thứ hai cần PyTorch nên chạy trên máy bạn. Hai ô chạy độc lập.
Chat template đồ chơi và nhãn −100
Phần tiêu đề “Chat template đồ chơi và nhãn −100”Ô dưới đây dựng template đồ chơi của mục Lý thuyết. Hàm render ghép messages thành danh sách token (tách theo khoảng trắng cho dễ đọc) và đánh dấu token nào thuộc lời của assistant. Ô in chuỗi dùng khi sinh, rồi in từng vị trí khi huấn luyện: token đang đọc, và nhãn mà vị trí đó phải đoán.
Chat template đồ chơi và nhãn -100
# Chat template đồ chơi và nhãn -100: chỉ chấm lời của assistantimport numpy as np
messages = [ {"role": "system", "content": "Trả lời ngắn."}, {"role": "user", "content": "Thủ đô của Pháp là gì?"}, {"role": "assistant", "content": "Đó là Paris."},]
def render(msgs, add_generation_prompt=False): # Mỗi lượt: <|vai|> các từ <|end|>. Token tách theo khoảng trắng toks, scored = [], [] for m in msgs: body = m["content"].split() + ["<|end|>"] toks += [f"<|{m['role']}|>"] + body scored += [False] + [m["role"] == "assistant"] * len(body) if add_generation_prompt: # khi sinh: mở sẵn lượt của assistant toks.append("<|assistant|>") scored.append(False) return toks, scored
print(" ".join(render(messages[:2], add_generation_prompt=True)[0]))print()
toks, scored = render(messages) # khi huấn luyện: cả câu trả lờivocab = {t: i for i, t in enumerate(dict.fromkeys(toks))}ids = np.array([vocab[t] for t in toks])labels = np.where(scored, ids, -100) # -100: không tính loss
# Dịch một vị trí: đọc token t, đoán token t + 1for t in range(len(toks) - 1): target = toks[t + 1] if labels[t + 1] != -100 else "-100" print(f"{toks[t]:>14} -> {target}")print(f"scored: {(labels[1:] != -100).sum()} / {len(toks) - 1}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- Dòng đầu là chuỗi đưa vào khi sinh: chỉ có
systemvàuser, kết thúc bằng<|assistant|>nhờadd_generation_prompt=True. Mô hình sẽ viết tiếp từ đó. - Bảng là chuỗi khi huấn luyện, đã dịch một vị trí. Mọi vị trí đọc
systemvàusercó nhãn . Vị trí đọc<|assistant|>phải đoán “Đó”, rồi “là”, “Paris.”, và cuối cùng là<|end|>. scored: 4 / 17: chỉ 4 trong 17 vị trí được tính loss. Vị trí cuối cùng được chấm là vị trí phải sinh<|end|>, nên mô hình học được lúc nào dừng.
Loss có mask trên một batch ba tác vụ
Phần tiêu đề “Loss có mask trên một batch ba tác vụ”Ô này làm đúng việc của một bước SFT trên một batch nhỏ, bằng PyTorch. Ba cặp phỏng theo bảng định dạng dữ liệu: dịch, tóm tắt, sinh code. Mỗi byte UTF-8 là một token, thêm một token EOS, nên . Mỗi chuỗi là prompt, một dấu xuống dòng, đáp án, rồi EOS. Chuỗi ngắn được đệm bằng EOS cho dài bằng chuỗi dài nhất. Mô hình là một lớp embedding và một lớp tuyến tính, chưa huấn luyện: ô chỉ kiểm cách tính loss, không huấn luyện gì.
Chạy trên máy bạn
Loss chỉ trên phần trả lời, batch ba tác vụ
- Cần cài
pip install torch- Phần cứng
- CPU là đủ, chạy trong vài giây
# Loss chỉ trên phần trả lời: một batch ba tác vụ, nhãn -100import math
import torchimport torch.nn.functional as F
torch.manual_seed(0)EOS, V = 256, 257 # mỗi byte (0-255) là một token, thêm một token EOSPAIRS = [ # (prompt, đáp án): dịch, tóm tắt, sinh code ("Dịch sang tiếng Anh: Chào buổi sáng.", "Good morning."), ("Tóm tắt: Trời mưa to nên trận bóng chiều nay bị hoãn.", "Trận bóng bị hoãn vì mưa."), ("Viết hàm Python trả về bình phương của x.", "def sq(x):\n return x * x"),]
def encode(prompt, answer): p = list((prompt + "\n").encode("utf-8")) a = list(answer.encode("utf-8")) + [EOS] # giữ EOS: mô hình học dừng return p + a, [-100] * len(p) + a # nhãn của prompt là -100
rows = [encode(p, a) for p, a in PAIRS]n = max(len(ids) for ids, _ in rows)# Đệm cho bằng chuỗi dài nhất: token đệm là EOS, nhãn đệm là -100ids = torch.tensor([r + [EOS] * (n - len(r)) for r, _ in rows])labels = torch.tensor([lb + [-100] * (n - len(lb)) for _, lb in rows])
# Mô hình giả, chưa huấn luyện: embedding rồi một lớp tuyến tínhmodel = torch.nn.Sequential(torch.nn.Embedding(V, 32), torch.nn.Linear(32, V))logits = model(ids) # (B, n, V)
# Dịch một vị trí: logit ở vị trí t dự đoán token ở vị trí t + 1z = logits[:, :-1].reshape(-1, V)y = labels[:, 1:].reshape(-1)keep = y != -100print(f"shape: {tuple(ids.shape)}")print(f"counted per row: {keep.view(len(PAIRS), -1).sum(1).tolist()}")print(f"counted: {keep.sum()} / {y.numel()}")
loss = F.cross_entropy(z, y) # ignore_index=-100 là mặc địnhby_hand = F.cross_entropy(z[keep], y[keep]) # tự lọc rồi lấy trung bìnhprint(f"loss: {loss:.4f}, by hand: {by_hand:.4f}, ln V: {math.log(V):.4f}")
# Sai lầm: che theo giá trị token đệm. EOS thật của đáp án cũng bị chewrong = labels.masked_fill(ids == EOS, -100)[:, 1:].reshape(-1)print(f"mask by pad id: {(wrong != -100).sum()} counted")print(f"all -100: {F.cross_entropy(z, torch.full_like(y, -100))}")Kết quả mẫu
Lấy từ một lần chạy thật.
shape: (3, 103)counted per row: [14, 34, 28]counted: 76 / 306loss: 5.7787, by hand: 5.7787, ln V: 5.5491mask by pad id: 73 countedall -100: nanĐọc kết quả:
shape: (3, 103): 3 chuỗi, cùng dài 103 token sau khi đệm. Chuỗi dài nhất là cặp tóm tắt: 69 byte prompt (kể cả dấu xuống dòng) và 34 token đáp án.counted per row: [14, 34, 28]: mỗi hàng được tính đúng số byte của đáp án cộng 1 cho EOS. “Good morning.” có 13 byte. “Trận bóng bị hoãn vì mưa.” có 25 ký tự nhưng 33 byte, vì mỗi chữ có dấu chiếm 2 hoặc 3 byte. Đoạn code có 27 byte.counted: 76 / 306: sau khi dịch một vị trí, batch có vị trí, nhưng chỉ 76, khoảng 25%, được tính loss. Phần còn lại là prompt và phần đệm.lossbằngby hand:ignore_indexcho đúng kết quả của việc tự lọc các vị trí có nhãn rồi lấy trung bình trên cả 76 vị trí của batch, nên hàng tóm tắt (34 vị trí) nặng hơn hàng dịch (14 vị trí). Loss gần vì mô hình chưa học gì và gần như đoán đều. Đừng đọc gì thêm từ con số loss này: nó không cho biết che prompt tốt hơn hay kém hơn. Muốn biết thì phải huấn luyện thật và đánh giá câu trả lời.mask by pad id: 73: nếu che theo giá trị token (ids == EOS) thay vì theo vị trí, cả 3 EOS thật của đáp án cũng bị che, còn 73 thay vì 76. Hộp thứ hai ở mục Lỗi hay gặp nói hậu quả.all -100: nan: khi mọi nhãn trong batch là , mẫu số bằng 0 và loss lànan.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- GPT-1 (2018) pre-train rồi fine-tune riêng từng tác vụ, kèm mục tiêu phụ là loss mô hình ngôn ngữ. GPT-2 (2019) viết tác vụ vào đầu vào, mô hình hóa , nhưng zero-shot còn yếu. GPT-3 (2020) học trong ngữ cảnh từ vài ví dụ, không đổi tham số.
- Instruction tuning fine-tune một lần trên nhiều tác vụ, mỗi tác vụ nhiều cách diễn đạt (FLAN, T0), hoặc trên câu trả lời mẫu do người viết (bước SFT của InstructGPT), để mô hình làm theo cả lời dặn chưa gặp. Nó không thêm kiến thức và có thể làm hại mô hình nhỏ.
- Mọi tác vụ thành cặp prompt và completion. Chat template ghép hội thoại thành một chuỗi có token điều khiển, riêng cho từng mô hình. Khi sinh thì mở sẵn lượt của assistant.
- Loss SFT chỉ tính trên đáp án: nhãn của prompt và phần đệm là , giữ nhãn của token kết thúc, dịch một vị trí.
Đọc thêm:
- Radford, A., Narasimhan, K., Salimans, T., Sutskever, I. (2018). Improving Language Understanding by Generative Pre-Training (PDF), báo cáo kỹ thuật của OpenAI, mục 3. GPT-1: fine-tune có mục tiêu phụ, và cách xếp đầu vào có cấu trúc thành một chuỗi.
- Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., Sutskever, I. (2019). Language Models are Unsupervised Multitask Learners (PDF), báo cáo kỹ thuật của OpenAI, mục 2 và 3.5–3.8. GPT-2: ý , cách nhắc tóm tắt, dịch, hỏi đáp bằng chữ, và các giới hạn tác giả tự nêu.
- Brown, T. B., Mann, B., Ryder, N., và cộng sự (2020). Language Models are Few-Shot Learners, arXiv:2005.14165, mục 2. GPT-3: ba thiết lập zero-shot, one-shot, few-shot, so với fine-tune.
- Wei, J., Bosma, M., Zhao, V. Y., và cộng sự (2021). Finetuned Language Models Are Zero-Shot Learners, arXiv:2109.01652. FLAN: instruction tuning, cách bỏ cả cụm tác vụ khi đo, và các giới hạn.
- Sanh, V., Webson, A., Raffel, C., Bach, S. H., và cộng sự (2021). Multitask Prompted Training Enables Zero-Shot Task Generalization, arXiv:2110.08207, ICLR 2022. T0 và kho prompt P3.
- Ouyang, L., Wu, J., Jiang, X., và cộng sự (2022). Training language models to follow instructions with human feedback, arXiv:2203.02155, mục 3. InstructGPT: dữ liệu SFT, Bảng 1, và quan sát về loss kiểm định. Phần còn lại dành cho bài RLHF.
- Hugging Face. Chat templates (Transformers 5.20.2) và SFT Trainer (TRL 1.16.0), tài liệu chính thức, đọc tháng 10/2026.
apply_chat_template,add_generation_prompt, các dạng bộ dữ liệu và các tùy chọn tính loss chỉ trên phần trả lời.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu