現(xiàn)與訓(xùn)練調(diào)優(yōu)實(shí)戰(zhàn))
簡介這份PPT課件面向NLP入門與進(jìn)階學(xué)習(xí)者系統(tǒng)講解Transformer模型及其論文《Attention is all you need》的核心思想。內(nèi)容從傳統(tǒng)Seq2Seq與RNN的并行計(jì)算瓶頸切入引出Attention機(jī)制再對Transformer的編碼器-解碼器架構(gòu)做宏觀與微觀雙重解讀逐步拆解自注意力中Q、K、V的生成、點(diǎn)積縮放、softmax加權(quán)求和等完整流程并延伸至多頭注意力、位置編碼、Padding Mask、殘差連接與LayerNorm等關(guān)鍵細(xì)節(jié)最后補(bǔ)充訓(xùn)練階段要點(diǎn)與推理階段的自回歸解碼策略。資源為單個(gè)pptx文件壓縮包約17.08MB結(jié)構(gòu)清晰、圖文并茂適合對照論文逐節(jié)研讀。目前已有3548人學(xué)習(xí)可作為課堂講義或自學(xué)筆記幫助讀者建立從Seq2Seq到Transformer的完整知識脈絡(luò)并為后續(xù)理解BERT、GPT等預(yù)訓(xùn)練模型打下基礎(chǔ)。1. 從一份 PPT 到能跑通的 Transformer先搞清楚它到底在算什么很多人第一次接觸 Transformer是在一份名為「Transformer詳解.pptx」的分享材料里?;脽羝袭嬛?Encoder-Decoder 堆疊、多頭注意力方框、位置編碼的正余弦公式看完覺得懂了合上電腦卻寫不出一行能跑的代碼。問題不在數(shù)學(xué)而在于 PPT 把「數(shù)據(jù)怎么流、張量怎么變、梯度怎么回傳」這三件事壓縮成了靜態(tài)圖。Transformer 的核心只有一句話用自注意力替代循環(huán)讓序列中任意兩個(gè)位置直接建立聯(lián)系。它解決的是 RNN 長距離依賴衰減、CNN 感受野受限的問題適合機(jī)器翻譯、文本分類、圖像分類ViT、時(shí)間序列回歸等場景。適合誰讀寫過 PyTorch 基礎(chǔ)層、能看懂矩陣乘法、想從「看得懂圖」跨到「手撕得出來」的工程師。接下來按「原理立住 → 最小實(shí)現(xiàn) → 訓(xùn)練實(shí)戰(zhàn) → 排錯(cuò)進(jìn)階」推一遍每步都給可復(fù)現(xiàn)的代碼和參數(shù)。2. Transformer 架構(gòu)拆解編碼器層數(shù)、注意力頭數(shù)與位置編碼怎么定2.1 編碼器到底有多少層這個(gè)數(shù)字從哪來熱搜里常出現(xiàn)「transformer 編碼部分有多少編碼器呢」。答案不是固定的原始論文《Attention Is All You Need》里base 模型編碼器和解碼器各堆 6 層big 模型各 6 層但維度更大。層數(shù)N是超參不是架構(gòu)常量。層數(shù)越多感受野和表達(dá)能力越強(qiáng)但顯存和訓(xùn)練時(shí)間線性上漲。常見做法是小數(shù)據(jù)集分類任務(wù)用 24 層機(jī)器翻譯用 6 層大規(guī)模預(yù)訓(xùn)練用 1224 層。判斷依據(jù)是任務(wù)復(fù)雜度和數(shù)據(jù)量不是「越多越好」。層數(shù)堆過頭小數(shù)據(jù)上直接過擬合驗(yàn)證集 loss 先降后升。2.2 多頭注意力頭數(shù)、維度與縮放因子多頭注意力把d_model拆成h個(gè)頭每個(gè)頭維度d_k d_model / h。原始論文 base 用d_model512, h8, d_k64。縮放因子1/sqrt(d_k)是為了防止點(diǎn)積過大導(dǎo)致 softmax 梯度消失。import torch import torch.nn as nn import math class MultiHeadAttention(nn.Module): def __init__(self, d_model512, num_heads8): super().__init__() assert d_model % num_heads 0, d_model 必須能被 num_heads 整除 self.d_model d_model self.num_heads num_heads self.d_k d_model // num_heads # Q/K/V 和輸出各一個(gè)線性層一次性算完所有頭 self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) self.w_o nn.Linear(d_model, d_model) def forward(self, q, k, v, maskNone): batch q.size(0) # 投影后拆頭: [B, L, d_model] - [B, h, L, d_k] Q self.w_q(q).view(batch, -1, self.num_heads, self.d_k).transpose(1, 2) K self.w_k(k).view(batch, -1, self.num_heads, self.d_k).transpose(1, 2) V self.w_v(v).view(batch, -1, self.num_heads, self.d_k).transpose(1, 2) # 縮放點(diǎn)積注意力 scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) out torch.matmul(attn, V) # [B, h, L, d_k] out out.transpose(1, 2).contiguous().view(batch, -1, self.d_model) return self.w_o(out)邏輯說明view transpose把d_model切成h份每個(gè)頭獨(dú)立算注意力最后拼回。masked_fill用極小值屏蔽 padding 或未來位置。參數(shù)說明d_model是模型主維度num_heads決定并行子空間數(shù)量d_k由兩者相除得到改num_heads時(shí)必須保證整除否則斷言直接報(bào)錯(cuò)。2.3 位置編碼正余弦公式與可學(xué)習(xí)嵌入的取舍Transformer 本身沒有順序概念位置信息必須顯式注入。原始論文用固定正余弦class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) # [1, max_len, d_model] def forward(self, x): return x self.pe[:, :x.size(1)]register_buffer讓pe隨模型搬到 GPU 但不參與梯度。偶數(shù)維用 sin、奇數(shù)維用 cos不同頻率保證每個(gè)位置編碼唯一。常見做法是翻譯類任務(wù)用固定編碼視覺和長序列任務(wù)用可學(xué)習(xí)位置嵌入或相對位置編碼后者對超出訓(xùn)練長度的序列泛化更好。配置項(xiàng)原始論文 base小數(shù)據(jù)分類常用說明d_model512128256主維度越大越吃顯存num_heads848必須整除 d_modelencoder layers624數(shù)據(jù)少就減層d_ff20484×d_model前饋層中間維度dropout0.10.10.3小數(shù)據(jù)調(diào)大3. 手寫一個(gè)最小 Transformer 并跑通前向傳播3.1 編碼器層與前饋網(wǎng)絡(luò)的拼裝一個(gè)編碼器層 多頭注意力 殘差 LayerNorm 前饋網(wǎng)絡(luò) 殘差 LayerNorm。前饋就是兩層線性夾一個(gè)激活class EncoderLayer(nn.Module): def __init__(self, d_model512, num_heads8, d_ff2048, dropout0.1): super().__init__() self.attn MultiHeadAttention(d_model, num_heads) self.ffn nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # 子層1注意力 殘差 x self.norm1(x self.dropout(self.attn(x, x, x, mask))) # 子層2前饋 殘差 x self.norm2(x self.dropout(self.ffn(x))) return x這里用的是 Post-LN先殘差后歸一化原始論文寫法?,F(xiàn)在很多實(shí)現(xiàn)改成 Pre-LN先歸一化再進(jìn)子層訓(xùn)練更穩(wěn)、對學(xué)習(xí)率不敏感。邏輯上殘差保證梯度直通LayerNorm 穩(wěn)定分布。參數(shù)d_ff一般取4×d_model改小省顯存但削弱非線性表達(dá)。3.2 用正弦函數(shù)回歸驗(yàn)證模型能學(xué)熱搜里有「transformer 預(yù)測正弦函數(shù)」這是驗(yàn)證實(shí)現(xiàn)是否正確的最小閉環(huán)。構(gòu)造輸入序列預(yù)測下一時(shí)刻值import numpy as np def make_sin_data(n2000, seq_len20): x np.linspace(0, 100, n) y np.sin(x) xs, ys [], [] for i in range(n - seq_len): xs.append(y[i:iseq_len]) ys.append(y[iseq_len]) return torch.tensor(xs, dtypetorch.float32).unsqueeze(-1), torch.tensor(ys, dtypetorch.float32) class SinTransformer(nn.Module): def __init__(self, d_model64, num_heads4, num_layers2): super().__init__() self.input_proj nn.Linear(1, d_model) self.pos PositionalEncoding(d_model) self.layers nn.ModuleList([EncoderLayer(d_model, num_heads, d_model*4) for _ in range(num_layers)]) self.head nn.Linear(d_model, 1) def forward(self, x): x self.pos(self.input_proj(x)) for layer in self.layers: x layer(x) return self.head(x[:, -1, :]).squeeze(-1) # 取最后位置預(yù)測訓(xùn)練循環(huán)用 MSE 損失、Adam 優(yōu)化器學(xué)習(xí)率 1e-3跑 50 個(gè) epoch 就能擬合。邏輯說明input_proj把標(biāo)量升到d_model位置編碼注入順序編碼器層提取時(shí)序關(guān)系head取最后時(shí)間步輸出預(yù)測值。參數(shù)說明seq_len20是回看窗口num_layers2足夠擬合正弦層數(shù)多了反而震蕩。3.3 訓(xùn)練循環(huán)與損失下降的判斷model SinTransformer() opt torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.MSELoss() X, Y make_sin_data() for epoch in range(50): model.train() pred model(X) loss loss_fn(pred, Y) opt.zero_grad() loss.backward() opt.step() if epoch % 10 0: print(fepoch {epoch}, loss {loss.item():.6f})正常情況 loss 從 0.5 量級降到 1e-3 以下。如果 loss 卡在 0.2 不動先查位置編碼有沒有加、mask 是否誤屏蔽了有效位置、學(xué)習(xí)率是否過大導(dǎo)致震蕩。這套流程跑通說明注意力、殘差、歸一化的連接順序沒錯(cuò)。4. 訓(xùn)練調(diào)優(yōu)與常見報(bào)錯(cuò)從 loss 不降到顯存溢出4.1 學(xué)習(xí)率預(yù)熱與梯度裁剪Transformer 對學(xué)習(xí)率敏感原始論文用 warmup前若干步線性升溫之后按步數(shù)平方根衰減。常見做法是 warmup 4000 步、峰值學(xué)習(xí)率 1e-4 到 1e-3。梯度裁剪clip_grad_norm_(model.parameters(), 1.0)防止梯度爆炸尤其層數(shù)多時(shí)必加。def lr_lambda(step, d_model512, warmup4000): if step 0: step 1 return (d_model ** -0.5) * min(step ** -0.5, step * warmup ** -1.5) scheduler torch.optim.lr_scheduler.LambdaLR(opt, lr_lambda) # 每個(gè) step 后調(diào)用 scheduler.step() 和梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)參數(shù)說明warmup太小前期不穩(wěn)太大收斂慢max_norm一般 0.51.0太小會壓制正常梯度。4.2 顯存溢出與 batch 組織顯存溢出常見原因序列太長導(dǎo)致注意力矩陣[B, h, L, L]爆炸、d_model或?qū)訑?shù)過大、沒開混合精度。排查順序是先降 batch size再降序列長度再考慮梯度累積模擬大 batch。# 梯度累積小顯存模擬大 batch accum_steps 4 for i, batch in enumerate(loader): loss model(batch) / accum_steps loss.backward() if (i 1) % accum_steps 0: torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() opt.zero_grad()注意力復(fù)雜度是O(L2)序列翻倍顯存翻四倍。長序列場景考慮稀疏注意力或分塊計(jì)算這是 ViT、Swin Transformer 處理高分辨率圖像時(shí)的核心優(yōu)化點(diǎn)。4.3 訓(xùn)練不收斂的排查清單現(xiàn)象可能原因處理loss 不降學(xué)習(xí)率過大/過小加 warmup試 1e-4loss 變 NaN梯度爆炸梯度裁剪降 lr驗(yàn)證集先降后升過擬合加 dropout減層數(shù)輸出恒定值mask 全屏蔽檢查 mask 形狀和填充顯存溢出序列過長降 batch梯度累積提示mask 形狀錯(cuò)誤是最隱蔽的坑。padding mask 應(yīng)為[B, 1, 1, L]因果 mask 為[1, 1, L, L]廣播維度對不上會靜默算錯(cuò)。5. 進(jìn)階技巧把 Transformer 用到圖像分類與長序列5.1 ViT 與 Swin Transformer 的 patch 切分差異Vision Transformer 把圖像切成固定 patch如 16×16每個(gè) patch 展平后當(dāng) token 送進(jìn)編碼器位置編碼改成可學(xué)習(xí)二維嵌入。Swin Transformer 引入窗口注意力和層級下采樣把注意力限制在局部窗口內(nèi)復(fù)雜度從O(L2)降到線性適合高分辨率圖像分類和檢測。# ViT 的 patch 切分核心 patch_size 16 patches x.unfold(2, patch_size, patch_size).unfold(3, patch_size, patch_size) # [B, C, H/P, W/P, P, P] - [B, N, C*P*P] patches patches.contiguous().view(B, C, -1, patch_size * patch_size).transpose(1, 2)邏輯說明unfold按步長滑窗切塊再展平成 token 序列。參數(shù)說明patch_size越小 token 越多、精度越高但顯存越大常見 16 或 32。5.2 長序列的位置外推與相對位置編碼固定正余弦編碼在超出訓(xùn)練長度時(shí)性能驟降。相對位置編碼把位置差注入注意力分?jǐn)?shù)對長度外推更友好。實(shí)現(xiàn)上在scores上加一個(gè)可學(xué)習(xí)的相對位置偏置表按i-j索引。# 相對位置偏置scores [B, h, L, L] 加 bias[i-j] rel_bias nn.Parameter(torch.zeros(2 * max_len - 1, num_heads)) idx torch.arange(L).unsqueeze(0) - torch.arange(L).unsqueeze(1) max_len - 1 scores scores rel_bias[idx].permute(2, 0, 1).unsqueeze(0)參數(shù)說明max_len是支持的最大相對距離rel_bias可學(xué)習(xí)。這套做法在長文本和語音任務(wù)里比絕對位置編碼穩(wěn)。5.3 用注意力權(quán)重做可解釋性驗(yàn)證訓(xùn)練完可以取出注意力矩陣看模型關(guān)注哪里驗(yàn)證是否學(xué)到合理模式。分類任務(wù)里正確樣本的注意力往往集中在關(guān)鍵 token 上如果注意力均勻鋪滿說明模型沒學(xué)到東西回去查數(shù)據(jù)和 mask。with torch.no_grad(): attn model.layers[0].attn Q attn.w_q(x).view(B, -1, attn.num_heads, attn.d_k).transpose(1, 2) K attn.w_k(x).view(B, -1, attn.num_heads, attn.d_k).transpose(1, 2) weights torch.softmax(Q K.transpose(-2, -1) / math.sqrt(attn.d_k), dim-1) print(weights[0, 0]) # 第一個(gè)樣本第一個(gè)頭的注意力分布把這份權(quán)重畫成熱力圖橫縱軸是 token 位置顏色深淺代表關(guān)注強(qiáng)度。這是排查「模型到底看沒看對地方」最直接的手段比只看 loss 曲線信息量大得多。本文還有配套的精品資源點(diǎn)擊獲取