適合對象: 讀完
04a-gpt-decoder-only.md後,想理解「不是為了生成,而是為了理解」的那一支 Transformer 的讀者。讀完後你能做什麼:
- 說明 BERT 為什麼拿掉 Causal Mask,用「雙向」注意力,以及這對任務的意義
- 解釋 MLM(Masked Language Modeling)與 next-token prediction 的差異,並寫出它的損失
- 描述
[CLS]/[SEP]與 token / segment / position 三種 embedding 的角色- 說明「預訓練 + 微調」範式,講清楚它與「遷移學習」的包含關係,以及分類 / 序列標註 / QA 各自怎麼接 head
- 用 HuggingFace Transformers 寫出一段最小的微調程式,並判斷什麼樣的任務適合這樣做
- 分辨 encoder 家族(RoBERTa、ELECTRA、Sentence-BERT…)並知道何時該選 encoder、何時選 decoder
前置文件:
03a-transformer-architecture.md(Transformer Block)、04a-gpt-decoder-only.md(§1 Encoder-Decoder)、05a1-forward-propagation.md(§2 Causal Masking)定位: 主線的 encoder-family 選讀分支。主線(01→04→NB4)走的是 decoder-only 的 GPT;
06是往 LLaMA 的 decoder 出口;本文是往 BERT 的 encoder 出口。兩者共用同一個 Transformer Block,差別只在遮罩與訓練目標。學完後的下一步: →
../notebooks/NB5-bert-mlm.ipynb(從零手刻最小 BERT)
- 閱讀地圖:GPT 與 BERT 的分岔
- 雙向 Self-Attention:拿掉 Causal Mask
- MLM 預訓練目標(Masked Language Modeling)
- 輸入表示:
[CLS]/[SEP]與三種 Embedding - NSP 與後續的質疑
- 預訓練 + 微調範式:BERT 怎麼做遷移學習
- 5.1 遷移學習與微調不是同義詞
- 5.2 兩階段流程
- 5.3 下游任務怎麼接 head
- 5.4 實作:用 HuggingFace Transformers 微調
- 5.5 什麼樣的問題適合用 BERT 微調
- BERT 家族速覽
- encoder vs decoder 選型指南
- 對照表:BERT vs GPT
04a §1 講過:2017 年原始 Transformer 是 Encoder-Decoder。之後兩大預訓練範式各自只取一半:
原始 Transformer(Encoder + Decoder)
│
┌───────────────┴───────────────┐
取 Encoder 取 Decoder
│ │
BERT(2018) GPT(2018–)
雙向、理解 因果、生成
Masked LM 預訓練 Next-token 預訓練
→ 本文 07 / NB5 → 主線 04 / NB4
一句話定調:
| GPT(decoder-only) | BERT(encoder-only) | |
|---|---|---|
| 注意力方向 | 因果(只看過去) | 雙向(左右都看) |
| 預訓練目標 | 預測下一個詞 | 填被遮住的詞 |
| 天生擅長 | 生成 | 理解 / 表示 |
關鍵是:這兩者的 Transformer Block 幾乎一模一樣——多頭注意力、殘差、LayerNorm、FFN 全部照 03a 學過的。真正的差別只有兩處,就是本文 §1(遮罩)與 §2(目標)。理解了這兩處,你就同時懂了 encoder 與 decoder 兩大家族。
把整份教材攤平來看,主線走到 nanoGPT(NB4)就分出兩條選讀分支,本文是其中的 encoder 這條:
01→02→03 打好 Transformer Block 地基(多頭 / 殘差 / LayerNorm / FFN)
│
▼
04 + NB4 nanoGPT(causal mask + next-token)── 主線終點
│
┌───────────┴────────────┐
decoder 分支 encoder 分支(本文)
06 現代變體 07 BERT ── NB5(重用 NB4,去 mask + 換 MLM)
→ LLaMA(decoder 出口) │
09 文字轉向量 / RAG(encoder 應用出口)
主線終點是 nanoGPT,但 BERT 不是更進階的續集,而是平行的另一半。你在 NB4 已經有一個能跑的 Transformer;NB5 幾乎原封不動重用 NB4 的元件(多頭注意力、殘差、LayerNorm、FFN、Pre-LN Block 全部照舊),只動兩個地方:
-
拿掉 causal mask(§1):注意力矩陣從下三角變回全連接,位置
$i$ 能看全序列。 - 換掉訓練目標(§2):把「預測下一字」的輸出頭改成「填被遮住的字」(MLM)。
所以學過 nanoGPT 的人不必重學架構,只要理解這兩個 diff。讀完本文再往下走,encoder 產生的向量會在 09 接到語意檢索與 RAG——那是這條分支的應用出口(與 decoder 分支的 06 → LLaMA 平行;完整分支圖見 00 §5)。
回顧 05a1 §2:GPT 為了「生成時不偷看未來」,在 softmax 前把未來位置的分數設成
GPT(Causal)— 位置 i 只能看 0..i:
位置 0:[1, 0, 0, 0]
位置 1:[1, 1, 0, 0]
位置 2:[1, 1, 1, 0]
位置 3:[1, 1, 1, 1]
BERT 不做生成,它的任務是理解一整句已經給定的句子——句子的每個字左右都在,沒有「未來」要保護。所以 BERT 直接不加遮罩,每個位置都能看到全序列:
BERT(雙向)— 位置 i 能看全部:
位置 0:[1, 1, 1, 1]
位置 1:[1, 1, 1, 1]
位置 2:[1, 1, 1, 1]
位置 3:[1, 1, 1, 1]
程式上的差異小到只有一行。GPT 的單頭注意力(04b §1)長這樣:
wei = q @ k.transpose(-2, -1) * d_k**-0.5 # (B, T, T) 原始分數(d_k = k.shape[-1])
wei = wei.masked_fill(tril[:T, :T] == 0, float('-inf')) # ← GPT 專屬:遮住未來
wei = F.softmax(wei, dim=-1)BERT 版把中間那行刪掉即可:
wei = q @ k.transpose(-2, -1) * d_k**-0.5 # (B, T, T)
# 沒有 masked_fill —— 這就是「雙向」
wei = F.softmax(wei, dim=-1)注意:這裡刪的是因果遮罩(下三角)。實務上仍會保留一個 padding mask,把批次中補齊長度用的
[PAD]位置遮掉,避免真實 token 去關注無意義的填充。padding mask 與因果 mask 是兩回事,前者是工程細節、後者才是 encoder/decoder 的本質差異。
考慮這個填空:
The animal didn't cross the street because it was too ___.
要決定 it 指的是 animal 還是 street、空格該填 tired 還是 wide,模型必須同時利用左邊(animal、cross)與右邊(如果句子後面還有線索)的資訊。因果模型在預測某個位置時右側被遮住,只能靠單邊;雙向模型左右通吃,對「理解」類任務(分類、抽取、標註)先天有利。
但天下沒有白吃的午餐——雙向模型不能拿來自迴歸生成。因為它訓練時每個位置都看過完整句子,一旦讓它「一個字一個字往下寫」,它從沒學過「只根據左文預測右邊」這件事。這正是為什麼生成走 decoder、理解走 encoder。§2 會看到,這個限制其實是被預訓練目標逼出來的。
有趣的巧合:naive self-attention(
01b§「注意力權重矩陣」)在還沒引入$W_Q, W_K$ 時,相似度矩陣$E = XX^\top$ 本來就是對稱的($E_{ij}=E_{ji}$)——也就是說「雙向」才是注意力的原始面貌,因果遮罩反而是為了生成而後加的限制。BERT 只是把這個後加的限制拿掉而已。
拿掉遮罩之後,還有一個問題:訓練目標要換掉。
GPT 的目標是 next-token prediction(05a1 §7):位置
BERT 的解法是 Masked Language Modeling(MLM,克漏字):把輸入句子隨機挖掉一些字,讓模型用左右文把它們填回來。
BERT 隨機挑 15% 的 token 當作預測目標。對每個被挑中的 token,再依下列比例決定它在輸入端長什麼樣:
| 比例 | 輸入端替換成 | 目的 |
|---|---|---|
| 80% | [MASK] 特殊 token |
主要的克漏字訊號 |
| 10% | 隨機一個其他 token | 逼模型別盲信輸入、要靠上下文糾錯 |
| 10% | 維持原字不變 | 讓模型對「非 [MASK]」的位置也保持表示能力 |
為什麼不是全部換成 [MASK]? 因為微調與推論時輸入裡沒有 [MASK]。若預訓練時模型只在看到 [MASK] 才需要輸出好的表示,就會與下游任務產生落差(train/inference mismatch)。混入 10% 隨機字與 10% 原字,讓模型對「每一個位置」都維持好的上下文表示,而不只針對 [MASK]。
兩個「mask」不是同一回事。 BERT 與 GPT 都在做「猜字」、也都用到 mask,但遮的東西完全不同:
遮什麼 遮在哪一層 效果 BERT 的 [MASK]輸入序列裡的 token(約 15%) 輸入端(embedding 之前) 製造克漏字題目,答案要用左右文還原 GPT 的因果遮罩 注意力矩陣中的未來位置 注意力分數上(softmax 之前) 讓位置 $i$ 看不到$i$ 之後,避免抄答案換句話說,BERT 遮的是「題目」,GPT 遮的是「視野」。再加上 §1.1 提到的 padding mask(遮掉補齊長度的
[PAD],兩家都要),本教材出現過的三種 mask 就到齊了。
設被挑中的位置集合為
其中 05a1 §7 的 cross-entropy 形式完全相同,唯一差別是求和範圍:next-token 對每個位置都算損失(一個序列同時訓練
程式上對應 F.cross_entropy 的 ignore_index——把沒被遮的位置的 target 設成 -100,就不計入損失:
# logits: (B, T, vocab) labels: (B, T),未被遮的位置填 -100
loss = F.cross_entropy(logits.view(B*T, vocab), labels.view(B*T), ignore_index=-100)MLM 每個序列只從 15% 的位置得到學習訊號,next-token 則是 100%(每個位置都預測下一字)。用具體數字看更清楚:一條長度
這裡要修正一個常見的誤解:「BERT 靠 mask 學得比較有效率」這句話是反的。就訓練訊號密度而言 GPT 才高,BERT 的優勢在上下文品質(雙向),不在樣本效率。
BERT 在把句子送進 Transformer 之前,做了兩件 GPT 沒有的事。
輸入: [CLS] the cat sat [SEP] it slept [SEP]
└句首 └句A結束 └句B結束
[CLS](classification):放在最前面。因為是雙向注意力,這個位置能看到整句,經過數層後它的輸出向量就成了整句的表示——下游分類任務直接接在[CLS]的輸出上(§5)。[SEP](separator):分隔兩個句子,讓 BERT 能吃「句子對」輸入(問答、句子關係判斷都需要一次餵兩句)。
主線的 GPT 把 token embedding + position embedding 相加(03a §7.5 的可學習 PE)。BERT 多加一種 segment embedding,用來標記「這個 token 屬於句 A 還是句 B」:
三者都是可學習的向量、逐元素相加,維度都是
BERT 的 position embedding 是可學習的(同
03a§7.5),不是正弦式,也還沒有 RoPE(RoPE 見06§3)——這一點與 nanoGPT 相同。
原始 BERT 除了 MLM,還有第二個預訓練目標 NSP(Next Sentence Prediction,下一句預測):給定句 A 與句 B,用 [CLS] 的輸出做二分類,判斷「B 是不是 A 在原文中的下一句」。動機是讓模型學會句子之間的關係,對問答、自然語言推理(NLI)這類需要理解句對的任務有幫助。
但後續研究(尤其 RoBERTa,§6)發現:NSP 幾乎沒有貢獻,甚至可能有害。原因是 NSP 把「主題預測」和「連貫性預測」混在一起,任務太容易,模型學不到真正有用的句間關係。RoBERTa 直接拿掉 NSP、只留 MLM,並用更多資料、更長訓練、更大 batch,效果反而更好。
結論: MLM 是 BERT 真正的核心;NSP 屬於「當年這樣設計、後來被證明可省」的歷史細節。手刻最小 BERT(NB5)時只實作 MLM 即可。
BERT 讓「預訓練一次、到處微調」成為主流。這一節先釐清概念(§5.1、§5.2),再看下游怎麼接(§5.3)、程式怎麼寫(§5.4),最後回答「什麼樣的問題適合這樣做」(§5.5)。
「BERT 是在做遷移學習」這句話沒錯,但常被說得太籠統,也常和「微調」混為一談。兩者其實是包含關係:
- 遷移學習(transfer learning) 是較大的概念,指把在來源任務(source task)上學到的知識,應用到目標任務(target task)上。BERT 的來源任務是 MLM 預訓練,目標任務是下游的分類、標註、抽取。遷移的主體是「表徵知識」——模型對詞彙、語法、語意甚至部分常識的內部表示。
- 微調(fine-tuning) 只是實現遷移的其中一種手段:拿預訓練權重當初始值,再用目標任務的資料繼續做梯度更新。
除了微調,遷移學習還有別的做法,最常見的是特徵萃取(feature extraction):把預訓練權重整個凍結(不參與梯度更新),只訓練後面新加的分類層,等於把 BERT 當成一台固定的「句子 → 向量」轉換器。資料量極小或算力有限時會這樣做;但 BERT 最常見、效果也最好的用法仍是全模型微調。
這個模式在電腦視覺(computer vision, CV)領域早就成熟:先用 ImageNet 訓練一個 ResNet 學到通用的視覺特徵,再把它轉移到只有幾千張標註影像的目標任務上,避免從零訓練又缺標註資料的困境。BERT 把同一套邏輯搬到自然語言處理(Natural Language Processing, NLP),而且因為雙向架構的上下文表徵品質高(§1.2),轉移效果特別明顯——這正是它在 2018 年提出後迅速成為 NLP 標準做法的原因。
階段一:預訓練(一次,昂貴)
海量無標註文本 ──MLM──> 一個「懂語言」的 encoder(權重 θ)
階段二:微調(每個任務一次,便宜)
θ 當起點 + 接一個小 head + 少量標註資料 ──> 專用模型
階段一用的是無標註語料(原始 BERT 用 BooksCorpus 與英文維基百科),透過 MLM(§2)與 NSP(§4)學到不針對任何特定任務的通用表徵。階段二則換上有標註的目標任務資料,讓 BERT 本體加上新接的 head 一起做梯度更新。由於大部分有用的表徵在階段一已經學好,階段二通常只要很小的學習率(常見 2e-5 到 5e-5)與很少的 epoch(2 到 4 個)——它做的是局部調整,不是重新學習。
| 任務類型 | 接在哪 | head 形狀 | 例子 |
|---|---|---|---|
| 句子分類 |
[CLS] 的輸出向量 |
|
情感分析、垃圾郵件 |
| 句子對分類 |
[CLS](吃 A [SEP] B) |
|
NLI、語意相似 |
| 序列標註 | 每個 token 的輸出 |
|
命名實體辨識(NER)、詞性標註 |
| 抽取式 QA | 每個 token 的輸出 | 兩個 |
SQuAD |
關鍵在於:主體 encoder 不變,只換最上面那層薄薄的 head,再用該任務的少量標註資料微調整個網路。這就是為什麼一個 BERT 預訓練權重能撐起幾十種下游應用。
這與 GPT 家族後來的路線(zero-shot / few-shot prompting,不改權重)形成對比:BERT 時代靠「微調」,GPT-3 之後靠「提示」。兩條路各有適用場景。
實務上不必自己手刻 head。HuggingFace Transformers 針對上表每一列都提供了包好的類別,載入預訓練權重時 head 會一併建好(隨機初始化):
| 任務類型 | 對應類別 |
|---|---|
| 句子分類 | BertForSequenceClassification |
| 序列標註 | BertForTokenClassification |
| 抽取式 QA | BertForQuestionAnswering |
| 句子對任務 | 同 BertForSequenceClassification,靠 token_type_ids 區分前後句 |
微調流程有四步:
- 載入預訓練模型與對應的 tokenizer。兩者必須配對(例如都用
bert-base-chinese),因為 tokenizer 決定了詞彙表與子詞切分規則,跟預訓練時用的必須一致——換了 tokenizer,token ID 就對不上預訓練學到的 embedding。 - 資料前處理:把原始文字轉成 BERT 的輸入格式,包含
input_ids(token 的數字 ID)、attention_mask(標記哪些是真實 token、哪些是 padding,即 §1.1 的 padding mask)、必要時還有token_type_ids(§3.2 的 segment embedding 索引)。 - 接上任務 head:用上表的類別,或自己在
[CLS]輸出上接一層nn.Linear。 - 微調訓練:小學習率、少量 epoch,整個模型一起更新。
from transformers import BertTokenizer, BertForSequenceClassification
from torch.optim import AdamW
# load pretrained BERT with a classification head already attached
tokenizer = BertTokenizer.from_pretrained("bert-base-chinese")
model = BertForSequenceClassification.from_pretrained(
"bert-base-chinese", num_labels=3 # e.g. negative / neutral / positive
)
# tokenize input text into BERT's expected format
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
inputs["labels"] = labels
optimizer = AdamW(model.parameters(), lr=2e-5) # small lr: pretrained weights are already good
# standard fine-tuning loop
for batch in dataloader:
outputs = model(**batch)
loss = outputs.loss # CE over [CLS] logits, computed inside the model
loss.backward()
optimizer.step()
optimizer.zero_grad()整段核心邏輯不到 50 行——這就是遷移學習在工程上的樣子:昂貴的部分(預訓練)別人做完了,你只付微調的成本。
這段程式需要
pip install transformers,不在本教材主線的相依範圍內。想實際跑一次,見NB5末尾的選讀延伸段(預設RUN_HF=False,要自行開啟)。
判斷標準看三件事:
一、任務本質是理解而非生成。 BERT 是 encoder-only,沒有生成能力(原因見 §1.2),只能對輸入做判斷或抽取,寫不出一段新文字。摘要、對話、翻譯這類任務不適合直接用 BERT 微調,該找 decoder(GPT)或 encoder-decoder(T5、BART)——選型判斷見 §7。
二、標註資料量偏小。 目標任務只有幾百到幾萬筆標註時,從零訓練一個 Transformer 學不到好的表徵,用預訓練權重當起點會明顯勝出。反過來,如果標註資料已到千萬等級,從零訓練與微調的差距會縮小,遷移學習的優勢就沒那麼明顯。
三、任務的語言與領域跟預訓練語料相近。 若目標任務屬於高度專業領域(醫療、法律),用字習慣與一般預訓練語料(維基百科、新聞)差很多,直接微調效果可能不理想。此時常見做法是先做領域內持續預訓練(domain-adaptive pretraining)——拿該領域的無標註文本再跑一輪 MLM,得到 BioBERT、LegalBERT 這類模型——再進入任務微調,等於在兩階段之間多插一個中間步驟。
實務上像客服工單分類、產品評論情感分析、履歷與職缺配對、法律條文的關鍵資訊抽取,都是典型會用 BERT 微調的場景:輸出格式套得進「分類 / 序列標記 / 片段抽取」這幾種模板,而且標註資料通常只有幾千筆。
BERT(2018)之後,encoder 家族沿著「更好的預訓練」與「更小更快」兩個方向演化:
| 模型 | 一句話差異 |
|---|---|
| RoBERTa (2019) | 拿掉 NSP、動態遮罩、更多資料更久訓練——證明「BERT 沒訓練夠」 |
| ALBERT (2019) | 跨層共享參數 + embedding 分解,大幅減參數量 |
| ELECTRA (2020) | 改用 replaced token detection:判斷每個 token 是否被替換過,讓所有位置都有訓練訊號,解決 §2.3 的 15% 稀疏問題,樣本效率大增 |
| DistilBERT (2019) | 知識蒸餾,約 40% 參數、60% 速度,保留約 97% 效果 |
| Sentence-BERT (2019) | 用 siamese 結構微調,讓 [CLS] / 池化向量可直接用餘弦相似度比對——句子嵌入與語意檢索的基石 |
前四個是「怎麼把 encoder 訓得更好 / 更省」,Sentence-BERT 則指向下一節的應用出口。
學到這裡,你手上有兩種架構。實務上怎麼選?
你的任務是「產生新文字」嗎?
├─ 是 → decoder-only(GPT / LLaMA) → 主線 04 / 06
│ 生成、對話、續寫、翻譯、摘要
└─ 否,是「理解 / 分類 / 比對既有文字」→ encoder-only(BERT 家族)→ 本文
分類、NER、抽取式 QA、句子相似、檢索嵌入
這棵樹只切「生成 vs 理解」這一刀。確定要走 encoder 之後,還要再確認標註資料量與領域落差是否適合微調——三條判準見 §5.5。
其中「把句子變成一個向量」是 encoder 最有價值的出口,直接銜接 RAG 檢索(完整展開見 09):
Sentence-BERT ──> 句子 / 文件 embedding ──> 向量資料庫 ──> 相似度檢索(RAG)
RAG(檢索增強生成)常見的組合,正是用 encoder 做檢索、用 decoder 做生成——兩大家族各司其職。所以這條 encoder 分支不是主線的替代品,而是補上另一半:讀完主線你會生成,讀完本文你會理解與檢索。從句向量到 RAG 檢索流程的完整說明,見 09-text-to-vector-rag.md。
補充:也有 encoder-decoder 模型(T5、BART),把「理解輸入」與「生成輸出」都要的 seq2seq 任務(翻譯、摘要)用兩半一起做。它就是原始 Transformer 的直系後代(
04a§1)。
| BERT(encoder-only) | GPT(decoder-only) | |
|---|---|---|
| 取自原始 Transformer 的 | Encoder | Decoder |
| 注意力遮罩 | 無因果遮罩(雙向) | 下三角(因果) |
| 注意力矩陣 | 全連接 | 下三角 |
| 訓練時 mask 遮的對象 |
輸入 token(15% 換成 [MASK]) |
注意力矩陣的未來位置 |
| 預訓練目標 | MLM(填 15% 被遮的字) | Next-token(預測下一字) |
| 訓練訊號密度 | 稀疏(15% 位置,$T{=}512$ 約 77 個) | 稠密(每個位置,$T{=}512$ 有 512 個) |
| 特殊 token |
[CLS] / [SEP]
|
通常只有句界/BOS |
| Embedding | token + segment + position | token + position |
| 天生擅長 | 理解、分類、抽取、嵌入 | 生成、續寫、對話 |
| 下游用法 | 接 head 微調 | 微調或 prompting |
| 代表模型 | BERT、RoBERTa、ELECTRA、Sentence-BERT | GPT、LLaMA、Mistral、Qwen |
| 本教材對應 | 07 / NB5 | 04 / NB4、06 |
不變的部分:多頭注意力、$\text{softmax}(QK^\top/\sqrt{d_k})V$、殘差、LayerNorm、FFN、Pre-LN Block——03a 學到的骨架兩家共用。變的只有這張表列出的「遮罩」與「目標」兩條主軸,其餘全是它們的衍生。
- 動手:
../notebooks/NB5-bert-mlm.ipynb從零手刻最小 BERT——把 NB4 的Head刪掉一行masked_fill、把 next-token 換成 MLM,親眼看到「同一個 Block、換遮罩換目標」就從 GPT 變成 BERT。Notebook 末尾附選讀延伸:載入 HuggingFace 預訓練 BERT 做克漏字與分類,對照手刻版。 - 應用:
09-text-to-vector-rag.md——從 Word2Vec 到 Sentence-BERT,再到 RAG 的完整檢索流程(encoder 分支的應用出口)。 - 對照:想看 decoder 家族怎麼演化到 LLaMA,見
06。