八股文
闲聊¶
目前主流的开源模型体系¶
主流开源模型体系包括: Qwen系, LLaMA系, GLM系, Kimi系, Minimax系, Deepseek系, Baichuan系, Mistral系, InternLM系等.
- Qwen系: 公开了dense和MoE两条线, 尺寸覆盖0.6B, 1.7B, 4B, 8B, 14B, 32B, 以及30B-A3B, 235B-A22B(注意A后面的是MoE中实际参数激活量). 主要是32K/128K上下文. 覆盖从端侧到高性能服务器端的大多数区间. 官方强调多语言, 推理, agent能力, 采用Apache 2.0, 比较适合商业用途
- LLaMA系: 8B, 70B, 405B, 128K上下文, 支持多种语言, 工具使用能力好, 生态好, 几乎所有推理框架, 微调框架, 量化方案都优先适配. 但是他的社区许可有点问题, 不是Apache/MIT的宽松许可.
- DeepSeek系: 最具有代表性的是V3和R1路线, V3官方公开为671B-A37B, 128K上下文, R1侧重强化推理, 并开源了若干基于LLaMA/Qwen的蒸馏模型. 最大优势是性价比很高, 本地部署和服务器部署采用率都很高, 函数调用官方也有支持. R1是MIT, 但是V3有责任使用限制.
Prefix LM和Causal LM的区别是啥?¶
- Casual LM: 严格的自回归, token只能看左侧的历史
- Prefix LM: 将输入分为前缀区和生成区, 前缀区是双向可见, 生成区按照自回归方式生成. Prefix LM的目的是让那些decoder only的模型能更好的处理条件生成(其实有一点encoder-decoder的那个味)
为何现在的模型大部分都是decoder-only架构?¶
- 训练目标: next-token prediction很适合弱监督训练
- 生成任务: 对话, 写作, 代码补全都是自回归生成
- 扩展性强: 参数和数据放大的时候性能提升规律清晰
- 生态成熟: KV Cache, 并行推理框架都围绕decoder-only进行优化
如何让大模型处理更长的文本?¶
- 分块检索: 检索相关的片段发给大模型
- 长上下文模型: 选支持更大上下文长度的模型
- 滑动窗口: 分段重叠处理后聚合答案
- 层次摘要: 段落总结->全局总结
- 位置编码外推技术: 模型训练的时候会设定一个最大上下文长度, 如2048 tokens, 如果输入4096, 模型通常会出现注意力退化, 推理错误, 性能明显下降, 这是因为模型没有见过更大的位置编号. 经典的Transformer模型使用的绝对位置编码无法外推, 后来的很多模型(如LLaMA, Qwen)使用RoPE(Rotary Positinoal Embedding), 他可以外推扩展上下文.
LLM复读机问题¶
复读机是模型重复输出相同短语/句子的现象, 常见的原因:
- 温度太低, top-p过窄
- repetition_penalty未设置
- prompt指令不正确
- 长上下文导致局部高概率循环
常见解决方案:
- 提高temperature, top-p
- 设置repetition_penalty
- 缩短上下文或者分段生成
- 在提示词明确禁止重复
激活函数¶
常见的激活函数有Sigmoid, Tanh, ReLU, Leaky ReLU.
- Sigmoid函数: \(f(x) = \frac{1}{1+e^{-x}}\). 它能够把输入的连续值变换为0和1之间的输出. 特别的, 如果是非常大的负数, 那么输出是0; 如果是非常大的正数, 输出为1. 它有三个缺点:
- 容易梯度消失: 在反向传播的时候容易梯度消失, 梯度爆炸发生的概率较小;
- 不是0均值: 它的输出不是0均值. 这会导致锯齿状更新(Zigzag), 在反向传播中, 损失函数对于权重的梯度可以被表示为\(\frac{\partial L}{\partial w} = \frac{\partial L}{\partial f}\cdot \frac{\partial f}{\partial w}\). 假设上一层经过了sigmoid函数之后输出全是整数, 那么\(\frac{\partial f}{\partial w}\)恒大于0, 那么梯度的符号完全取决于\(\frac{\partial L}{\partial f}\)的符号. 对于这个神经元的所有权重来说, 这个标量是公用的, 所以这个神经元权重的所有分量在同一次更新中, 要么全部增加, 要么全部减小, 他们无法发生"部分权重增加, 部分权重减小"的复杂调整. 如果最优解的方向实际上需要\(w_1\)增加而\(w_2\)减小, 模型无法直接沿着该方向更新, 它必须走Z字形的路径来迂回逼近最优解, 极大降低了收敛效率;
- 求解较难: 解析式中有幂运算, 计算机求解相对来说比较费时, 对于规模比较大的深度网络, 这会较大增加训练时间
- Tanh函数: \(f(x) = \frac{e^x-e^{-x}}{e^x+e^{-x}}\). 取值范围是\((-1, 1)\). 均值为\(0\). 因为输出有正有负, 输入到下一层的数据也是有正有负的, 这意味着在反向传播的时候, 梯度的更新方向更加灵活, 不再被迫同增同减, 收敛速度比Sigmoid快. 虽然比Sigmoid好, 但是Tanh依旧是饱和函数(Saturated Function). 当输入\(x\)很大或者很小的时候, 函数曲线趋近于平坦, 倒数趋近于0. 导致深层参数无法更新. 依然涉及幂运算, 计算效率低.
-
ReLU函数: \(f(x) = \max(0, x)\). 这是目前深度学习中最常用的激活函数. 核心有点包括:
- 解决梯度消失: 在\(x>0\)的区间, ReLU的导数恒为1, 这意味着无论网络多深, 梯度都能无损地传回到前面的层, 彻底解决了Sigmoid和Tanh在深层网络中梯度都趋近于0的问题.
- 计算高效: 不需要进行昂贵的指数运算.
- 稀疏性: 当输入\(x<0\)的时候, 输出为\(0\). 这使得一部分神经元处于沉默状态, 模拟了生物神经元的稀疏激活特性. 稀疏性有助于减少参数间的耦合. 让模型更容易挖掘相关特征, 并具有一定的抗噪能力.
当然, 它也不是完美的:
- Dead ReLU问题: 当输入\(x<0\)时, 梯度为0. 如果某个神经元在训练过程中不幸陷入了这个区域(例如学习率过大导致权重剧烈变化), 它将永远无法被激活, 梯度永远是0, 权重也永远无法更新. 这个神经元就像"死掉"了一样.
- 不是0均值: 和 Sigmoid 一样, ReLU 的输出要么是 0, 要么是正数, 均值不是0. 这依然会导致权重更新时的"Zigzag"问题, 但由于其梯度不饱和的巨大优势, 这个问题通常可以接受 .
-
Leaky ReLU函数: \(f(x) = \max(\alpha x, x)\). 其中, \(\alpha\)是一个很小的常数, 通常取\(0.01\), 称为负斜率. 主要优势就是解决Dead ReLU. 即使输入是负数, 导数也不是0, 而是\(\alpha\). 这意味着神经元即使暂时休眠, 也能通过这个微弱的梯度慢慢调整权重, 有机会重新激活. 保留了ReLU的优点, 计算简单, 速度快, 在正区间没有梯度消失问题. 缺点是需要超参数调优, \(\alpha\)的值需要人工设置, 虽然\(0.01\)是常用的默认设置, 但是不同任务的最优值可能不同, 需要额外调试; 在某些任务(如图像超分辨率)中, 负区间的微小梯度可能干扰高频细节的学习. 有研究指出Leaky ReLU的表现并不是总优于ReLU, 在某些数据集上可能略差.
归一化¶
BN和LN¶
BN和LN本质的区别在于计算均值和方差的维度不同.
- Batch Normalization: 纵向归一化. 对于一个Batch内的数据, 在同一个特征维度上进行归一化. 如果你有N张照片, BN是把这N张照片的红色通道全部拿出来算一个均值和方差, 然后归一化. 严重依赖Batch Size的大小, Batch太小会导致统计量不稳定, 效果变差.
- Layer Normalization, 横向归一化. 对单个样本的所有特征维度进行归一化. 对于第1张照片, 把它的红绿蓝等所有通道的数据拿出来算一个均值和方差, 自己对自己归一化. 无论Batch size是1还是100, 每个样本的计算方式完全一样, 不受干扰.
另外一个深挖的点在于训练和测试时的区别.
- Batch Normalization: 训练和测试时候的行为是不一致的. 训练的时候, 使用的是当前mini-batch的均值和方差进行归一化. 同时, 模型会维护一个全局运行均值和方差, 通过指数加权平均来更新. 测试的时候, 不能使用测试数据的统计量(因为测试数据可能只有一个), 而是直接使用训练期间存下来的全局运行均值和方差. 这保证了对单张图片预测结果的确定性.
- Layer Normalization: 训练和测试行为一致. 无论是训练还是测试, LN始终是计算当前输入样本自身的均值和方差进行归一化. 不需要维护全局统计量, 因为单条数据内部的信息就足够完成计算.
Loss¶
多标签¶
什么是多标签Loss: 区别于传统分类(一个样本对应一个标签), 多标签分类允许一个样本拥有多个标签. 比如一张图片里面可能既有猫, 又有草地和阳光. 编码方式: 当标签数量不确定, 但是总数确定的时候, 需要使用多热编码(Multi-hot Encoding). 例如, 包含编号0, 3, 9的图片, 其编码为10010000010. 损失函数和激活函数: 多标签分类的核心思路是将任务拆解为多个独立的二分类问题. 对于二分类问题(是/否), 和多标签分类(图中既有猫和狗), 我们采用的是sigmoid而不是softmax作为激活函数. 损失函数为二分类交叉熵损失(Binary Cross-entropy, BCE), \(L = -\sum_{k} y_k \log(\sigma(l_k)) + (1-y_k) \log(1-\sigma(l_k))\), \(y_k\)为标签真值, \(\sigma(l_k)\)是第\(k\)个标签的预测概率.
指标¶
多标签¶
多标签分类中有四种常见的评价标准:
- 子集准确率: 只有当模型预测的所有标签集合和真是标签集合完全一致的时候, 才算预测准确. 它忽略了部分正确的预测, 评价标准较高, 公式为\(\frac{1}{p}\sum_{i=1}^{p} 1\{h(x^i) = y^i\}\), 其中\(p\)为样本数, \(1{\cdot}\)为指示函数.
- 汉明损失: 统计所有样本中, 预测错误的标签数(包括漏报和误报)占总标签容量的比率. 数值越小表示模型性能越好. 公式为\(\frac{1}{p}\sum_{i=1}^{p}\frac{1}{q}|h(x^i)\Delta y^i|\), \(\Delta\)表示对称差, 即只在一个集合中出现的元素.
- 一错率: 考察模型预测概率最大的那个标签是否在真实的标签集合中, 如果概率最大的预测标签不在真实集合中, 则记为一次错误, 数值越小越好.
- 覆盖度: 计算平均需要多少步, 才能覆盖掉样本所有的真实标签.
数据集¶
数据类别不平衡¶
类别不平衡指的是分类任务中不同类别的训练样本数量差别比较大的情况. 例如, 正样本的样本数量极少, 但是反例样本的数量极多. 有以下的解决方法:
- 阈值移动(Threshold-Moving): 设\(y\)为模型预测样本为正例的概率, \(1-y\)为模型预测样本为反例的概率. 我们在预测阶段调整决策临界点, 传统的二分类以\(0.5\)为阈值, 即几率\(\frac{y}{1-y}>1\)的时候预测为正例. 当训练集中正负样本数量不同的时候(\(m^+\)为正例数量, \(m^-\)为反例数量), 若满足\(\frac{y}{1-y} > \frac{m^+}{m^-}\), 则预测为正例. 这实际上是将分类器的决策边界向样本较少的类别倾斜.
- 欠采样(Undersampling): 也称为下采样(Downsampling). 去除一些多数类的样本, 使得不同类别的样本数量接近, 然后再进行学习.
- 过采样(Oversampling): 也成为上采样(Upsampling). 增加一些少数类的样本(如通过复制或者合成新的样本), 使得类别分布趋于平衡.
- 焦点损失(Focal Loss): 这是一项专门针对目标检测中正负样本严重不平衡而设计的改进损失函数. 在标准交叉熵损失的基础上, 通过减少易分类样本的权重, 使得模型在训练的时候更加专注于稀疏的, 难分类的样本. 防止大量容易区分的负样本在损失计算中占据主导地位, 从而提升模型对少数类或者困难样本的识别能力.
训练¶
训练/微调一个LLM的流程¶
- 确定任务目标. 先确定为啥要训练或者微调这个模型, 比如是做问答, 文本分类, 代码生成, 客服对话还是行业专用助手. 想清楚输入是啥, 输出是啥, 希望模型达到什么效果, 用什么指标评估.
- 选择基础模型. 通常不会从零开始训练一个LLM, 因为成本很高, 更常见的是在已有模型基础上机型微调, 比如选择一个开源模型作为底座. 选择模型的时候会考虑参数规模是否合适, 是否支持中文或者多语言, 推理成本高不高, 是否允许商用, 是否适合你的任务类型.
- 数据收集和清洗: 这是很关键的一步, 模型效果往往很大程度上取决于数据质量, 常见的操作包括: 收集领域数据, 去重, 去噪, 纠错, 格式统一, 去除低质量样本, 处理敏感或者违规内容.
-
数据标注和构造数据集: 根据任务的不同, 主备不同形式的数据:
- 预训练: 大规模原始文本
- 监督微调(SFT): 指令-回答对
- 偏好对齐: 优质回答 vs. 较差回答
- 分类任务: 文本+标签
然后一半会把数据集分为: 训练集, 验证集, 测试集.
-
数据预处理: 把文本变为模型可以读入的模式. 主要包括分词, 截断或者填充长度, 转换为token id, 构建attention mask, 按照训练模板拼接prompt.
-
开始训练或者微调: 这一步是让模型开始学, 常见的方式有三类:
- 第一类: 预训练. 让模型在海量文本上学习语言规律. 成本最大, 需要大量算力和数据
- 第二类: 监督微调. 用人工整理好的回答, 指令数据, 让模型更加符合具体任务. 这是常见的微调方式
- 第三类: 对齐训练. 例如用人类反馈优化模型输出, 让回答更加安全, 更加符合偏好, 这一步通常用于提升回答质量, 风格和可控性.
训练中需要设置很多参数, 比如学习率, batch size, epoch, 最大长度, 优化器, warmup, 梯度累计.
如果资源有限, 还会用:
- LoRA/QLoRA
- 混合精度训练
- 分布式训练
- 梯度检查点
-
监控训练过程: 训练的时候不能只让它一直跑, 要持续观察结果. 通常会关注 training loss, validation loss, 是否过拟合, 是否梯度爆炸, 显存占用, 训练速度. 如果出现训练集效果越来越好, 但是验证集效果变差, 说明过拟合了.
- 模型评估: 训练完成之后, 要判断模型是否变好, 评估方法通常有准确率, BLEU, 等等, 还可以人工评估.
了解强化学习吗? 和SFT什么区别?¶
强化学习中, 模型不是直接背标准答案, 而是通过试错+奖励来学, 做得好就是奖励高, 做的差就是奖励低, 目标是让总奖励最大. SFT给模型提供"输入-标准输出"对, 让他直接学习如何回答.
- 学习方式不同: SFT学标准答案, RL靠奖励信号不断调整
- 反馈不同: SFT 看和正确答案差多少, RL看最终得分高不高
- 目标不同: SFT 学会"像样地回答", RL让回答"更加符合偏好/效果更好"
过拟合和欠拟合¶
- 过拟合: 训练误差很小但是测试误差很大, 表现为模型在训练集上表现优异, 但是泛化性能很差, 原因是模型复杂度高于实际问题, 死记硬背了数据中的噪声. 我们需要获取更多的数据, 降低模型复杂度, 使用L1/L2正则化, 使用Dropout, 使用Early Stopping.
- 欠拟合: 模型无法在训练集上获得足够低的误差, 表现为模型复杂度过低, 没能学习到数据背后的规律. 欠拟合通常发生在训练的初期, 会随着训练进行逐渐消失, 如果训练后期仍然存在欠拟合, 则必须调整模型结构. 我们需要增加模型的复杂度, 或者在模型中增加更多的特征.
现在有个模型欠拟合, 增加模型深度可以改善?¶
可以改善. 欠拟合意味着模型的复杂度低于数据的复杂度. 即模型学不到数据中的复杂规律. 增加深度可以提高模型的表达能力, 更多的层数允许模型提取更加高阶, 更加复杂的非线性特征, 从而更好地拟合训练数据.
可以先用少量的数据, 如一个batch尝试让模型过拟合. 如果模型连这几个样本都学不会, loss不下降, 说明网络结构或者参数初始化有问题. 如果没能学会小样本但是处理不了全量数据, 说明拟合能力上限不足, 此时增加深度或者宽度是有效的改进手段.
梯度爆炸和梯度消失的解决方案¶
- 梯度消失: 原因是在反向传播中, 梯度逐层递减, 解决方案包括:
- 激活函数: 使用ReLU, Leaky ReLU, ELU代替sigmoid/tanh
- 残差连接: ResNet中的shortcut connection直接传递梯度
- 批量归一化: 稳定各层输入分布
- 门控机制: LSTM, GRU通过门控控制信息流动
- 权重初始化: He初始化, Xavier初始化
- 梯度爆炸: 原因是在反向传播中, 梯度逐层递增, 解决方案包括:
- 梯度裁剪(Gradient Clipping): 设定阈值, 超出则缩放
- 权重正则化: L2正则化可以限制权重的大小, 从而间接控制梯度的大小.
- 学习率调整: 减小学习率可以减缓参数更新的步伐, 从而减少梯度爆炸的风险.
- 正确的初始化: 避免初始权重过大
优化器¶
Adam优化器¶
Adam Optimizer是一种非常常用的深度学习优化算法. 用来更新神经网络参数, 让模型更快, 更加稳定地收敛. 它本质上结合了两种思想: 动量和自适应学习率. 可以理解为即参考过去的提取趋势, 又根据每个参数的历史情况自动调整步长.
如果你学过普通的SGD, 它的更新规则很简单: \(\theta = \theta - \eta\cdot g\), 问题是, 学习率太大->震荡; 太小->收敛慢; 不同参数需要不同步长. Adam就是为了解决这些痛点.
Adam会维护两个参数:
- 一阶矩(Momentum): 类似于惯性, 本质上是梯度的指数加权平均, 如果梯度一直朝着一个方向, 更新会越来越坚定, 而不是来回摇摆. 公式: \(m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t\)$. \(m_t\)是当前时刻的动量, \(g_t\)是当前时刻的梯度, \(\beta_1\)是动量衰减率, 通常取0.9. 如果过去一直往左下降, 现在可能继续往左, 越来越坚定.
- 二阶矩(Adaptive Learning Rate): 记录每个参数的历史梯度的平方的指数加权平均, 用来调整每个参数的学习率. 公式: \(v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2\). \(v_t\)是当前时刻的二阶矩, \(\beta_2\)是二阶矩衰减率, 通常取0.999. 如果某个参数的梯度一直很大, 那么它的\(v_t\)就会很大, 学习率就会被自动缩小; 如果某个参数的梯度一直很小, 那么它的\(v_t\)就会很小, 学习率就会被自动放大.
Adam的这两个参数一开始是\(m_0=0, v_0=0\), 根据动量公式, \(m_1=\beta_1\cdot 0+(1-\beta_1)g_1=0.1g_1\), 但是真实梯度是\(g_1\), 直接缩水了10倍, 所以如何进行修正呢, Adam引入了偏置修正, \(\hat{m}_t = \frac{m_t}{1-\beta_1^t}\), \(\hat{v}_t = \frac{v_t}{1-\beta_2^t}\). 这样, 在初始阶段, \(\hat{m}_1 = \frac{0.1g_1}{1-0.9}=g_1\), \(\hat{v}_1=\frac{0.001g_1^2}{1-0.999}=g_1^2\). 从而保证了在训练初期, 学习率不会因为动量和二阶矩的初始化而被人为缩小. \(t\)表示当前的训练步数, 随着训练的进行, \(\beta_1^t\)和\(\beta_2^t\)会逐渐趋近于0, 修正项的影响会逐渐减小, 最终趋近于1, 不再对动量和二阶矩进行修正.
综上所述, Adam优化器的更新规则为: \(\theta = \theta - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}\), 其中\(\epsilon\)是一个很小的常数, 用来防止除以0. Adam通过结合动量和自适应学习率, 能够在训练过程中更快地收敛, 并且在面对稀疏梯度或者非平稳目标时表现更好.
在梯度累积中, 我们在每个mini-batch上计算梯度, 但是不立即更新参数, 而是将梯度累积起来, 等到累积了足够的梯度(例如4个mini-batch)之后, 再进行一次参数更新. 这相当于模拟了一个更大的batch size. 在使用Adam优化器时, 累积的梯度会被用来计算动量和二阶矩, 从而保证了优化器的正常工作. 使用梯度累积的好处是可以在显存有限的情况下, 训练更大的模型或者使用更大的batch size, 从而提高模型的性能和稳定性. 消除梯度噪声.
分布式训练¶
数据并行¶
将同一个模型复制到多张 GPU 上, 让每张卡处理不同的数据并独立计算梯度; 随后通过 AllReduce 将梯度求平均, 使所有设备的每个参数获得一致的全局梯度, 并在本地同步更新参数. 为啥不是在其中的一张GPU上更新参数再复制到别的GPU呢? 是因为optimizer.step()的开销远远小于复制参数的通信开销. 优点是最容易实现, 扩展性好, 缺点是模型必须放进单卡内存.
模型并行¶
把模型切开, 不同的GPU负责不同的层. 比如一个4层网络:
训练流程:
优点是可以训练更大的模型, 缺点是GPU经常在等数据, 效率不高.
流水线并行¶
这是为了解决上述的"等待问题". 在GPU1处理GPU0的第一批micro-batch的中间结果的时候, GPU0已经开始处理第二批micro-batch了. 这样就实现了GPU之间的并行, 大大提高了效率. 优点是GPU利用率大幅度提升, 缺点是实现困难, 调度困难.
张量并行¶
这是对于模型并行的升级, 模型并行切的是层, 但是张量并行切的是层内的张量. 例如, 对于一个全连接层, 输入是\(X\), 权重是\(W\), 输出是\(Y=XW\). 张量并行会将权重矩阵\(W\)切分成两部分\(W_1\)和\(W_2\), 分别放在GPU0和GPU1上. GPU0计算\(Y_1=XW_1\), GPU1计算\(Y_2=XW_2\), 最后将结果拼接起来得到完整的输出\(Y=[Y_1, Y_2]\). 优点是可以训练更大的模型.
微调¶
| 维度 | Full Fine-tuning | LoRA | Adapter | P-Tuning |
|---|---|---|---|---|
| 参数量 | 全部参数 | 1-3% | 2-5% | 0.1-1% |
| 显存占用 | 极高 | 低 | 中 | 极低 |
| 训练速度 | 慢 | 快 | 中 | 快 |
| 推理开销 | 无 | 额外推理 | 额外推理 | 无 |
| 效果 | 最好 | 接近FT | 接近FT | 略低于FT |
- Full Fine-tuning: 更新全部参数, 效果最好但是成本高; 需要多卡和大显存, 适合数据量大, 任务差异大的场景
- LoRA: 在注意力矩阵添加低秩分解矩阵, 训练的时候冻结原始权重, 合并后没有推理开销, 工业界最常用
- Adapter: 在每层插入小型网络模块, 参数量比LoRA稍大, 适合多任务切换
- P-Tuning/Prefix-tuning: 只训练prompt嵌入或者前缀, 参数量最少, 适合少样本
-
Full Fine-tuning和Parameter-efficient Fine-tuning(PEFT)的区别
- Full Fine-Tuning: 更新模型的所有参数, 训练成本高, 显存要求大, 效果通常最好
- PEFT: 只训练少量参数, 原模型权重冻结, 显存需求小, 更适合工业场景, 包括LoRA, Adapter, P-Funing, Prefix Tuning.
-
为什么现在工业界常用LoRA
因为它参数量小, 通常只占总参数量的1%-3%, 显存占用低, 只训练新增矩阵, 在大多数任务中性能接近FT, 不同任务的LoRA可以叠加加载并且可以高效的替换.
-
LoRA的核心思想是啥
不直接修改原始模型的权重, 而是通过训练一个低秩矩阵的增量来近似权重更新. 在Transformer结构中, LoRA通常插入到注意力层的线性投影矩阵里面, 也就是Query Projection, Key Projection和Value projection, 对这些线性层增加一个低秩旁路.
-
LoRA的Rank参数有什么用
rank参数用来控制低秩矩阵的大小, r越大, 表示表达能力越强, r越小, 参数更少.
-
什么是QLoRA?
QLoRA是Quantization+LoRA, 将模型权重量化为4bit, 冻结量化模型, 在其上训练LoRA.
-
Adapter和LoRA的区别?
Adapter在每层插入小网络, 额外增加推理计算, 参数量比LoRA略多. LoRA直接修改权重矩阵, 不增加推理延迟因为可以合并, 参数更少. 因此LoRA更加流行.
-
微调的时候如何防止过拟合?
Dropout, 简化模型, Early stopping, Data augmentation...
-
LoRA矩阵是否可以初始化为 0?
不可以的, 因为\(A=B=0\), \(\Delta W=BA=0\), 梯度\(\frac{\partial \Delta W}{\partial A}=B, \frac{\partial \Delta W}{\partial B} = A\), 可以看到梯度全是0, 那么学不动了. 常用的方式是一个随机, 一个置零.
推理¶
加速技术¶
剪枝¶
模型剪枝的核心思想是移除模型中不重要的参数, 减少计算量和内存. 分类包括:
- 非结构化剪枝: 随机将单个权重设置为0, 优点是精度损失小, 缺点是需要稀疏计算库支持, 硬件加速困难
- 结构化剪枝: 直接剪掉整个神经元, 通道或者注意力头, 优点是硬件友好, 缺点是容易掉点
量化¶
将模型权重和激活值从高精度浮点数转换为低精度整数. 这会显著减少显存占用, 减少内存带宽压力. 并利用硬件的低精度计算单元提升计算速度. 代价: 可能会带来轻微的模型精度损失, 但是通过PTQ(训练后量化)或者QAT(量化感知训练)等技术, 可以将精度损失降到最低.
-
QAT和PTQ的区别
- PTQ: 模型完全训练好之后进行量化. 它不需要重新训练模型, 而是通过输入少量的数据 (校准集, Calibration Dataset), 观察模型内部激活值的分布范围, 从而计算出最优的量化参数 (缩放因子和零点). 速度快, 成本低, 几分钟到几个小时就可以完成, 不需要昂贵的GPU资源.
- QAT: 在模型训练或微调的过程中, 提前模拟量化带来的误差. 它在计算图中插入伪量化节点 (Fake Quantization), 让模型在训练时就意识到自己未来会被量化, 从而通过反向传播不断调整权重, 主动适应并抵消量化带来的误差. 精度高, 能够最大程度保留模型能力, 需要完整的训练流程, 消耗大量算力和时间, 且需要训练数据.
-
权重量化和激活量化, KV量化有什么区别
- 权重量化: 量化模型的参数, 比较容易, 因为权重是不会随着输入变化的, 是固定的
- 激活量化: 量化中间层的输出, 难度很高, 因为激活值会随着输入变化的, 它能在权重量化的基础上进一步降低缓存和提升推理效率.
- KV量化: 量化键值对的存储, 通常在Transformer模型中用于缓存注意力机制的计算结果.
所以很多LLM方案都是优先做权重量化的.
-
什么是zero-point, 对称量化和非对称量化
zero-point是量化里面的一个偏移量, 作用是让浮点数里面的0, 能准确映射到整数域中的某个值. 量化本质上是将浮点数映射为整数, 常见形式是\(x_{\text{int}}=\text{round}(x_{\text{float}} / \text{scale}) + \text{zero-point}\), 其中, scale是缩放因子, 用来控制量化的精度; zero-point是偏移量, 用来确保浮点数中的0能够准确映射到整数域中的某个值. 例如, 如果我们使用8位无符号整数进行量化, 那么整数域的范围是0到255. 如果我们希望浮点数中的0映射到整数域中的128, 那么zero-point就是128. 这样, 当输入为0时, 输出就是128, 确保了量化后的模型能够正确处理输入为0的情况.
对称量化就是zero-point为0, 非对称量化就是zero-point不为0. 对称量化的好处是计算更简单, 但是可能会导致量化误差较大; 非对称量化可以更好地适应数据分布, 减少量化误差, 但是计算稍微复杂一些.
-
为什么大模型量化里面4bit比8bit更难, 什么是outlier, 量化误差从何而来
bit数量越低, 代表可表示的离散值越少, 量化误差越大, 大模型里面存在outlier, 就是那些特别大的激活值或者权重, 量化的时候如果范围被outlier拉大, 大多数正常值的分辨率就会下降, 结果就是整体量化误差上升. 所以, 4bit虽然更加节省现存, 但是更加容易掉精度.
-
量化的粒度
- per-tensor: 对整个张量使用同一个scale和zero-point, 计算简单, 但是可能会导致较大的量化误差, 因为outlier的存在会拉大量化范围, 使得大多数正常值的分辨率降低. 而整个张量中出现outlier的概率较高, 因此per-tensor量化的误差通常较大.
- per-channel: 对每个通道使用不同的scale和zero-point, 可以更好地适应数据分布, 减少量化误差, 但是计算复杂度增加. 在卷积层中, per-channel量化通常会显著提升模型性能, 因为不同通道的权重分布可能差异较大. 在全连接层中, per-channel量化的优势可能不太明显, 因为权重分布相对均匀.
-
FP32, FP16, INT8, INT4的主要区别是啥?
- FP32: 精度最高, 训练和数值稳定性最好, 但是显存和带宽开销最大
- FP16: 训练和推理都是用, 精度较高, 成本比FP32低一半
- INT8: 常见工业量化的格式, 精度和效率较好
- INT4: 压缩更加激进, 适合LLM推理压缩, 但是误差更大, 对算法设计要求更高
-
如果把一个7B模型从FP16压缩到INT4, 要考虑什么东西
- 权重量化还是权重+激活量化
- 是per-tensor还是per-channel
- 校准集怎么选
- KV Cache是否量化
- 推理kernel是否支持高效的INT4 matmul
- 量化后的权重如何pack: 量化之后, 权重在内存中的存储方式会重新组织, 以便硬件更快计算, 这一步叫做packing.
- 是否保留embedding, norm, lm_head为高精度: attention和linear层的参数占据了90%以上, 但是也有其他的参数, 如embedding的矩阵, 是一个vocab_size * hidden_dim的矩阵, 还有是lm_head, 很多模型lm_head和embedding共享权重, 所以是否量化embedding会直接影响lm_head. 还有一类是norm, norm的参数很少, 通常只有scale.. 但是它对数值稳定很关键, 所以通常不量化. 所以通常采取的策略是: attention/MLP linear量化, Embedding可选量化, LM Head可选量化, Norm不量化.
-
量化为什么可以加速推理
- 模型权重更小, 显存占用更低, 对于memory bandwidth的压力更小
- 低比特证书运算在硬件上可能有专门的加速, 效率更高
-
什么是LLM.int8()
LLM.int8()是一种给大模型做8-bit量化推理的方法, 它的目标是大多数值按8-bit处理, 但把outlier单独拎出来, 用16-bit计算. 官方文档将其描述为大约50% memory reduction, 比无脑INT8更稳; 但是后续的研究和讨论指出, 由于要单独处理outlier, 延迟不一定比纯FP16更好, 某些场景下, 速度可能不及预期.
-
什么是GPTQ/AWQ
- GPTQ: Generative Pretrained Transformer Quantization, 是一种PTQ方法, 不需要重新训练模型. 流程: 取一小部分calibration data, 对某一层进行量化, 计算量化产生的误差, 通过近似Hessian信息, 调整剩余权重补偿误差. 特点: layer-by-layer量化, 常见配置是W4A16
- AWQ: Activation-aware Weight Quantization, 是一种PTQ方法. 流程: 用calibration data跑模型, 收集activation, 找出对输出影响最大的权重通道, 对这些通道进行scale调整或保护, 再进行INT4量化
动态批处理¶
这是一种推理阶段的加速技术, 其核心思想是在短时间内手机多个同时到达的请求, 将他们动态合并为一个batch在GPU上一次性计算, 从而提升硬件效率和系统吞吐量. 系统通常通过设置最大batch size和最大等待时间来控制何时进行批处理.
Flash Attention¶
标准self-attention计算: \(S=QK^T\), 得到的矩阵大小是\(N\times N\), 这个矩阵会被存入HBM, 再进行softmax, 再和\(V\)矩阵相乘. Flash Attention的核心思想是不显式计算和存储整个\(N\times N\)的注意力矩阵, 而是分块计算. SRAM中只需要容纳BB的中间结果, 而不是NN的矩阵, 通过选择合适的block size. 使得B*B能够放入SRAM.

KV Cache¶
在生成式模型的推理过程中, 每次生成一个新token, 都需要重新计算之前所有token的注意力分数. 这会导致计算量随着生成长度的增加而呈指数级增长. KV Cache的核心思想是缓存之前计算过的键值对(KV), 在生成新token时, 只需要计算当前token与之前token的注意力分数, 而不需要重新计算之前token之间的注意力分数. 这样可以显著减少每一步的计算量, 提升推理效率.

Paged Attention¶
传统KV Cache面临的问题: 预分配固定长度的连续内存, 导致显存浪费. 内存碎片化严重, 利用率仅仅为20%-40%. PagedAttention的解决方案是将KV Cache划分为固定大小的块, 每个块成为一个page, 物理上可以不连续存储. 为每个序列维护自己的逻辑地址到物理地址的映射, 程序使用的时候感觉内存是连续的, 实际物理块可以分布在显存的各个地方. 这里序列的意思是用户的多个请求, 每个请求的KV Cache是独立的. 这样就大大提升了内存利用率, 几乎可以达到100%.
如何解决大模型API服务的响应延迟问题?¶
- 模型层面的优化: 可以使用量化模型, 使用小模型, 使用蒸馏模型, 使用加速框架
- 请求层面的优化: 精简prompt, 移除冗余指令, 限制max_tokens, 多个步骤合并为一个请求
- 架构层面的优化: 启用流式响应, 缓存, 负载均衡, 异步处理
- 网络层面饿优化: 使用连接池复用, 启用压缩传输, 启用HTTP/2, 使用CDN加速
解码策略¶
- 温度: 控制输出随机性的超参数. 范围通常在0-2之间, 默认值为1. 当温度接近0时, 模型更倾向于选择概率最高的词, 输出更确定; 当温度增加时, 模型会更多地考虑概率较低的词, 输出更随机. 例如, 温度为0.5时, 模型会放大高概率词的优势, 使输出更集中; 温度为1.5时, 模型会平滑概率分布, 使输出更加多样化.
- Top-k: 只从概率最高的k个词中采样, 其余词概率归零后重新归一化.
- Top-p: 也称为核采样(Nucleus Sampling), 从概率累积达到p的最小词集合中采样. 例如, p=0.9, 就从概率最高的词开始累积, 直到累积概率达到0.9, 这时的词集合就是采样范围.
-
Beam search: 维护多个候选序列(beam), 每次扩展时保留概率最高的k个序列. 适合需要高质量输出的任务, 但可能缺乏多样性.
分词¶
BPE¶
BPE分词(Byte Pair Encoding)是一种子词分词算法, 它介于单词级别和字符级别之间. 它的核心思想是通过迭代合并出现频率最高的相邻字符对, 来构建新的词汇单元. 他是目前大语言模型中最主流的分词技术之一. BPE的运作就像是搭积木, 从最基础的字符开始, 不断地将常见地组合拼接在一起: 首先, 将所有单词拆分为最小地字符, 如l, o, o, k. 然后统计语料库中所有相邻字符对的出现频率. 找到出现频率最高的一对字符如e和s, 把它们合并为一个新符号es. 重复上述过程, 直到词汇表达到预定的大小.
BPE的优点:
- 解决OOV问题: 通过将罕见单词拆分为更小的子词, BPE能够有效地处理未登录词, 减少OOV问题对模型性能的影响.
- 平衡词表大小和语义: 纯字符分词(Char-level)词表小但句子太长且无语义;纯单词分词(Word-level)词表太大且稀疏.BPE 在两者之间取得了平衡,常用词保持完整(如 apple),罕见词拆分为子词(如 app + le).
嵌入¶
Word2Vec¶
Word2Vec算法假设上下文相似的词, 其语义也相似. 目标是将词语从稀疏的高维One-hot向量映射到低维, 稠密的实数特征向量, 从而捕捉词和词之间的语义关系, 如vec(king)-vec(man)+vec(woman)约等于vec(queen).
Word2Vec包含两种主要架构, 都是简单的三层神经网络(输入层, 投影层, 输出层):
- CBOW: 通过上下文来预测中心词, 对小型数据库更加高效, 平滑了分布式信息
- Skip-gram: 通过中心词来预测上下文, 大型语料库中表现较好, 能更好处理生僻词
由于词表通常很大, 直接结算Softmax比较耗时, Word2Vec引入了:
- 层级Softmax, 利用哈夫曼树, 将多分类转为多个二分类, 复杂度从\(O(V)\)降到\(O(\log V)\). 只有1个正样本, 沿着哈夫曼树走, 做\(\log V\)次二分类计算, 结构式树状的, 不需要抽取\(K\)个负样本, 因为在树上面, 每一次选择左边, 就相当于否定了右边的那一大堆词.
- 负采样: 每次只更新一小部分负样本(非上下文词)的权重, 显著降低计算量, 是目前常用的方式. 1个正样本+K个负样本(随机抽取), 进行\(1+K\)次简单的二分类运算, 结构是扁平的, 我就只需要把这\(1+K\)个词的权重调一调, 其他的几万个词语我假装没看见, 不管他们.
注意, 这是两种互斥的方案.
GloVe¶
GloVe(Global Vectors for Word Representation)是斯坦福大学于2014年提出的一种基于全局词频统计的词向量生成模型. GloVe不像Word2Vec那样通过神经网络预测上下文, 而是直接基于语料库构建一个全局共现矩阵\(\bm{X}\). \(X_{ij}\)表示单词\(j\)出现在单词\(i\)上下文窗口的次数. 利用全局统计信息来弥补Word2Vec仅利用局部上下文的不足, 训练速度通常更快.
GloVe的独特之处在于它关注单词之间共现概率的比率. 假设\(i=ice\), \(j=steam\). 对于\(k=solid\), \(P(k|i)\)应该远大于\(P(k|j)\), 比率很大; 对于\(k=gas\), \(P(k|i)\)应该远小于\(P(k|j)\), 比率很小. 对于\(k=water\), 两者都相关, 比率接近\(1\). 训练出的词向量\(w_i\), \(w_j\)应该使得他们的点积\(w_i^Tw_j\)能够你和这个共现概率的对数值.
经典模型¶
CNN结构¶
池化层¶
池化层通常紧跟在特征提取器卷积层之后, 通过下采样缩小特征图的尺寸, 增大感受野, 减少后续层的计算压力, 防止过拟合. 池化层通过一个在特征图上滑动的窗口来执行操作:
- 最大池化: 取窗口内的最大值, 它能提取出区域内的显著特征.
- 平均池化: 取串口内所有值的平均数. 它能保留背景信息使得特征图更加平滑
- 全局池化: 将一整张特征图压缩为一个数值
特征图大小公式¶
\(W_{out}=\lfloor\frac{W_{in}+2P-K}{S}\rfloor+1\)
\(H_{out}=\lfloor\frac{H_{in}+2P-K}{S}\rfloor+1\)
- \(W_{in}/H_{in}\): 输入的宽度和高度.
- \(P\)(Padding): 填充层数.
- \(K\)(Kernel Size): 卷积核(或池化核)的大小.
- \(S\)(Stride): 步长.
- \(\lfloor\dots\rfloor\): 向下取整符号(通常在代码实现中, 如果不能整除则舍弃余数部分).
参数量大小公式¶
- 卷积层: 对于\(m\)个\(k\times k\times n\)的卷积核, 参数量为\((k\times k\times n + 1)\times m\). \(+1\)是因为一个卷积核配置一个偏置.
- 全连接层: 全连接层每个输入节点都与输出节点相连. \(N_{in}\)为输入节点数, \(1\)为偏置项, \(N_{out}\)为输出节点数. 参数量为\((N_{in}+1)\times N_{out}\).
- BN层: \(4C\), \(C\)为通道数量. 每个通道有\(4\)个参数, 缩放, 平移, 均值和方差.
- 激活层: 0参数.
- 池化层: 0参数.
计算量公式¶
- 卷积层: \(FLOPs = H\times W\times k\times k\times n\times m\times 2\). \(H\), \(W\)是特征图尺寸, \(H\times W\)表示卷积核需要滑动多少个位置. \(k\times k\times n\)表示每滑动到一个位置, 需要进行多少次乘法运算. \(m\)是多少个卷积核在同时工作. \(\times 2\)是因为一次乘加运算算\(2\)个\(FLOPs\).
ResNet结构¶
在深度学习中, 随着层数的加深, 准确率会出现达到饱和之后迅速下降的现象, 这不是过拟合, 称为退化问题. ResNet通过引入残差学习来解决这个痛点. 传统的网络试图直接学习目标映射\(H(x)\), ResNet改变了学习目标, 它主要学习一个残差\(F(x)=H(x)-x\), 通过shortcut connection, 将输入\(x\)直接传递到后面, 最终输出\(F(x)+x\). 如果某层是冗余的, 学习\(F(x)=0\)(恒等映射)比学习\(H(x)=x\)要容易得多, 这保证了即使网络很深, 性能也不会变差.
ResNet相比于VGG的改进之处: ResNet延续了VGG的堆叠思路, 引入了残差机制. 解决了退化问题. 不再使用Max Pooling, 改用stride=2的卷积, 这让网络在下采样的时候保留更多的参数(Max Pooling没有参数). 用全局平均池化替换了FC层, 参数量爆减, 能够有效方式过拟合.
ResNet的另外两种设计哲学: 1. 计算平衡, 特征图长宽减半的同时, 通道数加倍, 根据计算量公式, 面积缩小到一半的时候, 正好给通道数增加一倍腾出空间. 确保网络每一层的FLOPs基本恒定; 2. 两种残差块. 浅层ResNet如ResNet-18/ResNet-34使用Basic Block(两层33卷积), 深层ResNet使用Bottleneck结构, 11(压缩)→33(卷积)→11还原. 利用1*1卷积先降维, 在低维空间计算, 降低计算成本.
- Basic Block: 第一层HW332562562; 第二层HW332562562
- Bottleneck Block: 第一层HW11256642; 第二层HW3364642; 第三层HW11642562; 可以看到所需要的FLOPs是远远小于Basic Block的. 注意, 这几层的stride都是1, 对于33卷积, padding=1; 对于11卷积, padding=0, 保证特征图的尺寸不变.
残差连接要求输入和输出的形状必须一摸一样. 目前主流的解决方案是投影映射(Projection Shortcut), 做法是在旁路放一个11的卷积层, 设置这个卷积层的stride=2, 负责将特征图的长宽减半, 设置这个卷积的个数等于主路径的输出通道数(负责增加维度). 缺点是会增加计算量和参数. 另外一个方案是Zero-padding, 第一步是使用一个22的平均池化, 设置stride=2, 将输入的特征图长宽减半, 没有参数; 第二步是直接在当前的特征图后面, 补上全是0的特征图. 整个过程不需要学习任何权重, 计算开销低, 虽然ResNet论文里面提到了这个方案, 但是在主流的实现中, 大家基本都采用Projection Shortcut了, 因为多一点点参数换来更好的精度通常是值得的.
Transformer结构¶
Transformer = Attention + FNN. 它完全不再使用传统的CNN或者RNN结构. 只靠注意力机制来提取特征. 当时为啥要提出Transformer, 是因为RNN面临着两个重要的问题: 1. 计算效率较低, 计算t时刻必须等t-1时刻算完, 这种排队机制导致无法利用GPU强大的并行能力, 训练缓慢; 2. 长距离依赖问题: 虽然LSTM有门控机制, 但是序列一旦特别长, 开头的信息传到最后会严重模糊, 比较健忘.
多头注意力机制¶
如果使用一个Attention(单头), 模型学习到的特征就会比较单一, 通过将输入切人为多个头, 每个头可以独立学习不同的关联性. 例如, 在处理句子的时候, 一个头可能关注语法结构, 另一个头关注代词指代, 还有一个头关注语义相关性.
- 线性映射: 输入向量\(X\)分别和\(h\)组不同的权重矩阵相乘, 每组矩阵包含3个权重矩阵: \(W_i^Q\), \(W_i^K\), \(W_i^V\). 得到\(Q_i=XW_i^Q\), \(K_i = X W_i^K\), \(V_i = X W_i^V\).
- 并行注意力计算: 对每组\(Q_i\), \(K_i\), \(V_i\)进行缩放点积注意力计算: \(head_i = \text{Attention}(Q_i, K_i, V_i) = \text{softmax}(\frac{Q_i K_i^T}{\sqrt{d_k}}) V_i\)
- 拼接&融合: 将所有头的输出按照通道维度拼接在一起. 然后经过一层线性层\(W^O\)进行融合, 得到最终的输出结果. \(Concat(head_1, ..., head_h)W^O\)
矩阵数量: 假设模型有\(h\)个头, 每个头有3个矩阵, 线性映射一共有\(3h\)个权重矩阵, 再加上所有头拼接后相乘的那个输出映射矩阵, 整个多头注意力层一共有\(3h+1\)个权重矩阵.
矩阵维度: 设输入序列长度为L, 模型维度为\(d_{model}\), 头数为\(h\), 每个头的维度为\(d_k=d_{model}/h\). \(W_i^Q\), \(W_i^K\), \(W_i^V\)的维度为\((d_{model}, d_k)\). \(X\)的维度为\((L, d_{model})\). \(Q_i\), \(K_i\), \(V_i\)的维度为\((L, d_k)\). 单个头的输出维度为\((L, d_k)\). 将\(h\)个矩阵拼接, 得到\((L, d_{model})\), 输出权重矩阵\(W^O\)形状为\((d_{model}, d_{model})\), 最终输出\((L, d_{model})\).
打包线性投影: 在工业级实现如PyTorch中, 所有头的权重矩阵\(W_i^Q\), \(W_i^K\), \(W_i^V\)会被合并为一个巨大的矩阵进行一次矩阵乘法. 计算完之后再通过reshape和transpose操作把数据分给不同的头. 具体实现:
- 线性映射: 模型不再定义\(3h\)个小矩阵, 而只是定义一个巨大的线性层权重\(W_{all}\), 维度为\((d_{model}, 3\times d_{model})\), 这个大矩阵在内部逻辑上水平拼接了\(W_Q\), \(W_K\), \(W_V\)三个部分, 每个部分都包含了所有头的参数. 输入矩阵\(X\)的维度是\((B, L, d_{model})\), 直接进行一次矩阵乘法\(Y=X\cdot W_{all}\), 得到结果\(Y\)的维度为\((B, L, 3\times d_{model})\). 此时, 所有头的\(Q, K, V\)的信息全部挤在最后一个维度里面.
- 切分/转置/分割
- 切分: 将维度从\((B, L, 3\times d_{model})\)重塑为\((B, L, 3, h, d_k)\), 此时, 空间上已经切分出了Q, K, V维度和头维度
- 转置: 将维度顺序调整为\((3, B, h, L, d_k)\), 让\(B\)和\(h\)排在前面, 把\(B\times h\)看作是一个整体进行并行计算. 如果不进行转置, 那么序列长度和特征维度组成的小子矩阵在内存中不是连续的, 转置之后, 每一个头内部的这个小子矩阵在内存中变得连续, 这样下一步计算Attention分数的时候, 矩阵乘法效率是最高的.
- 分割: 在第一个维度上进行划分, 得到三个形状为\((B, h, L, d_k)\)的张量, 他们就是真正用于注意力计算的\(Q, K, V\)
*为什么每个head要进行降维**:
* 控制计算量和显存
* 每个head在不同的子空间学习不同的关系
* 保持总维度不变
位置编码¶
Transformer由于取消了RNN中的串行结构, 改为并行处理整个句子. 虽然这提高了速度, 但是也带来了一个致命的问题: 模型无法分辨单词的前后顺序. 如果不加位置编码, "我爱你"和"你爱我"对于模型来说是完全一样的, 为了找回顺序信息, 必须人为地给每个单词打上位置标签.
模型通过Element-wise Addition的方式, 将位置编码向量直接加到词向量上. 他们设计的公式为\(PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}})\), \(PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}})\). \(pos\)是单词在句子中的位置, \(2i\)和\(2i+1\)是词向量维度中的偶数和奇数索引. 例如, 要表示句子"我们", \(d_{model}=4\), "我"的\(PE\): \(PE_{(0,0)} = \sin(0/1) = 0\), \(PE_{(0,1)} = \cos(0/1) = 1\), \(PE_{(0,2)} = \sin(0/100) = 0\), \(PE_{(0,3)} = \cos(0/100) = 1\). 结果: \(PE(pos=0) = [0, 1, 0, 1]\).
为什么是相加而不是拼接呢? 因为拼接会增加维度, 导致后续所有权重矩阵都要变大, 增加计算量. 研究表明, 模型能够自动学习如何在词向量空间中分出一部分维度来识别位置信息. 实际上, 词向量的高维空间足够宽广, 足以同时容纳语义和位置.
为什么设计为正余弦函数的形式呢? 1. 确定性和唯一性: 对于每个位置\(pos\), 其编码向量是唯一的; 2. 线性表示相对位置: 由于三角函数的特性, \(PE_{pos+k}\)可以表示为\(PE_{pos}\)的线性变换. 这意味着模型能够轻松学习到某个词在另一个词后面\(k\)个位置这种关系;
参数¶
- 架构参数: \(d_{model}\), 词嵌入和隐藏层的维度. \(n_{layers}\), 编码器和解码器的层数. \(n_{heads}\), 多头注意力的头数. \(d_{ff}\), 前馈网络中间层的维度. \(v\)词汇表大小; \(max\_seq\_len\), 模型能处理的最大序列长度.
- 可学习参数: embedding, 词向量矩阵和位置编码; \(W^Q\), \(W^K\), \(W^V\)以及\(W^O\); \(\gamma\)和\(\beta\), LN的缩放系数和偏移量; \(W_1\), \(W_2\), \(b_1\), \(b_2\), 两个全连接层的权重矩阵和偏置.
- 训练参数: Dropout Rate, 防止过拟合的丢弃率; Learning Rate Schedule, 预热步数和衰减率; Label Smoothing, 标签平滑值.
Self Attention, Attention及双向LSTM的区别¶
- Attention: 这种又可以称为传统/交叉注意力机制, 通常用于Encoder-Decoder模型, 如机器翻译. 机制是计算两个不同序列之间的关系. 例如在翻译中, 解码器生成的每一个词, 都会去关注编码器中输入句子的哪些词重要. Query来自目标序列, Key/Value来自源序列.
- Self-Attention: 自注意力机制, 是Attention的一种特殊情况, 用于捕捉同一个序列内部的依赖关系. 序列中的每个词都会去关注序列中的所有其他词, 包括它自己, 以计算他们之间的相关性. Query, Key, Value都来自同一个序列.
- BiLSTM: 双向长短期记忆网络, 本质是一种循环神经网络的变体结构. 它通过两个独立的LSTM层(一个正向, 一个反向)处理序列. 它依赖于时间步, 必须按照顺序一个词一个词地读取, 擅长捕捉序列地局部上下文和顺序信息.
根号\(d_k\)的作用¶
在所有基于Transformer的模型中, 除以\(\sqrt{d_k}\)的作用主要有两个:
- 防止Softmax梯度消失: 如果不除以\(\sqrt{d_k}\), 当\(d_k\)(向量维度)较大的时候, \(Q\cdot K^T\)的点积结果会变得非常大. 大数值在经过Softmax函数后, 分布会变得极其尖锐, 即某个元素的概率接近1, 其余接近0, 在这些极端位置, Softmax的梯度几乎为0. 除以\(\sqrt{d_k}\)可以将数值拉回到更加有利于梯度的范围, 保证反向传播的时候梯度能够正常流动, 让模型更加容易训练.
- 稳定数值分布: 假设\(Q\)和\(K\)中的元素都是各自独立且服从均值为0, 方差为1的随机变量. 他们的点积根据统计学规律, 结果均值仍然为0, 但是方差会累积变成\(d_k\). 除以标准差之后, 点积的结果的方差会重新变为1, 这使得输入Softmax的数据分布更加稳定, 不再随着维度\(d_k\)的增加而剧烈波动.
softmax的作用¶
- 归一化: 将网络的原始输出(logits)转换为0-1之间的值, 输出之和为 1, 本质就是一个概率分布
- 放大差异: 指数运算会让大的值变得更大, 小的值变得更小, 增加区分度
- 多分类适配: 用于多分类任务的输出层, 告诉你每个类别的概率是多少
对比学习里面的temperature和LLM 解码器的 temperature 是一个东西吗¶
形式上相似, 本质上不是一回事. 前者是训练超参数, 后者是推理超参数, 前者改变学习过程, 后者只改变取样方式.
对比学习里面的temperature, 影响的是训练目标, 他决定 loss 怎么看待"正负样本分数差", 所以他会改变梯度, 进而改变最终学习到的表征. 也就是说, 他会影响模型怎么学.
LLM 解码里面的 temperature, 影响的是采样策略, 模型应训练完了, 他只是调整输出的概率分布, 让生成更加保守或者更加发散. 他不改变模型参数, 也就是说, 他影响模型怎么说, 不影响模型怎么学.
介绍一下RoPE¶
RoPE是Rotary Positional Embedding, 中文是"旋转位置编码". 他的核心作用, 是给Transformer注入位置信息, 但是方式不是"把位置向量直接加到 token 向量上", 而是对query和key做一种与位置相关的旋转.
想象一个二维点(x, y)在平面上. 把它逆时针旋转一个角度\(\theta\)之后, 会变成一个新的点(x', y'). 旋转之后, 向量的长度不变, 而是朝向变了. RoPE就是在做这样一件事情: 把向量的不同维度, 两两组成一个"小平面", 然后按照位置去旋转. 先来看最简单的一对维度, 二维旋转之后的结果是:
- \(x'=x \cos{\theta}-y\sin{\theta}\)
- \(y'=x\sin{\theta} + y\cos{\theta}\)
两个向量方向越接近, 点积通常越大, 方向差得越多, 点积通常越小. 把它放到attention上面, 就是算\(Q\cdot K\), RoPE做的是, 给第\(m\)个位置的\(Q\)旋转, 给第\(n\)个位置的\(k\)旋转, 结果是, 他们的点积大小, 不只和内容相关, 还会和相对位置相关, 也就是说, RoPE自动把"谁离谁更近/更远"塞进attention里面了.
那么, 具体的来说:
先假设在某一层里面, token在位置\(m\)上面的query是\(q\), key是\(k\), 它的维度是\(d\), RoPE会把\(d\)拆为很多对: \((q_0, q_1), (q_2, q_3), ...\), 每一对都看为平面上的一个向量. 然后给每一对分配一个频率, 第\(i\)对的频率通常写为: \(\omega_i=10000 ^{-2i/d}\), 这里\(i=0, 1, 2, ..., d/2-1\). 对于位置\(m\), 这一对维度对应的旋转角度就是\(\theta (m, i) = m\times \omega_i\). 也就是说, 位置越靠后, 旋转得越多, 不同维度对旋转速度不同. 然后对\(q\)和\(k\)分别做旋转, 对于第\(i\)对维度, 如果原来是\((x, y)\), 那么旋转之后变为\(x'=x \cos{\theta_i}-y\sin{\theta_i}\), \(y'=x\sin{\theta_i} + y\cos{\theta_i}\). 得到旋转之后的\(q', k'\). 最后, 用他们去算attention score.
-
为什么要设计为高维和低维转速不同?
转得块的那几个维度, 相邻位置改变, 角度就变化很多, 所以他们对局部位置很敏感. 比如token从位置10到位置11, 这些维度的相位差就会很明显, 适合区分近邻顺序. 转的慢的那几组维度, 走的远角度也只变化一点, 所以他们对长距离关系更加稳定. 这样两个相隔很远的位置也不会因为转太快绕了很多圈而很快混淆.
-
维度不是偶数怎么办?
标准RoPE需要把向量按照两两配对的方式旋转, 所以天然要求参与旋转的那部分维度数量是偶数. 如果总维度是奇数, 常见的处理方法有以下几种:
- 最常见的是: 只对前面最大的偶数部分做RoPE, 最后剩下1维不旋转. 比如\(d=65\), 那就对前面的\(64\)维做旋转, 第\(65\)维原样保留.
- 另一种做法是: 只对一个偶数的rotary_dim做RoPE, 其余维度不动: 这在很多模型中很常见, 比如head_dim是80, 但是rotary_dim可能只取64.
-
非线性长度外推
模型训练的时候只见过较短序列, 但是测试的时候需要长序列. 什么叫"线性"外推, 就是把位置按照固定比例拉伸或者压缩, 比如原来的位置是0, 1, 2, 3..., 现在简单的变为0, 0.5, 1, 1.5, 或者整体乘以一个系数.
而"非线性"长度外推是更进一步, 不是所有位置都按照同一个比例进行缩放, 而是不同区间缩放得不一样, 直觉上, 它像是一根橡皮筋, 不是均匀拉伸, 而是近处位置保留更多细节, 中间位置适度压缩, 很远的位置压得更加厉害. 这样做的原因很朴素, 模型对于近距离依赖和远距离依赖的需求不一样. 近处尝尝需要高精度顺序信息, 远处多是"知道大概多远, 在前还是在后"就够了. 所以使用非线性映射, 可以把有限的位置表达能力更聪明地分配出去. 可以将其想象为"地图缩放", 线性方法像把整张地图均匀缩小, 问题是, 城市中心和郊区都同样被缩小, 中心细节容易模糊掉. 非线性方法像是"鱼眼地图", 中心保留更细, 越远压缩越多, 这样更加符合使用需求.
如果放到RoPE上理解, 会更加自然一些. RoPE的本质是在Q/K上按照位置施加旋转, 问题在于, 当位置编号超出训练范围太多的时候, 这些旋转角度会进入模型没见过的区域, 导致注意力模式的失真. 长度外推方法的核心, 就是重新定义"位置编号送进RoPE之前应该长什么样", 线性方法是统一缩放, 而非线性方法是使用曲线映射, 让不同距离区间得到不同待遇.
介绍一下Transformer-XL¶
普通Transformer有一个问题, 它一次只能看这一个窗口里面的token. 假设文本太长, 被切问很多段, 它在处理第二段的时候, 基本就看不到第一段的隐藏状态了, 所以长距离依赖会断掉. Transformer-XL的核心就是在做两件事情:
- segment-level recurrence: 处理当前段的时候, 它会把上一段的隐藏表示当做记忆一起带过来. 所以第二段不只是看自己, 还能参考第一段留下来的memory, 这样上下文就能跨段连续, 而不是每段都从零开始.
- 相对位置编码: 如果你直接把上一段的hidden state带过来, 位置编号会比较麻烦, Transformer用相对位置而不是绝对位置, 让模型更加自然地处理"当前token和过去token相差多远", 这样跨段复用记忆会更合理.
BERT结构¶
BERT是谷歌在2018年提出的预训练语言模型. 它基于Transformer的Encoder部分, 利用Self-Attention机制并行计算. 和GPT(单向)不同, BERT能够同时利用某个词的左边和右边的上下文信息, 从而学习到更加深刻的语义. 其预训练任务分为两类: 一类是MLM(Masked Language Model), 随机Mask掉15%的词, 让模型根据上下文预测这些词(类似完型填空), 让模型学习到词和词之间的关系. 另一类是NSP(Next Sentence Prediction), 预测第二句话是否是第一句话的下文, 这让模型学到句子和句子之间的关系.
文本相似度计算¶
在使用BERT计算文本相似度的时候, 主要有两种形式, 一种是交互式, 一种是表示式.
- 方法1, 交互式, 将两个句子
SentenceA和SentenceB用[SEP]拼接成一条输入:[CLS] SenetenceA [SEP] SentenceB [SEP], 将整体输入到BERT, 取首位的[CLS]向量, 通过一个全连接层直接输出相似度分数(回归任务)或者类别(分类任务). 优点是精度最高, 因为两个句子的Token在BERT的每一层都在通过Attention进行深度的交互, 模型能够精准捕捉细节差异. 但是速度极慢. 如果要从100万个句子中找最相似的, 就要做100万次的BERT前向推理. - 方法2, 表示式, 将BERT作为特征提取器.
SentenceA输入BERT, 得到句向量u;SentenceB输入BERT, 得到句向量v. 计算两个向量的余弦相似度. 直接使用原生BERT的[CLS]做句向量效果很差, 必须要使用Sentence-BERT(SBERT)进行微调训练后效果才好. 优点是效率很高, 文档向量可以离线预计算并存入向量数据库, 如Milvus/Faiss, 检索的时候只要做向量点积, 速度非常快. 但是精度略差于方法一, 因为缺少深层次的语义交互.
RoBERTa和BERT的异同¶
RoBERTa和BERT在整体架构上是同一类模型, 但是RoBERTa可以看作是对BERT与训练策略的强化版本, 在训练任务, 数据规模和mask方式等发面做了系统优化, 从而在大多数的NLP任务上性能更好.
| 维度 | BERT | RoBERTa | 面试关键点 |
|---|---|---|---|
| Masking策略 | 静态Masking:预处理阶段一次性生成,训练中不变 | 动态Masking:每次输入数据时动态生成Mask | 动态Mask避免了模型"记住"Mask位置,增加了数据多样性 |
| 预训练任务 | MLM (Masked LM) + NSP (Next Sentence Prediction) | 仅 MLM,去掉NSP | 实验证明NSP任务对性能帮助不大,甚至有害(尤其在跨文档场景) |
| Batch Size | 较小 (256 sequences) | 很大 (2k ~ 8k) | 大Batch Size训练更稳定,梯度下降方向更准,也利于并行加速 |
| 数据量 | 16GB (BookCorpus + Wiki) | 160GB (+CC-News, OpenWebText等) | "大力出奇迹",更多的数据是性能提升的关键 |
| 文本编码 | Character-level BPE (30k vocab) | Byte-level BPE (50k vocab) | RoBERTa的词表更大,且Byte-level BPE对未登录词(OOV)处理更好 |
BERT为何选择15%作为MLM的比例¶
- 太低: 监督信号不足
- 太高: 输入被破坏过多
BERT的非线性来源¶
- 激活函数
- softmax
BERT和GPT的区别¶
- 模型架构上的区别: BERT使用的是Transformer的Encoder部分, 他在看一个词的时候, 会同时结合这个词的左边和右边的上下文理解. GPT使用的是Transformer的Decoder部分, 他是按照从左到右的方式, 一个词一个词往后预测.
- 训练方式上的区别: BERT采用的是MLM训练的, 训练的时候, 会把句子中的一些词遮住, 让模型去猜被遮住的词. GPT的训练方式是自回归语言模型, 给前面的词, 预测下一个词.
- 使用场景上的区别: BERT更加偏向理解类任务, 更加适合文本分类, 情感分析, 命名实体识别(从非结构化文本中识别具有特定意义的实体, 例如人名, 地名, 机构名, 专有名词, 时间, 数值, 货币等), 句子相似度判断, 搜索排序, 信息检索. GPT更偏向生成任务, 适合对话系统, 文章写作, 文本续写, 摘要生成, 翻译, 代码生成, 通用助手.
多模态大模型¶
CLIP的对比学习原理¶
CLIP的对比学习就是把正确的图文对拉近, 把错误的图文对推远. 过程很简单: 将图片编码为向量, 文本编码为向量, 正确配对的图文对相似度变高, 错误配对的图文对相似度变低. 让图文和文本对齐到同一个语义空间, 让模型知道"这张图对应哪段文本".
CLIP的loss本质上是InfoNCE, 而且是双向的. 先算一个batch里面所有图片向量和文本向量的相似度, 得到一个相似度矩阵, 如果batch size是N, 那么就会得到一个N*N的矩阵, 其中, 对角线位置代表正确的图文对; 非对角线位置代表错误的图文对, 目标是让对角线的相似度尽量高, 让其他位置的相似度尽量低. 具体的做法是双向cross-entropy, 以image -> text为例, 把这张图片和所有文本做相似度比较, 希望这张图片对应的那个文本概率最高, 相当于做一个N分类. 总的损失是双向 loss 取平均.

BLIP/BLIP-2/GME¶
详情见多模态大模型.