
走完大模型构建全流程1#

RMS Norm#
RMSNORM 正则(归一化)
如果输入X过大过小,导致梯度爆炸、消失。
归一化,标准差变1
RMSnorm比传统Norm少了均值的相关计算
(其中,是可学习的参数)
代码实现:
class RMSNorm(nn.Module):
def __init__(self, dim:int,eps:float=1e-5):
super().__init__()
self.dim=dim
self.eps=eps
self.weight=nn.Parameter(torch.ones(dim)) #可学习的参数gamma
def _norm(self,x):
return x*torch.rsqrt(x.pow(2).mean(-1,keepdim=True)+self.eps)
#__init__初始化
#主要逻辑
#前向forward
def forward(self,x):
return self.weight*self._norm(x.float()).type_as(x)pythonRoPE#
文本的先后位置与语义有很大关系,因此需要位置编码(相对位置、绝对位置)
使用相对位置编码,旋转位置编码(RoPE、YaRN)
为什么 Transformer 需要位置编码#
核心问题:Self-Attention 的置换不变性#
Transformer 的核心组件是 Self-Attention(自注意力机制)。其计算公式为:
其中:
- ,,
- 是输入序列的嵌入矩阵, 是序列长度, 是嵌入维度
关键观察:Self-Attention 本质上是对输入序列做加权求和,而加权求和操作具有置换不变性(Permutation Invariance)。
具体来说,如果我们打乱输入序列的顺序:
其中 是任意置换矩阵,则:
这意味着:无论输入序列中 token 的顺序如何,Self-Attention 的输出(在重排后)完全相同。
为什么这成为问题#
语言是有序的。同一个句子中词语的顺序不同,含义完全不同:
| 句子 | 含义 |
|---|---|
| 猫追狗 | 猫是施动者 |
| 狗追猫 | 狗是施动者 |
如果模型无法区分位置,那么”猫追狗”和”狗追猫”在模型看来就是相同的输入——这显然无法完成任何有意义的语言理解任务。
位置编码的目标是打破置换不变性,将序列中每个 token 的位置信息注入到模型中。形式化地说,我们需要一个函数 ,使得:
或者更一般地:
其中 是位置索引, 是注入了位置信息的表示。这样,模型就能区分不同位置上的 token。
3. RoPE 详细理论推导#
3.1 设计目标#
RoPE 的设计目标是:找到一种位置编码函数 ,使得 query 向量和 key 向量的内积仅依赖于相对位置:
其中 分别是 query 和 key 的绝对位置, 是某个只与相对位置 有关的函数。
3.2 二维情况的推导#
为了建立直觉,我们先从最简单的二维情况开始。
3.2.1 问题设定#
对于二维向量 ,我们希望找到函数 ,使得:
3.2.2 旋转矩阵的直觉#
在二维空间中,一个自然的选择是旋转操作:
其中 是旋转角度为 的旋转矩阵:
3.2.3 验证条件#
计算内积:
这里利用了旋转矩阵的性质:
- (转置等于反向旋转)
- (旋转的复合)
因此,,它确实只依赖于相对位置 !
3.3 推广到高维#
3.3.1 分块旋转#
对于 维向量( 为偶数),我们将 维空间分成 个二维子空间,每个子空间独立应用旋转:
其中每个 是二维旋转矩阵:
3.3.2 频率的选择#
每个二维子空间对应一个旋转频率 。RoPE 采用与原始 Transformer 正弦编码类似的几何级数:
展开来说:
- (最高频,对应前两个维度)
- (最低频,对应最后两个维度)
直觉理解:
- 高频分量(小 ):旋转速度快,对位置变化敏感,捕捉局部位置关系
- 低频分量(大 ):旋转速度慢,对位置变化不敏感,捕捉全局位置关系
这类似于傅里叶变换中不同频率分量的作用。
3.3.3 完整的旋转矩阵#
将位置 处的 维向量 应用 RoPE:
展开为:
RoPE 的核心数学性质#
性质 1:相对位置性#
内积只依赖于相对位置 。
性质 2:远程衰减#
随着相对距离 增大,内积趋向于衰减。这是因为不同频率分量 的贡献会相互抵消(类似于多频信号的干涉效应)。
数学上可以证明,当 足够大时:
这是一个随 增大而衰减的振荡函数。
性质 3:线性复杂度#
RoPE 的计算不需要额外的参数,只需要对 Q 和 K 的每个位置应用旋转,时间复杂度为 。
RoPE 实现详解#
核心实现思路#
直接构造 旋转矩阵效率低下。RoPE 的高效实现利用了一个关键观察:旋转操作可以分解为逐对的元素级操作。
对于每对 :
高效版本(避免交错操作)#
高效的做法是利用数学恒等式:
可以重写为:
其中 将 变为
def apply_rotary_pos_emb_efficient(q, k, cos, sin):
"""高效 RoPE 实现"""
def rotate_half(x):
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat([-x2, x1], dim=-1)
cos = cos.unsqueeze(0).unsqueeze(2)
sin = sin.unsqueeze(0).unsqueeze(2)
q_embed = q * cos + rotate_half(q) * sin
k_embed = k * cos + rotate_half(k) * sin
return q_embed, k_embedpython4.3 完整的 Transformer 层示例#
class MultiHeadAttentionWithRoPE(nn.Module):
"""带 RoPE 的多头注意力"""
def __init__(self, d_model: int, num_heads: int, max_seq_len: int = 8192):
super().__init__()
self.d_model = d_model
self.num_heads = num_heads
self.head_dim = d_model // num_heads
self.q_proj = nn.Linear(d_model, d_model)
self.k_proj = nn.Linear(d_model, d_model)
self.v_proj = nn.Linear(d_model, d_model)
self.o_proj = nn.Linear(d_model, d_model)
self.rope = RotaryPositionalEmbedding(self.head_dim)
def forward(self, x: torch.Tensor, mask: torch.Tensor = None):
batch, seq_len, _ = x.shape
q = self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
k = self.k_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
v = self.v_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
# 应用 RoPE 到 Q 和 K(V 不需要位置编码)
cos, sin = self.rope(x, seq_len)
q, k = apply_rotary_pos_emb(q, k, cos, sin)
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
scale = self.head_dim**-0.5
attn_weights = torch.matmul(q, k.transpose(-2, -1)) * scale
if mask is not None:
attn_weights = attn_weights.masked_fill(mask == 0, float("-inf"))
attn_weights = torch.softmax(attn_weights, dim=-1)
attn_output = torch.matmul(attn_weights, v)
attn_output = (
attn_output.transpose(1, 2).contiguous().view(batch, seq_len, self.d_model)
)
return self.o_proj(attn_output)pythonYaRN (Yet another RoPE extensioN)#
YaRN 结合了 NTK 和注意力缩放,对不同频率应用不同的缩放策略:
- 低频分量:使用插值(缩放位置)
- 高频分量:保持不变
def yarn_rope(dim, seq_len, train_len, target_len, base=10000.0):
"""YaRN RoPE 简化版"""
import math
scale = target_len / train_len
wavelength = 2 * math.pi / (base ** (torch.arange(0, dim, 2).float() / dim))
low_freq_factor = train_len / (2 * math.pi)
inv_freq = []
for w in wavelength:
if w > low_freq_factor:
inv_freq.append(1.0 / (w * scale))
else:
inv_freq.append(1.0 / w)
inv_freq = torch.tensor(inv_freq)
# ... 后续与标准 RoPE 相同python
FFN层#

为了保证总参数为8l^2,三个W都把维度调整到
动态学习率
余弦退火方法
LoRA
损失函数设计#
llm的末端实际是一个分类问题(自回归),根据前t个的词预测第t+1个词,
t+1词的真实值是one-hot向量,预测logits是此表长度的归一化的概率分布,
对两个向量求交叉熵损失,
p(x)是真实值,q(x)是预测值
这样代入可知,单个token的loss=
对于所有token总预测损失是,
大语言模型的核心任务是 自回归预测 ——给定前文,预测下一个词的概率分布:
P(wt∣w1,w2,…,wt−1;θ)
整个训练集上的目标函数是 最大化所有词的联合对数似然 ,即最小化其负对数似然,即为上文所说的交叉熵损失。