BERT
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu vì sao cần embedding theo ngữ cảnh (contextual embedding), và vì sao BERT là một Transformer encoder-only (chỉ có encoder) đọc cả hai phía của câu.
- Giải thích được hai nhiệm vụ pre-train: mô hình ngôn ngữ có che (masked language model, MLM) với quy tắc 15% và 80/10/10, và dự đoán câu kế tiếp (next sentence prediction, NSP).
- Biết cách fine-tune BERT cho ba kiểu đầu ra: phân loại cả chuỗi từ
[CLS], gán nhãn từng token, và dự đoán một đoạn (span) trong văn bản. - Cài đặt được bước che token bằng NumPy, và dùng được
pipeline("fill-mask")để xem BERT điền chỗ trống.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Word2Vec (CBOW, Skip-gram), GloVe, FastText, thuộc phần Xử lý ngôn ngữ tự nhiên (Đã học)
- Transformers, thuộc phần Học sâu (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Một chữ, nhiều nghĩa
Phần tiêu đề “Một chữ, nhiều nghĩa”Đọc ba câu sau:
| Câu | “bay” nghĩa là |
|---|---|
| Con chim bay qua sông. | chuyển động trên không |
| Máy bay hạ cánh lúc tám giờ. | một phần của tên phương tiện |
| Anh thợ cầm cái bay trát vữa. | dụng cụ của thợ xây |
Ở bài Word2Vec, GloVe, FastText, mỗi token có đúng một dòng trong bảng embedding. Cả ba chữ “bay” ở trên đều tra cùng một dòng, nên nhận cùng một vectơ. Vectơ đó là một kiểu trung bình của mọi nghĩa mà “bay” từng có trong dữ liệu. Ta gọi đó là embedding tĩnh (static embedding).
Người đọc thì không bao giờ hiểu “bay” một mình. Ta hiểu nó nhờ các chữ xung quanh. Điều ta muốn là một vectơ cho mỗi lần xuất hiện, tính từ cả câu.
Đọc cả hai phía
Phần tiêu đề “Đọc cả hai phía”Che một chữ và đoán thử: “Hà Nội là ___ đô của Việt Nam.”
Nếu chỉ đọc bên trái, “Hà Nội là ___” có thể là “một”, “nơi”, “thành”… Chữ “đô” ở bên phải mới làm đáp án gần như chắc chắn là “thủ”. Nhiều chữ cần ngữ cảnh phía sau: “máy” trong “máy bay” hay “máy giặt”, “bàn” trong “bàn tay” hay “bàn học”.
GPT đọc từ trái sang phải: khi tính vectơ cho một vị trí, nó chỉ thấy các token đứng trước. Như vậy là đúng cho việc sinh văn bản, vì lúc sinh, phần bên phải chưa tồn tại. Nhưng với việc hiểu một câu đã có sẵn (phân loại nhận xét, tìm tên người, trả lời câu hỏi dựa trên đoạn văn), bỏ phí nửa bên phải là thiệt.
Nhưng nhìn hai phía thì học bằng gì?
Phần tiêu đề “Nhưng nhìn hai phía thì học bằng gì?”GPT học bằng cách đoán token kế tiếp. Nếu mỗi vị trí đã nhìn thấy cả câu, cách học này hỏng: token kế tiếp nằm ngay trong tầm nhìn, mô hình chỉ cần chép.
Lời giải của BERT giống bài tập điền từ ở trường: giấu đi vài chữ, rồi bắt mô hình đoán lại từ ngữ cảnh hai bên. Devlin và cộng sự (2019) gọi đây là mô hình ngôn ngữ có che, và ghi rằng nó lấy cảm hứng từ dạng bài điền chỗ trống (cloze) của Taylor (1953).
Bài toán của bài này là: học một mô hình cho mỗi token một vectơ phụ thuộc cả câu, từ văn bản thô không có nhãn, bằng cách che bớt token rồi đoán lại; sau đó dùng mô hình đó làm điểm khởi đầu cho các bài toán có nhãn.
Lý thuyết
Phần tiêu đề “Lý thuyết”Kiến trúc: một chồng khối encoder
Phần tiêu đề “Kiến trúc: một chồng khối encoder”BERT (Bidirectional Encoder Representations from Transformers) là phần encoder của Transformer (Vaswani và cộng sự, 2017), không có decoder, không có cross-attention. Self-attention không có mặt nạ nhân quả: mỗi vị trí nhìn mọi vị trí trong chuỗi, ở mọi lớp. Bài báo gọi số lớp là , số chiều ẩn là , số đầu là ; bài này giữ quy ước của phần LLM: là số chiều ẩn, là số đầu, mỗi đầu có chiều.
| Thông số | BERT-base | BERT-large |
|---|---|---|
| Số khối encoder | 12 | 24 |
| Số chiều ẩn | 768 | 1024 |
| Số đầu attention | 12 | 16 |
| Số chiều mỗi đầu | 64 | 64 |
| Số tham số (bài báo, làm tròn) | 110 triệu | 340 triệu |
Mạng truyền thẳng trong mỗi khối có chiều ẩn (3072 với BERT-base), hàm kích hoạt GELU. Tự đếm cho BERT-base với từ vựng 30 522 token (tệp từ vựng thật; bài báo ghi tròn 30 000):
- Embedding: cho token, vị trí và đoạn, cộng chuẩn hóa lớp: khoảng 23.8 triệu.
- Một khối encoder: 4 ma trận của attention (cả hệ số tự do) là 2.36 triệu; FFN là 4.72 triệu; cộng hai lớp chuẩn hóa (3 072 số), tổng khoảng 7.09 triệu. Nhân 12 khối được 85.1 triệu.
- Lớp pooler trên
[CLS](một lớp , dùng cho NSP): khoảng 0.59 triệu.
Tổng là 109 482 240, đúng cỡ “110 triệu” của bài báo. Đếm cùng cách cho BERT-large được khoảng 335.1 triệu; bài báo ALBERT ghi 334 triệu, còn bài báo BERT làm tròn thành 340 triệu. Phần lớn tham số nằm trong các khối encoder; bảng embedding chiếm khoảng một phần năm.
Đầu vào: token, đoạn và vị trí
Phần tiêu đề “Đầu vào: token, đoạn và vị trí”BERT nhận một hoặc hai đoạn văn bản, gọi là câu A và câu B (thật ra là hai đoạn, có thể dài hơn một câu). Văn bản được tách bằng WordPiece (bài Tách từ), rồi thêm hai token đặc biệt:
[CLS]luôn đứng đầu. Nó là một token lớp (class token): vectơ của nó ở lớp cuối được dùng làm đại diện cho cả chuỗi, khi cần một nhãn cho cả chuỗi.[SEP]đứng cuối mỗi đoạn, để phân tách A và B.
Ví dụ cặp câu “trời mưa” và “đường ngập”, giả sử mỗi âm tiết là một token:
| Vị trí | Token | Đoạn |
|---|---|---|
| 0 | [CLS] | A |
| 1 | trời | A |
| 2 | mưa | A |
| 3 | [SEP] | A |
| 4 | đường | B |
| 5 | ngập | B |
| 6 | [SEP] | B |
Vectơ đầu vào của vị trí là tổng của ba embedding:
- có hàng, mỗi token một hàng; là số thứ tự của token ở vị trí . Đây chính là bảng embedding tĩnh.
- chỉ có 2 hàng: một cho A, một cho B. Nó là embedding đoạn (segment embedding), cho mô hình biết token thuộc câu nào ngoài dấu
[SEP]. - có 512 hàng, học được, nên BERT đọc tối đa 512 token mỗi chuỗi.
Mỗi bảng có cột. Sau 12 khối encoder, vị trí có vectơ (bài báo viết , và cho vectơ của [CLS]). chỉ biết token ; đã trộn thông tin của cả chuỗi qua 12 lần self-attention. chính là embedding theo ngữ cảnh mà phần Trực giác cần. Lưu ý ký hiệu: ở bài Word2Vec, là số chiều của embedding tĩnh; ở bài này, là số chiều của các vectơ ẩn (768), còn là số chiều của một đầu attention.
Mô hình ngôn ngữ có che (MLM)
Phần tiêu đề “Mô hình ngôn ngữ có che (MLM)”Bước 1: chọn vị trí. Với mỗi chuỗi, chọn ngẫu nhiên 15% số vị trí (không chọn [CLS], [SEP]). Gọi tập vị trí được chọn là . Mã gốc của Google chọn khoảng đúng 15% số vị trí mỗi chuỗi; thư viện transformers chọn từng token độc lập với xác suất 0.15, nên số vị trí dao động quanh 15%.
Bước 2: làm hỏng đầu vào. Với mỗi , tung một đồng xu ba mặt:
- 80%: thay token bằng
[MASK]; - 10%: thay bằng một token ngẫu nhiên trong từ vựng;
- 10%: giữ nguyên.
Gọi chuỗi sau khi làm hỏng là .
Bước 3: đoán lại token gốc ở các vị trí được chọn. Cho qua encoder, được . Một đầu MLM (một lớp nhỏ rồi một lớp tuyến tính ra logit) cộng softmax cho phân phối trên từ vựng ở vị trí . Hàm mất mát là cross-entropy, chỉ lấy trung bình trên :
Ở đây là token gốc, không phải token trong . Các vị trí ngoài không góp gì vào loss: mô hình không bị hỏi về chúng.
So với GPT: GPT tính loss ở mọi vị trí, mỗi vị trí có điều kiện là phần bên trái. BERT chỉ tính ở khoảng 15% vị trí, nhưng mỗi vị trí có điều kiện là cả chuỗi (đã bị làm hỏng). Mỗi câu vì vậy cho ít tín hiệu học hơn. Bài báo ghi nhận MLM hội tụ chậm hơn mô hình trái sang phải một chút, nhưng vượt nó về độ chính xác rất sớm.
Vì sao không thay hết bằng [MASK]? Token [MASK] chỉ có trong lúc pre-train. Khi fine-tune và khi dùng, đầu vào là văn bản thật, không có [MASK] nào. Nếu luôn che bằng [MASK], mô hình học được một mẹo: chỉ chỗ nào có [MASK] mới cần đoán, còn token thật thì cứ tin. Hai nhánh 10% phá mẹo này:
- Nhánh ngẫu nhiên: một token thật cũng có thể là token sai, nên mô hình phải kiểm tra mọi token bằng ngữ cảnh.
- Nhánh giữ nguyên: đôi khi đáp án chính là token đang thấy, nên biểu diễn của token thật được kéo về phía nghĩa thật của nó.
Mô hình không biết vị trí nào đang bị hỏi, nên phải giữ một biểu diễn tốt cho mọi token. Tính trên toàn bộ token, thấy [MASK], thấy token ngẫu nhiên, được giữ nguyên nhưng vẫn bị hỏi. Bài báo cho rằng 1.5% token ngẫu nhiên đủ ít để không làm hại khả năng hiểu ngôn ngữ.
Phụ lục của bài báo có thí nghiệm đổi tỉ lệ. Khi fine-tune, kết quả gần như không đổi (MNLI 84.2 với 80/10/10, 84.3 với 100% [MASK]). Nhưng khi giữ BERT cố định và chỉ lấy vectơ làm đặc trưng cho bài nhận dạng thực thể, 100% [MASK] cho F1 94.0, kém 80/10/10 (94.9). Đúng chỗ mà độ lệch giữa pre-train và lúc dùng bị phóng to.
Dự đoán câu kế tiếp (NSP)
Phần tiêu đề “Dự đoán câu kế tiếp (NSP)”Nhiều bài toán xét quan hệ giữa hai câu: câu B có suy ra từ câu A không, đoạn văn có chứa câu trả lời cho câu hỏi không. MLM không dạy trực tiếp điều này. BERT thêm nhiệm vụ thứ hai: lấy câu A, rồi 50% lấy B là đoạn thật sự đi sau A trong văn bản (nhãn IsNext), 50% lấy B là một đoạn ngẫu nhiên từ chỗ khác (nhãn NotNext). Vectơ đi qua một lớp phân loại hai lớp. Loss pre-train là tổng của loss MLM và loss NSP.
Trong thí nghiệm của bài báo, bỏ NSP làm điểm tụt trên QNLI (88.4 xuống 84.9) và vài tác vụ khác. Nhưng RoBERTa (Liu và cộng sự, 2019) huấn luyện lại BERT cẩn thận hơn và thấy ngược lại: bỏ NSP, thay bằng các chuỗi liền mạch dài tới 512 token, cho kết quả ngang hoặc nhỉnh hơn. Họ cho rằng bản thí nghiệm gốc có thể chỉ bỏ loss NSP mà vẫn giữ định dạng cặp đoạn A/B. ALBERT (Lan và cộng sự, 2020) giải thích thêm: một đoạn ngẫu nhiên thường khác chủ đề với A, nên NSP dễ đoán chỉ nhờ chủ đề. ALBERT thay NSP bằng dự đoán thứ tự câu (sentence-order prediction, SOP): hai đoạn liền nhau, đúng thứ tự hoặc bị đảo.
Fine-tune: ba kiểu đầu ra
Phần tiêu đề “Fine-tune: ba kiểu đầu ra”Sau pre-train, ta có cho mọi token của một chuỗi dài . Fine-tune thêm một lớp nhỏ phía trên rồi huấn luyện toàn bộ mô hình (cả BERT lẫn lớp mới) trên dữ liệu có nhãn, thường chỉ vài epoch. Có ba kiểu:
1. Một nhãn cho cả chuỗi. Phân loại cảm xúc, phát hiện thư rác, hay xét quan hệ của một cặp câu. Với lớp, chỉ thêm ma trận :
Vì sao lấy [CLS]? Nó không mang nghĩa riêng nào, ở mọi lớp nó nhìn được mọi token, và NSP đã tập cho nó tóm tắt cả chuỗi. Bài Phân tích cảm xúc dùng kiểu này.
2. Một nhãn cho mỗi token. Nhận dạng thực thể: “Nguyễn Du quê ở Hà Tĩnh” cần nhãn người cho “Nguyễn Du”, nhãn địa danh cho “Hà Tĩnh”. Cùng một lớp áp lên từng . Một từ bị WordPiece chia thành nhiều mảnh thì bài báo chỉ đưa vectơ của mảnh đầu vào lớp phân loại. Chi tiết ở bài Nhận dạng thực thể.
3. Một đoạn trong văn bản. Hỏi đáp trích xuất: câu hỏi là A, đoạn văn là B, đáp án là một đoạn liền của B. Chỉ thêm hai vectơ . Xác suất token là điểm đầu của đáp án:
với tổng lấy trên các token của đoạn văn; điểm cuối tính giống vậy bằng . Khi dùng, chọn cặp với có lớn nhất. Chi tiết ở bài Hỏi đáp.
Điểm chung: phần lớn tri thức nằm trong BERT đã pre-train, phần thêm vào rất nhỏ ( hay số). Đó là lý do một mô hình pre-train một lần dùng lại được cho nhiều bài toán. Khi ra mắt (cuối 2018), BERT-large đạt 80.5 điểm GLUE (hơn kết quả tốt nhất trước đó 7.7 điểm) và F1 93.2 trên SQuAD v1.1.
Họ BERT
Phần tiêu đề “Họ BERT”Sau BERT có nhiều biến thể, mỗi cái sửa một điểm:
| Mô hình | Năm, tác giả | Thay đổi chính |
|---|---|---|
| RoBERTa | 2019, Liu và cộng sự | Bỏ NSP; che động (chọn lại vị trí che mỗi lần chuỗi được đưa vào, thay vì cố định từ trước); lô lớn hơn, huấn luyện lâu hơn, hơn 160 GB văn bản; BPE theo byte |
| ALBERT | 2020, Lan và cộng sự | Tách bảng embedding thành hai ma trận nhỏ ( rồi , với nhỏ hơn ); dùng chung tham số giữa mọi lớp; thay NSP bằng SOP. ALBERT-large có 18 triệu tham số, so với 334 triệu của BERT-large (theo cách đếm của bài báo ALBERT). Ít tham số nhưng không ít phép tính, vì mỗi lớp vẫn phải chạy đủ; trong bảng so sánh của bài báo, bản large kém BERT-large (điểm trung bình 82.4 so với 85.2); bản xlarge (60 triệu) chỉ nhỉnh hơn (85.5) nhưng chậm hơn; bản xxlarge (235 triệu) vượt rõ (88.7) nhưng chạy chậm khoảng 3 lần |
| DistilBERT | 2019, Sanh và cộng sự | Chắt lọc tri thức (knowledge distillation) từ BERT-base: 6 lớp thay vì 12, khoảng 66 triệu tham số, nhỏ hơn 40%, nhanh hơn 60%, giữ khoảng 97% khả năng hiểu ngôn ngữ (đo trên GLUE) |
| ELECTRA | 2020, Clark và cộng sự | Một mô hình nhỏ (generator) thay vài token bằng token hợp lý; mô hình chính (discriminator) đoán từng token là gốc hay bị thay. Học từ mọi token chứ không chỉ 15%, và không có [MASK] trong đầu vào của mô hình chính |
BERT cho tiếng Việt
Phần tiêu đề “BERT cho tiếng Việt”Có hai hướng:
- Mô hình đa ngôn ngữ. mBERT (BERT-Base Multilingual Cased của Google) học trên Wikipedia của 104 ngôn ngữ, với một từ vựng WordPiece chung khoảng 119 nghìn token. Tự đếm từ tệp cấu hình: khoảng 178 triệu tham số, hơn một nửa (91.8 triệu) nằm ở bảng embedding token. XLM-R (Conneau và cộng sự, 2020) học trên hơn 2 TB văn bản CommonCrawl của 100 ngôn ngữ, và vượt mBERT rõ rệt (cao hơn trung bình 14.6 điểm trên XNLI).
- Mô hình riêng cho tiếng Việt. PhoBERT (Nguyen và Nguyen, 2020) có bản base (135 triệu tham số) và large (370 triệu), học theo quy trình của RoBERTa trên 20 GB văn bản tiếng Việt. Điểm riêng: tiếng Việt dùng dấu cách giữa các âm tiết, không phải giữa các từ, nên “thủ đô” là hai âm tiết nhưng một từ. PhoBERT tách từ tiếng Việt (word segmentation: gộp âm tiết thành từ) trước, bằng RDRSegmenter của VnCoreNLP (“Hà_Nội là thủ_đô của Việt_Nam”), rồi mới tách subword bằng BPE. Vì vậy văn bản đưa vào PhoBERT phải được tách từ theo cùng cách. Trên các bài gán nhãn từ loại, phân tích cú pháp, nhận dạng thực thể và suy luận ngôn ngữ tiếng Việt, PhoBERT vượt XLM-R dù ít tham số hơn, và dù kho văn bản của XLM-R có tới 137 GB tiếng Việt, gấp khoảng 7 lần.
Giới hạn
Phần tiêu đề “Giới hạn”- BERT không sinh văn bản tự nhiên. Nó điền chỗ trống trong một câu đã có, không viết tiếp từ trái sang phải. Việc sinh là của GPT (bài GPT và huấn luyện phân tán) và các mô hình encoder–decoder.
- Tối đa 512 token. Văn bản dài hơn phải cắt thành nhiều đoạn. Chi phí self-attention tăng theo bình phương độ dài; các cách xử lý ở bài Transformers nâng cao.
- Vectơ thô chưa phải embedding câu tốt. Xem hộp thứ ba ở mục Lỗi hay gặp.
Minh họa
Phần tiêu đề “Minh họa”Khác biệt cốt lõi giữa BERT và GPT nằm ở mặt nạ attention. Trong hình dưới, mỗi hàng là token đang được tính, mỗi cột là token mà nó được nhìn. Hãy so hai ô ở hàng “chim”, cột “bay”: BERT được nhìn, GPT bị che.
Hàng cuối của lưới GPT là hàng duy nhất nhìn đủ cả câu: chỉ token cuối mới biết mọi thứ. Ở lưới BERT, hàng nào cũng nhìn đủ, kể cả hàng đầu. Cái giá: lưới đầy thì không thể dạy bằng cách đoán token kế tiếp, nên BERT cần MLM.
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 11/2026: ô che token với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), ô BERT với Python 3.14, transformers 5.22 và PyTorch 2.14 trên CPU. Ô che token chạy ngay trên trang. Ô BERT cần PyTorch và tải mô hình từ Hugging Face Hub, nên chạy trên máy bạn.
Che token theo quy tắc 15% và 80/10/10
Phần tiêu đề “Che token theo quy tắc 15% và 80/10/10”Ô dưới làm lại Bước 1 và Bước 2 của MLM trên một câu tiếng Việt, tách theo âm tiết cho dễ đọc (BERT thật dùng WordPiece). Mỗi token thường được chọn với xác suất 0.15, giống cách thư viện transformers làm, nên số vị trí được chọn trong một câu dao động quanh 15%. Phần sau đếm tỉ lệ trên 2000 câu giả lập, mỗi câu 126 token, để thấy các con số hiện ra.
Chọn và che token cho MLM
# Chọn và che token cho MLM theo cách của BERT: 15%, rồi 80/10/10import numpy as np
rng = np.random.default_rng(0)# Một câu đã tách token (theo âm tiết, để dễ đọc), có [CLS] và [SEP]tokens = ("[CLS] hà nội là thủ đô của việt nam , nơi có hơn " "một nghìn năm lịch sử và rất nhiều món ăn ngon . [SEP]").split()special = {"[CLS]", "[SEP]"}vocab = sorted(set(tokens) - special) + ["mèo", "sông", "xanh"]
def mask_tokens(toks, rng): out, picked = list(toks), [] for i, t in enumerate(toks): if t in special or rng.random() >= 0.15: continue # không chọn: vị trí này không tính loss u = rng.random() if u < 0.8: out[i] = "[MASK]" # 80%: thay bằng [MASK] elif u < 0.9: out[i] = vocab[rng.integers(len(vocab))] # 10%: token ngẫu nhiên # 10% còn lại: giữ nguyên token gốc picked.append(i) return out, picked
out, picked = mask_tokens(tokens, rng)print(" ".join(out))print(f"{len(picked)} of {len(tokens) - 2} tokens picked")for i in picked: print(f"pos {i:2d}: {tokens[i]:>6} -> {out[i]}")
# Đếm trên 2000 câu giả lập, mỗi câu 126 token (cộng [CLS], [SEP])n_sent, n_tok = 2000, 126sel = rng.random((n_sent, n_tok)) < 0.15u = rng.random((n_sent, n_tok))[sel] # chỉ xét các vị trí được chọnprint(f"picked: {sel.mean():.4f}")print(f"[MASK] {np.mean(u < 0.8):.3f} random " f"{np.mean((u >= 0.8) & (u < 0.9)):.3f} keep {np.mean(u >= 0.9):.3f}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- Câu mẫu có 24 token thường, 3 vị trí được chọn (12.5%, vì một câu ngắn dao động nhiều quanh 15%). Vị trí 3 (“là”) và vị trí 18 (“và”) thành
[MASK]. Vị trí 11 (“có”) bị thay bằng “năm”: câu thành “nơi năm hơn một nghìn năm”, sai nhưng trông vẫn như chữ thật. Mô hình phải nhận ra và đoán lại “có”. - Loss chỉ tính ở 3 vị trí này, với nhãn là “là”, “có”, “và”. 21 vị trí còn lại không bị hỏi.
- Trên 2000 câu, tỉ lệ được chọn là 0.1507; trong số được chọn, 0.797 thành
[MASK], 0.101 thành token ngẫu nhiên, 0.102 giữ nguyên. Đúng 15% và 80/10/10 như lý thuyết, cộng sai số ngẫu nhiên.
Câu mẫu không có vị trí nào rơi vào nhánh giữ nguyên. Với một vị trí như vậy, đầu vào và nhãn giống nhau, nhưng mô hình không biết điều đó, và vẫn phải đoán.
Điền chỗ trống bằng BERT đa ngôn ngữ
Phần tiêu đề “Điền chỗ trống bằng BERT đa ngôn ngữ”File dưới dùng mBERT (google-bert/bert-base-multilingual-cased), vì nó đọc được cả tiếng Việt lẫn tiếng Anh. Phần đầu gọi pipeline("fill-mask") cho hai câu có [MASK], in 5 token có xác suất cao nhất. Phần sau nạp riêng encoder (AutoModel, không có đầu MLM), lấy last_hidden_state (các vectơ ) cho ba câu có chữ “bay”, và tính cosine giữa các vectơ của “bay”.
Chạy trên máy bạn
fill-mask và vectơ theo ngữ cảnh với mBERT
- Cần cài
pip install transformers torch- Phần cứng
- CPU là đủ. Lần đầu tải khoảng 717 MB (tệp trọng số 714 MB). Trên máy phát triển, cả file chạy khoảng một phút, phần lớn là thời gian nạp mô hình
# BERT đa ngôn ngữ: điền chỗ trống, rồi so vectơ của "bay" trong ba câuimport torchfrom transformers import AutoModel, AutoTokenizer, pipeline
MODEL = "google-bert/bert-base-multilingual-cased"fill = pipeline("fill-mask", model=MODEL)for text in ["Hà Nội là [MASK] đô của Việt Nam.", "I drink a cup of [MASK] every morning."]: print(text) for r in fill(text, top_k=5): # 5 token có xác suất cao nhất print(f" {r['token_str']:<8} {r['score']:.3f}")
tok = AutoTokenizer.from_pretrained(MODEL)model = AutoModel.from_pretrained(MODEL) # chỉ phần encoder, không có đầu MLMmodel.eval()
def encode(sentence): enc = tok(sentence, return_tensors="pt") with torch.no_grad(): h = model(**enc).last_hidden_state[0] # (số token, 768) tokens = tok.convert_ids_to_tokens(enc["input_ids"][0]) print(f"{tuple(h.shape)} {tokens}") return tokens, h
t1, h1 = encode("Con chim bay qua sông.")t2, h2 = encode("Đàn chim bay về phía nam.")t3, h3 = encode("Máy bay hạ cánh lúc tám giờ.")v1, v2, v3 = (h[t.index("bay")] for t, h in [(t1, h1), (t2, h2), (t3, h3)])cos = torch.nn.functional.cosine_similarityprint(f"cos(bird flies, birds fly) = {cos(v1, v2, dim=0):.3f}")print(f"cos(bird flies, airplane) = {cos(v1, v3, dim=0):.3f}")print(f"cos(birds fly, airplane) = {cos(v2, v3, dim=0):.3f}")# Mốc so sánh: "bay" của câu 1 với một chữ khác hẳn ("về" của câu 2)print(f"cos(bay, ve) = {cos(v1, h2[t2.index('về')], dim=0):.3f}")# Mốc nền: trung bình cosine giữa mọi cặp token của câu 1 và câu 3n1 = torch.nn.functional.normalize(h1, dim=1)n3 = torch.nn.functional.normalize(h3, dim=1)print(f"mean cos, any token pair = {(n1 @ n3.T).mean():.3f}")Kết quả mẫu
Lấy từ một lần chạy thật.
Hà Nội là [MASK] đô của Việt Nam. thủ 0.935 Thủ 0.031 kinh 0.012 đô 0.004 đại 0.002I drink a cup of [MASK] every morning. coffee 0.664 drink 0.093 milk 0.056 wine 0.052 beer 0.021(8, 768) ['[CLS]', 'Con', 'chim', 'bay', 'qua', 'sông', '.', '[SEP]'](10, 768) ['[CLS]', 'Đà', '##n', 'chim', 'bay', 'về', 'phía', 'nam', '.', '[SEP]'](12, 768) ['[CLS]', 'Má', '##y', 'bay', 'hạ', 'cánh', 'lúc', 'tá', '##m', 'giờ', '.', '[SEP]']cos(bird flies, birds fly) = 0.729cos(bird flies, airplane) = 0.562cos(birds fly, airplane) = 0.539cos(bay, ve) = 0.542mean cos, any token pair = 0.325Đọc kết quả:
- Câu tiếng Việt: “thủ” được 0.935. Đây đúng là câu ở phần Trực giác: đáp án chắc chắn nhờ chữ “đô” ở bên phải. Phương án thứ hai “Thủ” chỉ khác chữ hoa, vì đây là mô hình phân biệt hoa thường (cased). “kinh” (0.012) cũng hợp lý: “kinh đô”.
- Câu tiếng Anh: “coffee” được 0.664. Hạng hai lại là “drink” (0.093): mô hình chép lại một chữ có sẵn trong câu, mà “a cup of drink” thì không chỉ một đồ uống cụ thể nào. Sau đó mới tới “milk”, “wine”, “beer”. Phương án đầu hợp lý không có nghĩa mọi phương án trong top-5 đều hợp lý.
- Hình dạng
(8, 768): câu đầu có 8 token tính cả[CLS]và[SEP], mỗi token một vectơ 768 chiều. Bộ tách từ (tokenizer) của mBERT cắt một số âm tiết tiếng Việt thành mảnh: “Máy” thànhMávà##y, “tám” thànhtávà##m. Bài Tách từ giải thích vì sao. - Cosine: “bay” trong hai câu về chim gần nhau nhất (0.729). “bay” của “chim bay” và của “máy bay” xa hơn (0.562 và 0.539). Với embedding tĩnh, cả ba con số này đều bằng 1, vì cùng một dòng của bảng.
- Hai mốc để đọc các con số trên. “bay” của câu 1 so với một chữ khác hẳn, “về” của câu 2, được 0.542: gần bằng 0.562 của cặp “chim bay” và “máy bay”. Tức là với mô hình, chữ “bay” trong “máy bay” cách chữ “bay” của “chim bay” xa cỡ như một chữ khác. Mốc nền, trung bình cosine giữa mọi cặp token của câu 1 và câu 3 (kể cả
[CLS],[SEP]và dấu chấm), là 0.325.
Hai lưu ý trung thực. Thứ nhất, vectơ của BERT không đẳng hướng (anisotropic): chúng chụm về một vùng hẹp, nên cosine giữa hai token bất kỳ đã dương đáng kể (Ethayarajh, 2019); mốc nền 0.325 ở trên cho thấy điều đó. Vì vậy hãy so các con số với nhau và với mốc, đừng đọc riêng một con số. Thứ hai, ba câu chưa đủ để kết luận chung. Đây là một minh họa, không phải phép đo.
Khi chạy, transformers còn in ra (ở luồng lỗi) hai báo cáo nạp trọng số, liệt kê các tham số UNEXPECTED: có trong checkpoint nhưng lớp mô hình không dùng. Lần thứ nhất, pipeline nạp BertForMaskedLM và bỏ qua bert.pooler.* (lớp pooler) cùng cls.seq_relationship.* (đầu NSP), vì điền chỗ trống chỉ cần đầu MLM. Lần thứ hai, AutoModel nạp BertModel (encoder cùng lớp pooler) và bỏ qua cls.predictions.*, tức chính đầu MLM, cùng cls.seq_relationship.*, vì ở bước này ta chỉ cần encoder. Checkpoint giữ mọi đầu từ lúc pre-train; mỗi lớp mô hình lấy phần nó cần. Đây không phải lỗi.
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:
- Embedding tĩnh cho mỗi token một vectơ. BERT cho mỗi lần xuất hiện một vectơ , tính từ cả câu qua một chồng khối encoder có attention hai chiều (BERT-base: 12 lớp, , 12 đầu, khoảng 110 triệu tham số).
- Đầu vào là tổng embedding token, đoạn và vị trí, có
[CLS]ở đầu và[SEP]sau mỗi đoạn, tối đa 512 token. - MLM chọn 15% vị trí; trong đó 80% thành
[MASK], 10% thành token ngẫu nhiên, 10% giữ nguyên, để giảm độ lệch giữa pre-train và lúc dùng. Loss chỉ tính ở các vị trí được chọn, so với token gốc. NSP thêm vào lúc đầu, RoBERTa cho thấy bỏ đi không thiệt. - Fine-tune thêm một lớp nhỏ: phân loại từ , gán nhãn từ từng , hoặc chọn span bằng hai vectơ đầu và cuối. Cho tiếng Việt, có mBERT, XLM-R và PhoBERT (cần tách từ trước).
Đọc thêm:
- Nguyen, D. Q., Nguyen, A. T. (2020). PhoBERT: Pre-trained language models for Vietnamese, Findings of EMNLP 2020, mục 2 và 3. Vì sao tiếng Việt cần tách từ trước khi pre-train, dữ liệu 20 GB, và so sánh với XLM-R trên bốn bài toán tiếng Việt.
- Devlin, J., Chang, M.-W., Lee, K., Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding, NAACL 2019, mục 3, 5.1 và phụ lục A.1, C.2. Bài gốc: kiến trúc, MLM, NSP, cách fine-tune, và các thí nghiệm bỏ NSP, đổi tỉ lệ che. Bài báo viết cho vectơ chỗ bài này viết , và cho số chiều ẩn chỗ bài này viết .
- Liu, Y. và cộng sự (2019). RoBERTa: A Robustly Optimized BERT Pretraining Approach, arXiv:1907.11692, mục 4. Che động, bỏ NSP, lô lớn: BERT được huấn luyện “chưa tới” như thế nào.
- Clark, K., Luong, M.-T., Le, Q. V., Manning, C. D. (2020). ELECTRA: Pre-training Text Encoders as Discriminators Rather Than Generators, ICLR 2020, mục 1 và 2. Vì sao học từ 15% token là lãng phí, và cách học từ mọi token.
- Hugging Face. Pipelines, tài liệu
transformers, mụcFillMaskPipeline. Các tham sốtop_k,targetsvà các trường của kết quả.
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