智慧型控制 Intelligent Control

生成模型與大型語言模型

前面的網路都是判別模型:給輸入,判斷類別或預測數值。生成模型則學習資料本身的機率分布,再從中取樣產生新的資料:影像、聲音、文字。大型語言模型(large language model,LLM)就是文字的生成模型。

三種主要的生成模型 Generative Models

VAE(2013)資料 → 編碼器 → 潛在向量 zz → 解碼器 → 重建資料從 z 的分布取樣產生新資料GAN(2014)生成器:雜訊 → 假資料判別器:分辨真假兩者對抗,假資料越來越像擴散模型(2020)逐步把資料加雜訊變成純雜訊訓練網路一步步去雜訊從純雜訊反覆去雜訊產生資料共同目標:學出資料的機率分布,再從中取樣產生新的影像、聲音或文字

語言模型 Language Models

一段文字 \(x_1, x_2, \dots, x_n\) 的機率可以用連鎖律拆成「每個字在前面的字之下的條件機率」相乘:

$$p(x_1, \dots, x_n) = \prod_{t=1}^{n} p(x_t \mid x_1, \dots, x_{t-1})$$

所以語言模型只要學一件事:給前文,預測下一個詞元(token:一個字、一個詞或詞的一部分)。訓練時最小化交叉熵 \(-\frac1n\sum_t \log p(x_t \mid x_{<t})\);常用的評估指標困惑度(perplexity)是交叉熵的指數,可以理解成「模型平均在幾個候選字之間猶豫」。生成時則一次取樣一個詞元,接到前文後面,再預測下一個。

範例:在本課程的文字上訓練迷你 GPT A Tiny GPT

把本網站各頁的中文說明(不含程式與公式)合併成約 5 萬字的語料(下載 corpus.txt),訓練一個字元級的小型 Transformer:每個「詞元」就是一個字,2 層、因果遮罩、上下文 64 個字。為了避免過擬合,語料切成 500 字一段,每 10 段取 1 段當模型沒看過的驗證文字,並在驗證誤差最低時停止:

# 迷你語言模型:在本課程網站的文字(corpus.txt,約 5 萬字)上,訓練一個字元級的 Transformer 預測「下一個字」
import json
import math
from pathlib import Path

import torch
from torch import nn

HERE = Path(__file__).parent
text = (HERE / "corpus.txt").read_text(encoding="utf-8")
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
data = torch.tensor([stoi[c] for c in text])
chunks = data.split(500)                              # 每 500 字一段,每 10 段取 1 段當驗證文字(模型沒看過)
train = torch.cat([c for i, c in enumerate(chunks) if i % 10 != 9])
val = torch.cat([c for i, c in enumerate(chunks) if i % 10 == 9])
cut = len(train)
V, T, D = len(chars), 64, 64                          # 字元種類、上下文長度、向量維度

# 基準:unigram(只看字頻)與 bigram(只看前一個字,加一平滑)
uni = torch.bincount(train, minlength=V).float() + 1
bi = torch.ones(V, V)
bi.index_put_((train[:-1], train[1:]), torch.ones(cut - 1), accumulate=True)
ppl_uni = math.exp(-(uni / uni.sum()).log()[val].mean().item())
ppl_bi = math.exp(-(bi / bi.sum(1, keepdim=True)).log()[val[:-1], val[1:]].mean().item())

class Block(nn.Module):
    def __init__(self):
        super().__init__()
        self.n1, self.n2 = nn.LayerNorm(D), nn.LayerNorm(D)
        self.att = nn.MultiheadAttention(D, 4, dropout=0.1, batch_first=True)
        self.ff = nn.Sequential(nn.Linear(D, 4 * D), nn.GELU(), nn.Linear(4 * D, D), nn.Dropout(0.1))
    def forward(self, h, mask):
        a = self.n1(h)
        h = h + self.att(a, a, a, attn_mask=mask, need_weights=False)[0]   # 因果遮罩:看不到後面的字
        return h + self.ff(self.n2(h))

class GPT(nn.Module):
    def __init__(self):
        super().__init__()
        self.tok, self.pos = nn.Embedding(V, D), nn.Embedding(T, D)      # 位置用可學習的向量
        self.blocks = nn.ModuleList([Block(), Block()])
        self.norm, self.head = nn.LayerNorm(D), nn.Linear(D, V)
    def forward(self, x):
        n = x.shape[1]
        mask = torch.ones(n, n, dtype=torch.bool).triu(1)
        h = self.tok(x) + self.pos(torch.arange(n))
        for b in self.blocks:
            h = b(h, mask)
        return self.head(self.norm(h))

def get_batch(src, n, g):
    i = torch.randint(0, len(src) - T - 1, (n,), generator=g)
    x = torch.stack([src[j:j + T] for j in i])
    return x, torch.stack([src[j + 1:j + T + 1] for j in i])          # 目標:每個位置的「下一個字」

torch.manual_seed(0)
g = torch.Generator().manual_seed(0)
net = GPT()
opt = torch.optim.AdamW(net.parameters(), lr=2e-3)
curve, best = [], (1e9, 0, None)
for step in range(1, 2001):
    net.train()
    x, y = get_batch(train, 32, g)
    loss = nn.functional.cross_entropy(net(x).reshape(-1, V), y.reshape(-1))
    opt.zero_grad(); loss.backward(); opt.step()
    if step % 100 == 0:
        net.eval()                                                    # 評估時關掉 dropout
        with torch.no_grad():
            xv, yv = get_batch(val, 256, torch.Generator().manual_seed(1))
            vl = nn.functional.cross_entropy(net(xv).reshape(-1, V), yv.reshape(-1)).item()
        curve.append([step, loss.item(), vl])
        if vl < best[0]:                                              # 提早停止:記下驗證誤差最低的模型
            best = (vl, step, {k: v.clone() for k, v in net.state_dict().items()})
net.load_state_dict(best[2]); net.eval()
print(f"字元 {len(text)} 個、{V} 種;模型參數 {sum(p.numel() for p in net.parameters())} 個")
print(f"驗證誤差最低在第 {best[1]} 步(之後開始過擬合),採用這時的模型")
print(f"驗證文字的困惑度(越小越好):unigram {ppl_uni:.0f}、bigram {ppl_bi:.0f}、Transformer {math.exp(best[0]):.0f}")

def generate(prompt, n, temp, seed):
    g = torch.Generator().manual_seed(seed)
    x = torch.tensor([[stoi[c] for c in prompt]])
    with torch.no_grad():
        for _ in range(n):
            p = torch.softmax(net(x[:, -T:])[0, -1] / temp, -1)       # 溫度:越低越保守
            x = torch.cat([x, torch.multinomial(p, 1, generator=g).view(1, 1)], 1)
    return "".join(chars[i] for i in x[0].tolist())

for temp in (0.5, 1.0):
    print(f"\n溫度 {temp}:", generate("類神經網路", 60, temp, 0).replace("\n", " "))
nxt = {}                                                              # 給網頁動畫:幾個開頭之後,機率最高的 12 個字與其 logit
for prompt in ("類神經網", "倒單擺的", "梯度"):
    with torch.no_grad():
        top = net(torch.tensor([[stoi[c] for c in prompt]]))[0, -1].topk(12)
    nxt[prompt] = [[chars[i], round(v, 4)] for v, i in zip(top.values.tolist(), top.indices.tolist())]
(HERE / "char_lm.json").write_text(json.dumps({"curve": curve, "ppl": [ppl_uni, ppl_bi], "best": best[1], "next": nxt}, ensure_ascii=False), encoding="utf-8")

⬇ 下載 char_lm.py(Python 3+PyTorch)

實際執行的輸出:

字元 51971 個、1157 種;模型參數 253445 個
驗證誤差最低在第 800 步(之後開始過擬合),採用這時的模型
驗證文字的困惑度(越小越好):unigram 280、bigram 169、Transformer 46

溫度 0.5: 類神經網路的特定。 感知器,只有「Sute Inondurit 模擬結論合 FIS tenstion Foletion Ed se

溫度 1.0: 類神經網路的特定在控制速度,只能只是回授一旦Inp PyTit 模擬合元增益,又用力的結論是否則是 0.5,粒、後仍有螞蟻走過去的
05001000150020002345訓練步數交叉熵(每字,nats)第 800 步驗證誤差最低bigram 基準訓練文字沒看過的驗證文字之後訓練誤差持續下降,驗證誤差卻回升:過擬合困惑度 = e^交叉熵

(補充)作為對照,2020 年的 GPT-3 有 1750 億個參數,約為這個迷你模型的 70 萬倍,訓練了約 3000 億個詞元。研究也發現,模型、資料與運算量一起放大時,誤差會照冪次律穩定下降(scaling law),這是大型語言模型越做越大的主要原因。

溫度與取樣 Sampling

模型輸出的是下一個字的機率分布。生成時常用溫度 T 調整:\(p_i \propto \exp(z_i / T)\)。T 小,機率集中在最可能的字,文字保守但容易重複;T 大,機率攤平,文字多樣但容易出錯。top-k 只從機率最高的 k 個字中抽樣,避免抽到極不合理的字。下方動畫使用上面迷你模型的真實輸出:

自製動畫(依控制理論數值模擬)

從語言模型到聊天助理 From LM to Assistant

預訓練資料:大量網路文字預測下一個詞元指令微調資料:人工撰寫的問答學會依指令回答人類回饋強化學習資料:人對回答排序訓練獎勵模型+強化學習(本頁範例只做到這一步)強化學習見後續的強化學習章後兩步的資料量遠小於預訓練,主要在調整「怎麼回答」,知識大多來自預訓練
  1. 預訓練:用大量網路文字訓練「預測下一個詞元」,模型因此學到語言與大量知識。
  2. 指令微調:用人工撰寫的「指令—回答」配對繼續訓練,讓模型學會依指令回答,而不是只會接續文字。
  3. 人類回饋強化學習(RLHF):請人比較模型的不同回答,用這些偏好訓練一個「獎勵模型」,再用強化學習調整語言模型,讓回答更有用、更安全(強化學習見後續章節)。

限制與在控制上的角色 Limitations