Transformer 原论文怎么训出来的:8 张 P100、12 小时、warmup 4000 步

上一篇我们把 Transformer block 的最后一个零件------前馈网络------讲透了。到这里,整个模型的结构已经完全摆开:embedding、位置编码、多头注意力、残差、LN、FFN、stack 6 层。

但「结构对了」不等于「能训出来」。深度学习里有一个让人头疼的事实:同一个模型架构,配方稍微一动,结果可能差几个 BLEU。学习率怎么调、warmup 多长、batch 怎么算、dropout 放哪儿、label 平滑多少------这些超参数的取值,比模型结构本身更难猜对。

2017 年原论文之所以能让人信服,不只是因为提出了新结构,更是因为他们公开了一套完整的训练配方,并在 WMT2014 这个公认的 benchmark 上跑出了当时的 SOTA。这份配方后来被几乎所有 Transformer 实现奉为「标准答案」,直到现代 LLM 时代才慢慢演化出新版本。

这一篇要做的事情,是把这个配方一项一项地拆开,讲清楚每一个数字、每一个公式背后的含义。读完之后你应该能做到:

  • 知道 base 和 big 模型分别用了多少 GPU、多长时间训出来;
  • 解释清楚那个看起来很神秘的学习率公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> lr = d − 0.5 ⋅ min ⁡ ( step − 0.5 , step ⋅ warmup − 1.5 ) \text{lr} = d^{-0.5} \cdot \min(\text{step}^{-0.5},\ \text{step} \cdot \text{warmup}^{-1.5}) </math>lr=d−0.5⋅min(step−0.5, step⋅warmup−1.5);
  • 回答「warmup_steps 为什么是 4000,去掉会怎样」;
  • 知道 batch by tokens 是什么、为什么不是 batch by samples;
  • 在你自己的项目里复现一份对得上原论文 BLEU 的训练;
  • 理解现代大模型(GPT-3、LLaMA)训练配方相对 2017 的演化。

先把整套配方一次性贴出来,然后我们一节一节地拆:

scss 复制代码
模型:    Transformer base / big
数据集:  WMT 2014 En-De(4.5M 句对)/ En-Fr(36M 句对)
硬件:    8 × NVIDIA P100 GPU
训练时长:base 12 小时(100k 步) / big 3.5 天(300k 步)
batch:   每步约 25000 source token + 25000 target token
优化器:  Adam, β₁=0.9, β₂=0.98, ε=1e-9
学习率:  lr = d^(-0.5) · min(step^(-0.5), step · warmup^(-1.5))
warmup:  4000 步
正则化:  dropout 0.1(embedding 之后、每个子层输出)
          label smoothing 0.1
推理:    beam search, beam=4, length penalty α=0.6
最终:    En-De BLEU 27.3 (base) / 28.4 (big)
          En-Fr BLEU 38.1 (base) / 41.8 (big)

每一行都有故事。

原文链接

一、数据集与基准任务

1.1 WMT 2014:那个时代的 ImageNet

WMT(Workshop on Machine Translation)从 2006 年开始举办,是机器翻译领域的标准评测。每届会发布若干语言对的训练数据、开发集、测试集,参赛者提交翻译结果,组委会统一评分(人工或 BLEU)。

2017 年原论文用的是 WMT 2014 的两组数据:

  • En-De(英语-德语):约 4.5M 句对,主要来自 Europarl、Common Crawl、News Commentary。这是当时最常用的 MT 基准之一,因为德语形态变化丰富、复合词长,对翻译模型的考验比 En-Fr 更大;
  • En-Fr(英语-法语):约 36M 句对,规模是 En-De 的 8 倍。法语和英语在词汇、语法上更接近,但数据量更大,挑战的是模型在大数据上能不能持续受益。

两个任务的官方测试集是 newstest2014,每对约 3000 个句子。BLEU(Bilingual Evaluation Understudy)分数是在这个测试集上算出来的------具体说,是用 SacreBLEU 或 multi-bleu 这样的工具,把模型翻译和参考译文比较 n-gram 匹配率。

为什么选这两组而不是别的?因为它们在 2017 年是「公认的硬骨头」------之前几年的 SOTA 模型(GNMT、ConvS2S、ByteNet)都在这上面打榜,已经形成了稳定的对比基线。

1.2 数据预处理

原论文用的是字节对编码(Byte Pair Encoding,BPE)做子词切分,词表大小约 37000(En-De)或 32000(En-Fr)。这个细节后面 29 篇会专门讲,这里只要记住:

  • 不是按词切(OOV 严重),不是按字符切(序列太长),而是按子词切;
  • 源语言和目标语言共享词表------这一点很关键,让 encoder 和 decoder 的 embedding 矩阵可以共享。

句子按长度排序后再分 batch(后面讲 batching 时再展开),最大长度截到 100 token 左右。

1.3 为什么这个任务现在看起来「小」

在 GPT-3 那个 570GB 数据、1750 亿参数的时代,回头看 WMT 2014 的几百万句对、6 千万参数的 Transformer,会觉得「就这?」

但要明白:2017 年这是相当不小的工程。8 张 P100 在那时候是顶级配置(A100、H100 还要等 4-5 年才出来),单机 8 卡训 12 小时跑出 SOTA,是一个大组才能做的实验。

而且更关键的是:原论文的训练配方在它的尺度下是经过仔细调过的------后来人复现,照搬这套配方,结果稳定可复现;这是一个工程上的重要保证。


二、硬件与训练时长

2.1 8 张 P100 是什么概念

NVIDIA P100(2016 年发布):

  • 16GB HBM2 显存;
  • FP16 算力约 19 TFLOPS(FP32 约 9 TFLOPS);
  • 单卡功耗 250W;
  • 当时 NVLink 也支持,但带宽比后来的 V100/A100 低不少。

对比 2024 年的 H100:

  • 80GB HBM3,是 P100 的 5 倍;
  • BF16 算力约 989 TFLOPS(不算稀疏化),是 P100 FP16 的 ~50 倍;
  • HBM 带宽 3.35 TB/s,是 P100 的 ~7 倍。

也就是说,今天一张 H100 大概顶 50-60 张 2016 年的 P100。原论文「8 张 P100」的总算力,按今天看大约相当于 0.15 张 H100。这能训出 SOTA 翻译模型,确实是工程上的奇迹。

2.2 base 12 小时、big 3.5 天

  • base 模型:d=512, n_layers=6, n_heads=8, d_ff=2048。约 65M 参数。训 100k 步,每步约 0.4 秒(8 P100 同时算),合计约 11.1 小时(论文说「about 12 hours」)。
  • big 模型:d=1024, n_layers=6, n_heads=16, d_ff=4096。约 213M 参数。训 300k 步,每步约 1 秒,合计约 83 小时(论文说「3.5 days」)。

两个模型的训练时长,主要由模型大小、序列长度、batch 大小三个因素决定。换到现代 GPU,整个训练时长会被压缩到几小时甚至更短------但配方本身不会变。

2.3 多卡同步

8 卡之间的同步方式是「数据并行」(Data Parallel):每张卡拿一份 batch 子集,独立前向反向,最后用 all-reduce 把梯度加到一起,再各自更新参数(参数完全相同)。

具体的 all-reduce 实现,原论文没细说,应该用的是 NCCL 或类似的工具。因为只有 8 卡同机,all-reduce 的通信成本不大;如果跨机,就要考虑 ring all-reduce、梯度压缩等。

后来 GPT-3、LLaMA 的训练用了「数据并行 + 流水线并行 + 张量并行」三套并行方案叠加(3D Parallelism),那是为千卡规模准备的;2017 年单机 8 卡的尺度,纯数据并行就够了。


三、优化器:Adam 的微调

3.1 Adam 简介

Adam(Adaptive Moment Estimation,Kingma & Ba, 2014)是当时最流行的自适应优化器。它维护两个移动平均:

  • 一阶矩 m(梯度的指数加权平均,类似动量);
  • 二阶矩 v(梯度平方的指数加权平均,类似方差估计)。

更新规则:

arduino 复制代码
m_t = β₁ · m_{t-1} + (1 - β₁) · g_t
v_t = β₂ · v_{t-1} + (1 - β₂) · g_t²
m̂_t = m_t / (1 - β₁^t)        # bias correction
v̂_t = v_t / (1 - β₂^t)
θ_t = θ_{t-1} - lr · m̂_t / (√v̂_t + ε)

直觉:m 给方向(带动量),v 给步长(在梯度方差大的方向走小步、方差小的方向走大步)。Adam 比纯 SGD 在多数任务上更稳更快,缺点是参数多一份(每个参数要存 m 和 v,显存占用 3x)。

3.2 原论文的 β₂ = 0.98(注意不是 0.999)

Adam 默认 β₂ = 0.999,但原论文用了 β₂ = 0.98。这是个小但重要的差异。

β₂ 控制 v(梯度平方的移动平均)的「记忆长度」。β₂ 越大,记忆越长,对噪声越鲁棒,但对学习率变化的响应越慢;β₂ 越小,记忆越短,对最近梯度更敏感。

β₂ = 0.999 对应「记忆 1000 步」,这个对长训练(百万步)很合适,但对 100k 步的训练就太长了------v̂ 的 bias correction 在前几千步还很大,可能导致更新不稳定。

β₂ = 0.98 对应「记忆 50 步」,更适合短训练,让 v 能更快跟上学习率的变化。这是原论文调出来的------后人复现 Transformer 翻译都沿用 0.98。

到了 GPT-3 时代,因为训练步数大幅增加(百万级),β₂ 又调回了 0.95 或 0.999。

3.3 ε = 1e-9(注意不是 1e-8)

Adam 的 ε 是分母里防止除零的小常数,默认 1e-8。原论文用 1e-9------比默认小一个数量级。

这个差异更细,影响主要在「梯度平方接近 0」时的步长。ε 小,分母可以更小,更新步长更大;ε 大,分母被压住,更新步长偏小但更稳。

实际复现时,ε 用 1e-8 还是 1e-9 影响不大,但要保持配方一致以便对照论文结果。

3.4 没有 weight decay

原论文没有 weight decay(权重衰减),这是另一个值得注意的点。

现代大模型(GPT-3、LLaMA)几乎都用 AdamW(Adam + decoupled weight decay),weight decay 设到 0.1 或 0.01。原论文那时还没有 AdamW(Loshchilov & Hutter 2019 才提出),他们也没用普通的 L2 正则。

为什么没用?我猜测两个原因:

  1. 当时 dropout + label smoothing 已经提供了相当充分的正则化;
  2. 翻译任务的训练数据相对模型容量已经足够大,过拟合不是主要矛盾。

到了现代大模型,因为参数量爆炸、数据相对模型容量未必充裕,weight decay 就重新成为标配了。


四、学习率公式:那个看起来很神秘的式子

4.1 公式与图像

原论文的学习率公式是:

scss 复制代码
lr(step) = d_model^(-0.5) · min(step^(-0.5), step · warmup_steps^(-1.5))

第一次看这个公式,多数人都会愣一下:为什么是这种形式?三个超参数(d_model、step、warmup_steps)混在一起,min 还把两支函数粘在一起。

把它拆开看就清楚了。min 里面是两个函数:

  • 函数 A: <math xmlns="http://www.w3.org/1998/Math/MathML"> step ⋅ warmup_steps − 1.5 \text{step} \cdot \text{warmup\_steps}^{-1.5} </math>step⋅warmup_steps−1.5 ------ 关于 step 是线性增长,斜率由 warmup_steps 决定;
  • 函数 B: <math xmlns="http://www.w3.org/1998/Math/MathML"> step − 0.5 \text{step}^{-0.5} </math>step−0.5 ------ 关于 step 是平方根衰减

两者交点在 <math xmlns="http://www.w3.org/1998/Math/MathML"> step = warmup_steps \text{step} = \text{warmup\_steps} </math>step=warmup_steps 处------你可以代入验证:当 step = warmup_steps 时,A = warmup_steps · warmup_steps^(-1.5) = warmup_steps^(-0.5) = B。

所以这个公式描述的是一条「先线性升、再 1/√step 衰减」的曲线,转折点在 warmup_steps 步:

  • step < warmup_steps:lr 沿 A 线性增长,从 0 升到峰值 lr_peak = d^(-0.5) · warmup^(-0.5);
  • step > warmup_steps:lr 沿 B 衰减,按 1/√step 慢慢降。

4.2 为什么先 warmup

「为什么要 warmup」是个非常有意思的问题,2017 年的时候大家也没完全说清楚,到 2020 年前后才有比较像样的理论解释。

直觉版的解释是这样的:

训练刚开始时,模型参数是随机初始化的,梯度方向「指向哪儿」非常嘈杂。Adam 的 v(梯度平方移动平均)需要积累几百到几千步才能给出一个相对靠谱的「方差估计」。

如果你一开始就用大学习率,会发生什么?

  • 模型参数被一组嘈杂的梯度推到一个奇怪的位置;
  • 这个位置可能离合理的解空间很远,后续训练即使学习率慢慢降下来也很难恢复------「陷入坏的局部 basin」;
  • LayerNorm 或 BN 的统计量也可能因此偏离正常分布;
  • attention softmax 可能输出极端 sharp 的分布(某个权重接近 1,其它接近 0),梯度几乎流不动。

warmup 的作用是「给优化器和模型几千步缓冲,让 v 估计稳定下来、让参数从随机初始化温和地移动」。等到 v 比较准了,再放开学习率。

4.3 warmup_steps = 4000:魔法常数

为什么是 4000 步?更准确地说,4000 是原论文在那套模型规模、batch 和硬件条件下选中的经验值。

从论文和后续公开复现里能看到一个稳定现象:warmup 太短,甚至直接去掉,训练更容易不稳定;warmup 太长,又会让前期大量 step 都停留在偏低的 lr 上,收敛速度变慢。4000 在当时那套配方里处在一个比较稳的折中点。

所以 4000 不是神秘常数,也不是所有 Transformer 都该照抄。它只是把「先稳住,再放开」这件事具体化到原论文那一档训练尺度上。

到了现代大模型,显式 warmup 仍然普遍存在,只是汇报口径常常不同:有的按 step 写,有的按 token 写,不能直接拿一个比例和原论文对齐。比如 GPT-3 写的是 375M tokens warmup,LLaMA-2 报的是 2000 steps。更稳妥的比较是:这些训练都没有把 warmup 彻底拿掉,只是在各自的训练长度和 batch 规模下重新选了尺度。

4.4 d_model^(-0.5) 是怎么来的

公式里 d_model^(-0.5) 这个因子的作用是「让学习率随模型宽度自动适配」。

直觉是:模型越宽,每个矩阵乘的输出方差越大;为了让参数更新幅度不随宽度爆炸,学习率要随宽度的某个幂次降下来。Vaswani 等人选了 -0.5 这个幂次,有点类似 Glorot/He 初始化的 1/√d 那个量。

具体说,对于 d=512:lr_peak = 512^(-0.5) · 4000^(-0.5) = (1/22.6) · (1/63.2) ≈ 7e-4。

对于 d=1024(big):lr_peak = 1024^(-0.5) · 4000^(-0.5) ≈ 4.95e-4。

big 模型的峰值学习率自动比 base 低一些------这正是「宽模型不能用太大 lr」这条经验的数学化。

4.5 后续 1/√step 衰减

warmup 之后按 1/√step 衰减。这个衰减率比 1/step(线性衰减)慢,比常数(不衰减)快------属于「中等强度」的衰减。

直觉是:

  • 训练早期,梯度大、变化快,需要大步长;
  • 训练后期,梯度小、变化慢,需要小步长精修;
  • 1/√step 让步长在 100k 步内从峰值降到约 1/10------既不会让训练后期完全停下,也不会让 lr 维持太高。

后来有些工作(GPT-3、LLaMA)改用「cosine decay」------按 cos 函数从峰值降到一个最小值(通常是峰值的 10%)。cosine 在工程上更平滑,但和 1/√step 在效果上差异不大。


五、Label Smoothing 0.1

5.1 一个 one-hot 的问题

标准的语言建模损失函数是交叉熵:模型给每个词输出一个概率分布 p,目标是让 p 与「真实分布 q」尽可能接近。

但「真实分布 q」长什么样?通常是 one-hot------目标词的概率是 1,其它所有词的概率是 0。

这个 one-hot 目标有两个问题:

  1. 过自信(overconfident):模型被推着把目标词的概率推到 1。要做到这点,目标词的 logit 必须远远大于其它词的 logit------softmax 的指数性意味着「无穷大的 logit 差距才能推出 1.0」。模型倾向于输出极端 sharp 的分布,这种「过度自信」反而损害泛化;
  2. 零概率惩罚不可控:如果模型给某个非目标词分配了一点点概率(比如 0.001),交叉熵会照样惩罚它(因为目标分布是 0),这种惩罚对于罕见词的处理有时过于严格。

5.2 Label smoothing 是怎么做的

label smoothing(Szegedy et al., 2016, Rethinking the Inception Architecture)的做法是把 one-hot 目标稍微「软化」:

ini 复制代码
q_i = (1 - ε) · 1[i = target] + ε / V

其中 ε 是平滑系数(原论文取 0.1),V 是词表大小。

直观上:目标词的概率从 1 变成 0.9,剩下 0.1 平均分给所有其它词。

这样一来:

  • 模型不再被推到「目标词概率 = 1」的极端;
  • 它学到的分布会自然「软」一些,预测时其它候选词也会有合理概率;
  • 模型 perplexity 反而可能变差(因为 loss 的最优解不再是 sharp 分布),但 BLEU 通常变好。

5.3 为什么 PPL 变差但 BLEU 变好

这是 label smoothing 的「反直觉」之处。

PPL(perplexity)= exp(交叉熵),它衡量的是「模型对真实数据的概率估计」。如果你让模型「不那么自信」,它给目标词的概率自然降低,PPL 自然变差。

但 BLEU 衡量的是翻译质量------具体说,beam search 解码出的句子和参考译文的 n-gram 匹配率。这里关键的是 beam search 的多样性:当模型分布太 sharp 时,beam search 早期就被某条路径锁定,错过更好的路径;分布柔和一点,beam search 能保留更多备选,最终找到更优解。

label smoothing 的好处主要体现在 beam search 的过程中------它让模型给「次优词」保留合理概率,beam search 能在这些次优词上展开更深的搜索。

原论文的 ε = 0.1 是经验值。后来很多工作沿用,但也有些尝试不同值(0.05、0.15)的,差异不大。

5.4 现代大模型还用吗

GPT 系列、LLaMA 系列基本不用 label smoothing。原因之一是:自回归生成(一步一个 token)在 beam search 不再常用(greedy 或 sampling 是主流);原因之二是:超大词表(50k+)下,label smoothing 把 ε 平摊到几万个词上,每个词得到的「软概率」太小,效果不明显。

但翻译任务、专门用 beam search 解码的场景,label smoothing 仍然是标配。


六、Dropout 0.1:放在哪些位置

dropout(Srivastava et al., 2014)是经典的正则化方法:训练时按一定概率随机把激活值置零,强制网络不依赖特定神经元。

原论文的 dropout 配置是 P_drop = 0.1,加在以下位置:

  1. Embedding + 位置编码之后:把 token embedding 和 PE 加起来后过一次 dropout;
  2. 每个子层的输出:attention 子层和 FFN 子层的输出,在加到残差之前过一次 dropout。

具体到一个 block 的写法:

scss 复制代码
x' = x + dropout(Attention(x))
y  = x' + dropout(FFN(x'))

注意 dropout 是在「子层输出」上做的,不是在 attention 的内部 softmax 上。后来很多实现还会额外加:

  • attention dropout(在 softmax 后的注意力权重上);
  • FFN 中间 dropout(在 ReLU 之后)。

这些是从 fairseq、Tensor2Tensor 等开源实现里来的微调,原论文没明确说。

6.1 为什么 0.1 而不是 0.5

经典视觉模型(VGG、ResNet 上的 dropout)常用 0.5。Transformer 用 0.1,差很多。

直觉是:Transformer 的层数和参数量已经很大,加上 LN、残差、label smoothing、weight tying 等多重正则,再加重的 dropout 反而让训练不稳定、收敛变慢。0.1 是「轻量正则化」,给训练加点扰动就够。

6.2 现代大模型里 dropout 几乎被放弃

GPT-3 训练时 dropout=0(取消了),LLaMA 系列也是。原因:

  • 数据量足够大(千亿 token),过拟合不是主要矛盾;
  • dropout 让训练 step 变多(每步要重做随机),算力浪费;
  • 大模型的隐式正则(参数压缩、scaling 本身的平均效应)已经够用。

但如果你做小数据量的 fine-tune,dropout 又会重新有用。它是「数据-模型容量比」的函数:数据相对模型小时有用,反过来没用。


七、Batching by Tokens:长度归一化的妙招

7.1 一个常被忽视的设计

原论文里有一句话:「Each training batch contained a set of sentence pairs containing approximately 25000 source tokens and 25000 target tokens.」

这句话的意思是:每个 batch 的大小不是「固定句对数」,而是「固定 token 数」

这是一个非常重要的工程细节,但很多教程一带而过。

7.2 为什么不用固定 batch_size

机器翻译的句子长度差异巨大------短的 5 个词,长的 100 个词。如果你按固定 batch_size = 64 做:

  • 一个全是短句的 batch:64 × 8 = 512 token,GPU 利用率极低;
  • 一个全是长句的 batch:64 × 80 = 5120 token,可能爆显存。

更糟糕的是,显存占用主要由 batch 内最长句子决定------padding 让 batch 实际大小是 64 × max_len,短句白白占着位置。

按 token 数 batch,行为完全不同:

  • 短句的 batch:50 句 × 平均 10 词 ≈ 500 token;动态扩到 50 句;
  • 长句的 batch:5 句 × 平均 100 词 ≈ 500 token;动态缩到 5 句。

显存占用稳定,GPU 利用率最大化。

7.3 实现:buckets + dynamic batch

典型实现是把训练数据按长度分桶(bucket),每个桶内长度相近:

yaml 复制代码
bucket 0: 句子长度 1-10
bucket 1: 句子长度 11-20
...
bucket 9: 句子长度 91-100

每次从一个 bucket 抽样,凑够 25000 token 就提交一个 batch。这样既保证 batch 内长度均匀(padding 浪费小),又能在 token 维度上保持稳定的 batch 大小。

fairseq、Tensor2Tensor 的实现都是这一套。

7.4 现代 LLM 怎么做

GPT-3、LLaMA 训练时同样按 token 数算 batch,但因为是单语自回归(不是翻译),处理简单一些:把所有训练文本拼成一长串、按固定上下文长度(2048、4096)切,每片就是一个样本。这样每个样本天然等长,padding 完全不需要。

但 token-based batching 的核心思想------「用 token 作为基本计算单位、而不是 sample」------一直延续到今天。


8.1 为什么不能 greedy

训练时模型用「teacher forcing」------每一步看真实历史预测下一个词。但推理时没有真实历史,必须用模型自己生成的词作为下一步输入。

最直接的做法是 greedy:每一步选概率最高的词。但 greedy 有一个致命问题------「早期局部最优锁死后续」。比如:

  • step 1 选了 "A",因为 P(A) = 0.4 > P(B) = 0.35;
  • 但如果选 "B",后面整体的 P(B + rest) 可能远大于 P(A + rest)。

greedy 看不到这个,它只看每一步当下的最优。

beam search 的想法是「保留多个候选路径」:

  • step 1:选概率最高的 k 个词作为「beam」(k = beam size,原论文取 4);
  • step 2:对每个 beam,扩展所有 V 个候选词,得到 k × V 条路径;从中再挑概率最高的 k 条;
  • step 3:继续扩展,直到所有 beam 都生成 </s> 或达到最大长度;
  • 最后从 k 个完成的句子中挑总概率(log-sum)最高的输出。

beam search 不能保证找到全局最优解(那需要遍历所有路径,exponential),但比 greedy 显著提升翻译质量。

8.3 length penalty α = 0.6

beam search 有一个臭名昭著的 bias:短句子总概率高。因为每生成一个词都要乘一个 P < 1,句子越长概率越低,beam search 倾向输出短句。

length penalty 的作用是抵消这个 bias。原论文用 GNMT 提出的版本:

scss 复制代码
score(y) = log P(y) / lp(|y|)
lp(|y|) = ((5 + |y|) / 6)^α

α = 0.6 时,lp(|y|) 会随着长度增加而增大;用 log P(y) 除以它,相当于对更长的候选少一些惩罚,从而抵消 beam search 对短句的天然偏好。

实际效果:α = 0 时 beam search 输出明显偏短,α = 1 时偏长,α = 0.6 是中间的甜点。

GPT-3、LLaMA 的对话生成几乎都不用 beam search,而是用 temperature sampling、top-k、top-p(nucleus sampling)。原因:

  • beam search 的输出「过于安全」,多样性差;
  • 对话生成要的是「合理且有趣」,不是「严格意义上的最高概率」;
  • 长生成时 beam search 算力开销大,sampling 一步一个 token 即可。

但翻译、摘要这类「有标准答案」的任务,beam search 仍然占主流。


九、复现性:fairseq、Tensor2Tensor 的微妙差异

原论文公开了大部分细节,但仍然有一些边角细节没明确写。后人的复现实现(Google 自家的 Tensor2Tensor、Facebook 的 fairseq、HuggingFace 的 transformers)在以下点上有差异:

9.1 Pre-LN vs Post-LN

原论文是 Post-LN(每个子层之后做 LN)。但后来发现这个写法在大尺度下训练不稳定------梯度在深层会爆炸或消失。

Pre-LN(先 LN 再过子层)由 Xiong et al. 2020 系统分析后成为主流。GPT-3、LLaMA 都用 Pre-LN。

但在 base 6 层的尺度上,Post-LN 和 Pre-LN 表现相近,原论文用 Post-LN 没问题。

9.2 Embedding 缩放

原论文有一行:「In the embedding layers, we multiply those weights by √d_model.」

也就是 token embedding 出来后要乘以 √d。这是为了让 embedding 的方差和位置编码的方差对齐------位置编码 sinusoidal 的幅度大约是 1,而 embedding 初始化方差 1/d,乘 √d 后方差变成 1。

这是论文里写了的,但很容易漏。fairseq 默认乘了,HuggingFace 的早期 transformers 实现里有过 bug。

9.3 Dropout 的精确位置

前面提到,原论文的 dropout 加在「子层输出」「embedding 之后」。但具体到细节:

  • 是 LN 之前还是 LN 之后?fairseq 是 LN 后;T2T 早期是 LN 前;
  • attention softmax 后要不要加 dropout?fairseq 加了(rate=0.1),T2T 没加。

这些差异让不同实现的 BLEU 在 ±0.2 范围内浮动。

9.4 初始化

原论文没明确说初始化方法。Xavier 是默认。但具体是 Xavier-uniform 还是 Xavier-normal?fan_in 还是 (fan_in + fan_out) / 2?这些细节不同实现有差异。

后来 GPT-2 系列用了一个特殊缩放:W₂(FFN 输出投影)和 W_O(attention 输出投影)的初始化额外乘 1/√(2 n_layers),目的是让深层残差累积方差不爆。这是 GPT-2 的细节,原 Transformer 没用。

9.5 总结

如果你照着论文复现,真正要对齐的不是某一个「神奇参数」,而是一串小细节是不是同时一致:LN 放在哪里、embedding 是否乘 <math xmlns="http://www.w3.org/1998/Math/MathML"> d \sqrt{d} </math>d 、dropout 放在哪一层、解码时用什么 BLEU 脚本,以及最后是否做 checkpoint averaging。fairseq 可以作为一个对照实现,但更重要的是把「论文明确写出的部分」和「开源实现补出来的部分」分开看。


十、训练曲线应该怎么看

原论文没有给出完整的 step-by-step 曲线,所以这里更合适的说法不是「精确到每个阶段 loss 多少」,而是「看曲线形状」。公开复现通常会看到三件事:warmup 阶段下降最快;中段进入稳定下降区;后段 BLEU 继续涨,但边际收益明显变小。

把图当成示意图更合适,而不是某一份官方日志的逐点复刻。你真正该关心的是:曲线有没有在 warmup 结束前后突然失稳,验证集是否和训练集大体同向,以及 50k 之后收益是否已经开始明显变慢。


十一、现代大模型 vs 2017 年配方

时隔 8 年,原论文的训练配方哪些被沿用、哪些被改了?

11.1 沿用的部分

  • Adam 类优化器:仍然主流(AdamW 是它的改良版);
  • warmup:所有大模型都有,warmup_steps 的比例和 2017 年一个量级;
  • token-based batching:基本没变;
  • 学习率衰减:1/√step 换成了 cosine,但形式相近;
  • embedding 缩放、weight tying:还在用。

11.2 改了的部分

  • β₂:从 0.98 调到 0.95 或 0.999(依训练长度);
  • weight decay:从 0 加到 0.1(AdamW);
  • dropout:从 0.1 减到 0;
  • label smoothing:从 0.1 减到 0;
  • batch size:从 25000 token 升到几百万 token(LLaMA-2 用 4M token);
  • 学习率峰值:相对小(~3e-4),因为模型大、batch 大;
  • 训练 token:从 ~30 亿(En-Fr)升到 1.4-15 万亿(LLaMA-2、LLaMA-3);
  • 混合精度:FP16/BF16 + loss scaling,原论文是 FP32。

11.3 没变的核心

最有意思的观察是:虽然 scale 变了 1000 倍,但训练配方的「形状」基本没变

  • 仍然是 Adam 系;
  • 仍然有 warmup;
  • 仍然按 token batch;
  • 仍然要小心初始化;
  • 仍然需要监控梯度范数、loss 曲线、验证 PPL。

这背后其实有一个深层原因:梯度下降优化的几何性质并不随模型规模变化太多。warmup 还是要 warmup(梯度估计需要稳定时间),衰减还是要衰减(后期需要小步精修)------这些「软规律」在不同 scale 下都有效。

11.4 一份对照表

把原论文 vs LLaMA-2 vs GPT-3 的配方放到一张表里,差异看得很清楚:

配方项 Transformer base (2017) GPT-3 175B (2020) LLaMA-2 70B (2023)
优化器 Adam Adam AdamW
β₁ 0.9 0.9 0.9
β₂ 0.98 0.95 0.95
ε 1e-9 1e-8 1e-5
weight decay 0 0.1 0.1
warmup steps 4000 375M tokens 2000
LR schedule inverse √step cosine cosine
LR peak ~7e-4 (d=512) 6e-5 (d=12288) 1.5e-4 (d=8192)
dropout 0.1 0 0
label smoothing 0.1 0 0
batch (tokens) 25k src + 25k tgt 3.2M 4M
训练 token ~3B (En-Fr) 300B 2T
训练步数 100k (base) / 300k (big) ~95k ~500k
精度 FP32 FP16 + dynamic loss scale BF16
梯度 clip 0.0 (无) 1.0 1.0

读这张表的几个有趣观察:

  • 学习率峰值随模型变大而变小:从 7e-4 到 6e-5,约一个数量级;和 d^(-0.5) 的预测吻合;
  • batch size 涨了 100 倍:从 25k token 到几百万 token;
  • 训练 token 涨了 1000 倍:从 3B 到 2T;这是 Chinchilla scaling law 之后大家才意识到「数据是瓶颈」;
  • 正则化几乎全部砍掉:dropout=0、label smoothing=0、但加了 weight decay;
  • 梯度 clip 普遍开启:现代训练不再相信 schedule 能压住所有 outlier,gradient clip 是保险。

十二、训练成本与论文影响

12.1 算成本

把 base 模型的训练 FLOPs 估算一下:

  • 每步 25000 source token + 25000 target token;
  • 每个 token 大约 2 × 65M = 130M FLOPs(前向);
  • 反向是前向的 ~2 倍:3 × 130M = 390M FLOPs/token;
  • 每步:50000 × 390M = 1.95e13 FLOPs;
  • 总步数 100000:1.95e18 FLOPs ≈ 2 PFLOPs。

8 张 P100 的 FP16 算力 ≈ 8 × 19 = 152 TFLOPS。如果利用率 50%,单步耗时 ≈ 1.95e13 / 7.6e13 ≈ 0.26 秒;100k 步 ≈ 7.2 小时。考虑数据加载、通信开销,实际 12 小时合理。

12.2 与对比方法

原论文 Table 2 给出了和 ConvS2S、GNMT、ByteNet 的对比。Transformer base 在 BLEU 上同时打过这三家,而且训练成本只有 GNMT 的 ~1/10、ConvS2S 的 ~1/30。

这个「算力 × 性能」的甜蜜点,是 Transformer 当年 take over 整个 NLP 的核心动力------不是它能做新事情,而是它能用更少算力做得更好

12.3 影响力

到 2024 年,Attention Is All You Need 的引用数已经超过 12 万次(Google Scholar 统计),是 21 世纪被引最多的 AI 论文之一。

更重要的是:几乎所有现代大语言模型------GPT 系列、Claude、Gemini、LLaMA、Qwen、DeepSeek------都基于这个 2017 年提出的架构,只是 scale 大了 1000 倍、加了一些边角改进(RoPE 位置编码、SwiGLU FFN、RMSNorm 等)。

架构稳定 8 年是非常罕见的。深度学习历史上,每隔几年就有「这次真的不一样」的新架构出现(LSTM → GRU → Transformer),但 Transformer 的根基一直没被撼动。


十三、一个手算的学习率示例

为了让公式从纸上走到具象,把原论文的配方带几个具体的 step 进去算一下。

设 d_model = 512, warmup_steps = 4000。

step = 1

  • 函数 A:1 · 4000^(-1.5) = 1 / 252982 ≈ 3.95e-6
  • 函数 B:1^(-0.5) = 1.0
  • min(A, B) = A = 3.95e-6
  • lr = 512^(-0.5) · 3.95e-6 = (1/22.627) · 3.95e-6 ≈ 1.75e-7

非常小的学习率。这是 warmup 第一步的样子------模型几乎不动,等 v 估计稳定。

step = 1000

  • 函数 A:1000 · 4000^(-1.5) ≈ 3.95e-3
  • 函数 B:1000^(-0.5) ≈ 0.0316
  • min = A = 3.95e-3
  • lr ≈ (1/22.627) · 3.95e-3 ≈ 1.75e-4

已经不算小,但还在 warmup 中(step < 4000),仍按线性增长。

step = 4000(warmup 完成的拐点):

  • 函数 A:4000 · 4000^(-1.5) = 4000^(-0.5) ≈ 0.01581
  • 函数 B:4000^(-0.5) ≈ 0.01581
  • 两者相等
  • lr = (1/22.627) · 0.01581 ≈ 6.99e-4

这是峰值学习率 ≈ 7e-4。论文配方就是按这个峰值的。

step = 16000(4 倍 warmup):

  • 函数 B:16000^(-0.5) ≈ 7.91e-3
  • lr ≈ (1/22.627) · 7.91e-3 ≈ 3.5e-4

峰值的一半。

step = 100000(base 训练结束):

  • 函数 B:100000^(-0.5) ≈ 3.16e-3
  • lr ≈ (1/22.627) · 3.16e-3 ≈ 1.4e-4

降到峰值的 1/5 左右。这是训练后期的精修阶段。

把这 5 个 step 连起来看,就是图里那条曲线------先线性快速升到 7e-4,然后慢慢降到 1.4e-4,整个过程跨越 100k 步。

13.1 改了 warmup 会怎样

把 warmup 从 4000 改成 8000,重算:

step = 4000(中点):

  • A:4000 · 8000^(-1.5) = 4000 · 1/715541 ≈ 5.59e-3
  • B:4000^(-0.5) ≈ 0.0158
  • min = A = 5.59e-3
  • lr ≈ 5.59e-3 / 22.627 ≈ 2.47e-4

比 warmup=4000 的峰值(7e-4)小 2.8 倍。也就是说,warmup 拉长一倍,整个训练前期的 lr 都偏低,模型「学得慢」------这就是图里 warmup=16000 那条曲线偏右下的原因。

新峰值出现在 step = 8000

  • lr_peak = 512^(-0.5) · 8000^(-0.5) = (1/22.627) · 0.01118 ≈ 4.94e-4

比 warmup=4000 的 7e-4 低 30%。

直观结论:warmup 长度越长,峰值学习率越低。这是公式 lr_peak = d^(-0.5) · warmup^(-0.5) 直接给出的。

如果你的训练总步数本来就少(比如只有 10k 步),warmup 取 4000 已经占了 40%------前期太慢,后期没几步收敛。这时该把 warmup 缩到 1000 左右。

13.2 改了 d_model 会怎样

把 d 从 512 改成 1024(big 模型):

step = 4000 处的峰值

  • lr_peak = 1024^(-0.5) · 4000^(-0.5) = (1/32) · (1/63.25) ≈ 4.95e-4

比 d=512 的 7e-4 低约 30%。这印证了 「模型越宽、学习率越低」 的经验,是公式自动给出的。

实际工程里你会看到很多大模型的峰值 lr 在 1-3e-4 量级------它们的 d 通常 4096-12288,按 d^(-0.5) 缩,比 d=512 的 7e-4 低好几倍。


十四、训练循环的伪代码

把整个训练 loop 用伪代码写一遍,所有上面提到的部件放到位:

python 复制代码
import math, torch
from torch.optim import Adam

def lr_schedule(step, d_model, warmup_steps):
    return d_model ** -0.5 * min(step ** -0.5,
                                  step * warmup_steps ** -1.5)

# 模型与优化器
model = Transformer(d_model=512, n_layers=6, n_heads=8, d_ff=2048,
                    vocab_size=37000)
optim = Adam(model.parameters(), lr=1.0,
             betas=(0.9, 0.98), eps=1e-9)
# 注意:lr=1.0 是占位,真实 lr 由 schedule 给

# Label smoothing 损失
loss_fn = LabelSmoothingLoss(eps=0.1, vocab_size=37000, pad_id=0)

step = 0
for batch in dataloader:                  # batch 已经 token-balanced
    step += 1
    # 1. 设定本步学习率
    lr = lr_schedule(step, d_model=512, warmup_steps=4000)
    for g in optim.param_groups:
        g["lr"] = lr

    # 2. 前向
    logits = model(batch.src, batch.tgt[:, :-1])  # teacher forcing
    loss = loss_fn(logits, batch.tgt[:, 1:])

    # 3. 反向 + 更新
    optim.zero_grad()
    loss.backward()
    optim.step()

    # 4. 日志
    if step % 1000 == 0:
        print(f"step {step}  lr {lr:.2e}  loss {loss.item():.3f}")

    if step >= 100_000:
        break

这段代码概念上完整,但工程上还差几样东西:

  • 梯度累积:如果单卡显存不够 25000 token,要把 batch 拆成多个小 batch 累积梯度;
  • 多卡 all-reduceloss.backward() 之后要做 DDP 的梯度同步(PyTorch DistributedDataParallel 自动处理);
  • 混合精度:FP16/BF16 训练要加 GradScaler;2017 年原论文是 FP32 的,bf16 是后来的优化;
  • gradient clipping :很多复现里加了 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0),防止偶发的大梯度把训练弄崩。原论文没明确说,但 fairseq 默认开启;
  • checkpoint 保存:每 5000 步存一次模型;
  • 验证:每 N 步在 dev set 上算一次 loss/BLEU。

加上这些工程细节,单文件训练脚本大概 300-500 行,可读性还可以------这正是 fairseq 早期版本的水平。


十五、Checkpoint Averaging:最后一步的复现细节

原论文报最终 BLEU 时用了 checkpoint averaging:不是直接拿最后一个 checkpoint,而是把训练末尾若干 checkpoint 的参数做平均,再用平均后的模型解码。这个细节和前面的学习率、dropout 一样,都属于「论文结果为什么能对上」的一部分。

它的作用不是创造新的能力,而是把训练末期参数抖动带来的噪声压平。不同实现里平均几个 checkpoint、间隔多久保存一次会有差异,所以这里更适合把它当成复现细节,而不是脱离上下文的「万能提分术」。


十六、原论文里几个容易忽视的细节

在收尾之前,我想再补几个原论文文本里写了、但读快了容易错过的工程细节。这些细节单看每个不大,但加在一起决定了你能不能复现到 BLEU 27.3 这个具体值。

16.1 Embedding weight tying

原论文 §3.4 提到一句:「we share the same weight matrix between the two embedding layers and the pre-softmax linear transformation」。

意思是:

  • encoder 输入的 token embedding;
  • decoder 输入的 token embedding;
  • decoder 输出层(把隐状态投影回 vocab 维度的 W_out);

这三个矩阵共享同一份参数------共用一个 [vocab_size, d_model] 的矩阵。

为什么这么做?

第一,省参数。词表 37000、d=512,单矩阵 19M 参数;如果三个独立,就是 57M。共享省下 38M,相当于一个完整的 base 模型大小。

第二,正则化效果。三个矩阵被强制保持一致,相当于「输入 → 表示」和「表示 → 输出」用同一个映射的转置------这种对称性约束让训练更稳。

第三,在小词表上几乎不损失性能;在大词表(GPT-3 的 50k)上仍然有效。

后来 Press & Wolf(2017)写了一篇专门的文章 Using the Output Embedding to Improve Language Models 系统验证了 weight tying 的好处。

实现上:

python 复制代码
# 共享一个 nn.Embedding
shared_embed = nn.Embedding(vocab_size, d_model)
# 输出投影把 embedding 矩阵转置
output_logits = hidden @ shared_embed.weight.T

16.2 没有 bias 的 attention 投影

很多后来的实现会把 W_Q、W_K、W_V、W_O 的 bias 关掉,但这不是原论文正文里明确规定的训练配方。复现时它可以作为需要核对的实现差异之一,但优先级应低于 LN 位置、embedding 缩放、dropout 和 averaging 这些更直接影响论文结果对齐的细节。

16.3 Dropout 在 attention 内部的位置

前面说过 dropout 加在「子层输出」。但实际 fairseq 实现还在 attention 的 softmax 之后加了一个 dropout:

python 复制代码
attn_weights = softmax(QK^T / sqrt(d_k))
attn_weights = dropout(attn_weights)   # 这一步原论文没明说
output = attn_weights @ V

这个细节很微妙------它让某些 token 在某些 head 上的「看到」被随机屏蔽,强迫不同 head 不依赖单一来源。后来很多实现沿用,HuggingFace 默认开启。

16.4 训练用 FP32,推理可以 FP16

2017 年 P100 已经支持 FP16,但混合精度训练还不成熟(Apex 是 2018 年的事)。原论文用的是纯 FP32。

但他们提到:「We used 8 NVIDIA P100 GPUs.」P100 的 FP16 算力是 FP32 的 2 倍------意味着训练时其实没用上 FP16 的加速。

到了 2020 年之后,BF16 训练成为大模型主流(更少精度问题),FP16 + loss scaling 也很流行。这块工程演化非常快。


十七、评测口径:BLEU 数字为什么会有偏差

对这篇论文来说,评测里最重要的不是再讲一遍 BLEU 公式,而是记住:不同脚本和分词口径算出来的 BLEU 不能直接横比。原论文 2017 年用的是 multi-bleu.perl 一类的脚本,今天很多人习惯用 SacreBLEU;同一个模型,数字差零点几到 1 分并不稀奇。

所以当你说「我复现到 27.3 了吗」时,至少要先对齐三件事:测试集是不是同一个,分词和大小写口径是不是同一个,checkpoint averaging 和解码超参数是不是同一个。否则你比较的可能不是模型,而是评测脚本。


十八、常见误解

误解一:warmup 可有可无,去掉只是慢一点。

错。对 Transformer 这类模型来说,warmup 不是装饰项。把它缩得过短,甚至直接去掉,训练稳定性通常会明显变差,因为 Adam 的二阶统计在起步阶段还没站稳。

误解二:Transformer 用 Adam 默认参数就行。

错。原论文用的是 β₂ = 0.98、ε = 1e-9,而不是很多框架里常见的默认值。是不是一定会掉很多分,取决于实现和数据,但如果你想复现论文结果,先把这些超参数对齐是更稳的做法。

误解三:label smoothing 让 PPL 变差,所以是个错误的设计。

不对。label smoothing 让训练 PPL 变差是预期内的(因为最优分布被人为软化),但 BLEU 变好------这才是机器翻译关心的指标。PPL 和 BLEU 不总是同向。

误解四:beam search size 越大越好。

错。beam=4 是原论文的选择。beam 再继续加大,并不会自动带来更好的翻译,很多时候只是把搜索算力花在非常相近的候选上;是否继续受益,还要看任务、length penalty 和评测口径。

误解五:现代 LLM 训练完全用了不一样的配方。

部分对。变了很多细节(dropout=0、weight decay=0.1、cosine 衰减等),但「形状」没变------仍然是 Adam 系、warmup、token-based batching、按学习率峰值的某个分数衰减。Transformer 训练的「骨架」是 2017 年定下来的。


十九、下一步

下一篇 28|原论文实验结果 会从「训练配方」切到「实验结果」------具体数字、消融实验、注意力可视化。我们会讲:

  • 主结果 BLEU 27.3 / 28.4 / 38.1 / 41.8 是怎么测的;
  • 消融实验告诉我们 head 数、d_k、d_model、dropout 各个超参数的最优点在哪儿;
  • 附录里那些注意力可视化看到了什么;
  • 这篇论文的「短板」------哪些消融它没做、后人是怎么补上的;
  • 跨任务泛化的伏笔:从翻译到 BERT/GPT 的飞跃。

再往后:


二十、参考文献

  1. Vaswani, A. et al. "Attention Is All You Need." NeurIPS 2017. 训练配方的源头。
  2. Kingma, D. P., Ba, J. "Adam: A Method for Stochastic Optimization." ICLR 2015. Adam 优化器原始论文。
  3. Loshchilov, I., Hutter, F. "Decoupled Weight Decay Regularization (AdamW)." ICLR 2019. 现代大模型用的优化器。
  4. Szegedy, C. et al. "Rethinking the Inception Architecture for Computer Vision." CVPR 2016. Label smoothing 提出。
  5. Srivastava, N. et al. "Dropout: A Simple Way to Prevent Neural Networks from Overfitting." JMLR 2014. Dropout 原始论文。
  6. Wu, Y. et al. "Google's Neural Machine Translation System (GNMT)." arXiv:1609.08144, 2016. length penalty 公式来源。
  7. Gehring, J. et al. "Convolutional Sequence to Sequence Learning (ConvS2S)." ICML 2017. Transformer 之前的 SOTA。
  8. Kalchbrenner, N. et al. "Neural Machine Translation in Linear Time (ByteNet)." arXiv:1610.10099, 2016.
  9. Ott, M. et al. "fairseq: A Fast, Extensible Toolkit for Sequence Modeling." NAACL 2019 demo. 最接近原论文的复现实现。
  10. Vaswani, A. et al. "Tensor2Tensor for Neural Machine Translation." AMTA 2018. Google 自家的复现。
  11. Xiong, R. et al. "On Layer Normalization in the Transformer Architecture." ICML 2020. Pre-LN vs Post-LN 系统分析。
  12. Liu, L. et al. "On the Variance of the Adaptive Learning Rate and Beyond (RAdam)." ICLR 2020. 解释 warmup 为什么重要。
  13. Goyal, P. et al. "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour." arXiv:1706.02677, 2017. linear warmup 的理论支持。
  14. Brown, T. et al. "Language Models are Few-Shot Learners (GPT-3)." NeurIPS 2020. 现代 LLM 训练配方对照。
  15. Touvron, H. et al. "LLaMA: Open and Efficient Foundation Language Models." arXiv:2302.13971, 2023.
  16. Touvron, H. et al. "Llama 2: Open Foundation and Fine-Tuned Chat Models." arXiv:2307.09288, 2023.
  17. Hoffmann, J. et al. "Training Compute-Optimal Large Language Models (Chinchilla)." NeurIPS 2022. 训练成本和数据规模的 scaling law。
  18. Kaplan, J. et al. "Scaling Laws for Neural Language Models." arXiv:2001.08361, 2020. OpenAI 的 scaling law。
  19. Sennrich, R. et al. "Neural Machine Translation of Rare Words with Subword Units (BPE)." ACL 2016. 原论文用的子词切分方法。
  20. Press, O., Wolf, L. "Using the Output Embedding to Improve Language Models." EACL 2017. weight tying 的来源。
  21. Bahdanau, D., Cho, K., Bengio, Y. "Neural Machine Translation by Jointly Learning to Align and Translate." ICLR 2015. WMT MT 评测的早期 SOTA 之一。
  22. Papineni, K. et al. "BLEU: a Method for Automatic Evaluation of Machine Translation." ACL 2002. BLEU 的原始定义。
  23. Post, M. "A Call for Clarity in Reporting BLEU Scores (SacreBLEU)." WMT 2018. 现代 BLEU 标准化工具。

← 上一篇:26|前馈网络 | 下一篇:28|原论文实验结果

相关推荐
why技术9 小时前
AI Coding开始进入第四个时代,我还没上车呢!
前端·人工智能·后端
程序猿追11 小时前
我搭了个网页工具:输入关键词,SERP API 自动吐出比价 Excel
后端
Lee川11 小时前
RAG 实战:从一篇掘金文章出发,拆解检索增强生成的全链路
前端·人工智能·后端
Lee川11 小时前
MCP 高德地图实战:当 AI 学会使用工具,一个协议如何重塑大模型的行动边界
前端·人工智能·后端
楼田莉子11 小时前
C++17新特性:__had_include/属性/求值顺序规则
开发语言·c++·后端
程序员cxuan11 小时前
Codex 把我家烂网给优化后,我 TM 直接原地起飞了。
人工智能·后端·程序员
IT_陈寒11 小时前
Redis批量删除踩了坑,原来DEL命令不是万能的
前端·人工智能·后端
叫我少年12 小时前
C# 命名空间与 using 指令 — 文件范围、全局导入、别名
后端
我是一颗柠檬13 小时前
【MySQL全面教学】MySQL基础SQL语句Day3(2026年)
数据库·后端·sql·mysql·oracle