Stanford CS336 | Assignment 1 - Transformer Language Model Architecture

cs336系列(1) 第一步先克隆到本地如果你的环境中没有下载uv,先安装一下然后激活下载需要的库和包,如果下载速度慢可以设置镜像源uv run。 阅读详情

所有关于 assignment1 的代码已开源在:
https://github.com/ACEEE-1222/Standford-CS336-Assignment-1
如果对你有帮助的话,记得顺手点个star喔!

作业要求见 https://github.com/stanford-cs336/assignment1-basics

作业1后半段要求从零实现一个基于Transformer的语言模型(LM)——这是理解现代大语言模型(LLM)内部机制的关键实践。

本文将详细拆解Transformer语言模型的完整实现过程,涵盖多头注意力、旋转位置编码(RoPE)、RMS归一化、SwiGLU前馈网络等核心组件,同时讲解自定义AdamW优化器、学习率调度、批量数据处理等训练辅助工具,帮助读者掌握从模型构建到训练落地的全流程。

一、项目概述

本次作业的核心目标是搭建一个仅含解码器的Transformer语言模型(类似GPT结构),使其具备预测序列中下一个token的能力。模型采用了当前主流的设计方案(如预归一化、RoPE、RMSNorm)以兼顾效率与性能,同时配套实现了完整的训练流水线,可直接在文本数据上进行优化。

模型与训练框架的核心特点:

  • 基于解码器的Transformer结构,包含多头自注意力机制
  • 采用旋转位置编码(RoPE),增强模型对序列位置信息的捕捉能力
  • 使用RMSNorm替代传统LayerNorm,提升训练稳定性
  • 前馈网络中引入SwiGLU激活函数,性能优于ReLU等传统激活
  • 自定义AdamW优化器、余弦学习率调度器与梯度裁剪模块
  • 支持训练 checkpoint 保存与加载,便于中断后续训

二、核心组件实现:transformer.py

transformer.py 文件包含了Transformer语言模型的所有核心模块,各组件设计遵循模块化原则,既便于调试,也为后续扩展预留了空间。

2.1 基础通用层

这类层是模型的"基础工具",在多个模块中被复用,负责实现最基本的张量变换操作。

2.1.1 无偏置线性层(Linear)

简化版的线性变换层,移除了偏置项(bias),并采用类Xavier初始化(截断正态分布)保证训练稳定性。前向传播通过einops库实现清晰的张量维度映射,避免手动reshape导致的维度混乱。

class Linear(nn.Module):
    def __init__(self, in_features: int, out_features: int, device=None, dtype=None):
        super().__init__()
        # 定义权重参数:形状为 (输出维度, 输入维度)
        self.weight = nn.Parameter(torch.empty((out_features, in_features), device=device, dtype=dtype))
        # 类Xavier初始化:标准差 = sqrt(2/(输入维度 + 输出维度)),避免梯度消失/爆炸
        std = (2 / (in_features + out_features)) ** 0.5
        nn.init.trunc_normal_(self.weight, std=std, a=-3*std, b=3*std)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 前向计算:y = x @ W^T(输入形状 ..., in_features → 输出形状 ..., out_features)
        return einsum(x, self.weight, "... in_features, out_features in_features -> ... out_features")
2.1.2 词嵌入层(Embedding)

将离散的token ID映射为连续的稠密向量,嵌入维度(embedding_dim)与模型隐藏层维度(d_model)保持一致。权重同样采用截断正态分布初始化,确保初始嵌入向量的分布合理性。

class Embedding(nn.Module):
    def __init__(
        self,
        num_embeddings: int,  # 词汇表大小(即总token数)
        embedding_dim: int,   # 嵌入向量维度(需等于d_model)
        device: torch.device | None = None,
        dtype: torch.dtype | None = None,
    ):
        super().__init__()
        self.vocab_size = num_embeddings
        self.d_model = embedding_dim
        # 嵌入权重矩阵:形状为 (词汇表大小, 嵌入维度)
        self.weight = nn.Parameter(torch.empty((self.vocab_size, self.d_model), device=device, dtype=dtype))
        nn.init.trunc_normal_(self.weight, std=1, a=-3, b=3)

    def forward(self, token_ids: torch.LongTensor) -> torch.Tensor:
        # 输入:(batch_size, seq_len) → 输出:(batch_size, seq_len, embedding_dim)
        return self.weight[token_ids]  # 通过索引直接获取对应token的嵌入向量
2.1.3 RMS归一化层(RMSNorm)

相比传统LayerNorm,RMSNorm移除了均值中心化步骤,仅对输入的均方根(RMS)进行归一化,在减少计算量的同时提升训练稳定性,是LLaMA、GPT-4等模型的默认归一化方案。

class RMSNorm(nn.Module):
    def __init__(
        self,
        d_model: int,          # 输入维度(需等于模型隐藏层维度)
        eps: float = 1e-5,     # 防止分母为0的微小值
        device: torch.device | None = None,
        dtype: torch.dtype | None = None,
    ):
        super().__init__()
        self.d_model = d_model
        self.eps = eps
        # 可学习的缩放参数:形状为 (d_model,),初始化为1(不改变归一化结果)
        self.weight = nn.Parameter(torch.ones(self.d_model, device=device, dtype=dtype))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 输入:(batch_size, seq_len, d_model) → 输出:同输入形状
        in_dtype = x.dtype  # 保存输入数据类型,避免精度损失
        x = x.to(dtype=torch.float32)  # 转为float32计算,提升数值稳定性
        
        # 1. 计算最后一维的均方根(RMS)
        rms = (x.pow(2).mean(-1, keepdim=True) + self.eps) ** 0.5
        # 2. 归一化 + 应用缩放参数
        out = x / rms * self.weight
        
        return out.to(dtype=in_dtype)  # 恢复原数据类型

2.2 激活函数与前馈网络

前馈网络(FFN)是Transformer中负责"特征转换"的核心模块,而SwiGLU则是当前性能最优的激活函数之一,两者结合可显著提升模型的表达能力。

2.2.1 SwiGLU激活函数

SwiGLU是GLU(Gated Linear Unit)的变体,通过Sigmoid门控对线性变换结果进行筛选,相比ReLU能更好地捕捉特征间的非线性关系,同时避免梯度消失问题。

class SwiGLU(nn.Module):
    def __init__(
        self,
        d_model: int,    # 输入维度(模型隐藏层维度)
        d_ff: int,       # 前馈网络中间层维度(通常为d_model的4倍)
        device: torch.device | None = None,
        dtype: torch.dtype | None = None,
    ):
        super().__init__()
        self.d_model = d_model
        self.d_ff = d_ff
        # 定义三个线性层:W1/W3用于生成门控与候选特征,W2用于输出投影
        self.w1 = Linear(d_model, d_ff, device=device, dtype=dtype)
        self.w2 = Linear(d_ff, d_model, device=device, dtype=dtype)
        self.w3 = Linear(d_model, d_ff, device=device, dtype=dtype)

    # 辅助函数:Sigmoid线性单元(SiLU)
    def _silu(self, x: torch.Tensor) -> torch.Tensor:
        return x * torch.sigmoid(x)

    # 辅助函数:门控线性单元(GLU)
    def _glu(self, x: torch.Tensor) -> torch.Tensor:
        return self._silu(self.w1(x)) * self.w3(x)  # SiLU门控 × W3线性变换结果

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 输入:(batch_size, seq_len, d_model) → 输出:同输入形状
        return self.w2(self._glu(x))  # 门控结果通过W2投影回d_model维度

2.3 位置编码:旋转位置编码(RoPE)

Transformer本身不具备位置感知能力,需通过位置编码注入序列顺序信息。RoPE通过旋转矩阵将位置信息编码到token的嵌入向量中,且支持长度外推(对长于训练序列的文本仍有效),是当前主流的位置编码方案。

class ROPE(nn.Module):
    def __init__(
        self,
        theta: float,       # RoPE基础频率(通常设为10000)
        d_k: int,           # 注意力头维度(需为偶数,因按奇偶维度分组旋转)
        max_seq_len: int,   # 支持的最大序列长度
        device: torch.device | None = None
    ):
        super().__init__()
        self.theta = theta
        self.d_k = d_k
        self.max_seq_len = max_seq_len
        self.device = device

        # 预计算cos/sin缓存(仅在首次初始化时计算,避免重复计算)
        if not hasattr(self, "cos_cached") or not hasattr(self, "sin_cached"):
            # 1. 计算频率矩阵:shape (d_k//2,)
            freqs_d = 1 / (theta ** (torch.arange(0, d_k, 2, device=device).float() / d_k))
            # 2. 计算位置矩阵:shape (max_seq_len,)
            pos_i = torch.arange(max_seq_len, device=device).float()
            # 3. 频率-位置外积:shape (max_seq_len, d_k//2)
            freqs = einsum(freqs_d, pos_i, "d_half, max_seq_len -> max_seq_len d_half")

            # 预计算cos和sin值(后续直接索引使用)
            self.register_buffer("cos_cached", torch.cos(freqs), persistent=False)
            self.register_buffer("sin_cached", torch.sin(freqs), persistent=False)

    def forward(
        self,
        x: torch.Tensor,                # 输入:(..., seq_len, d_k)
        token_positions: torch.Tensor   # 位置索引:(..., seq_len)
    ) -> torch.Tensor:
        # 1. 按最后一维的奇偶索引分组(d_k需为偶数)
        x_odd = x[..., 1::2]  # 奇数维度:索引1,3,5...
        x_even = x[..., ::2]  # 偶数维度:索引0,2,4...

        # 2. 获取当前序列长度对应的cos/sin值
        cos = self.cos_cached[token_positions]  # (..., seq_len, d_k//2)
        sin = self.sin_cached[token_positions]  # (..., seq_len, d_k//2)

        # 3. 应用旋转公式:将位置信息融入向量
        out1 = cos * x_even - sin * x_odd  # 偶数维度旋转结果
        out2 = sin * x_even + cos * x_odd  # 奇数维度旋转结果

        # 4. 重组维度:将奇偶分组合并回原d_k维度
        out = torch.stack([out1, out2], dim=-1).flatten(-2)  # (..., seq_len, d_k)
        return out

2.4 注意力机制:多头自注意力(Multi-Head Self-Attention)

注意力机制是Transformer的核心,负责捕捉序列内token间的依赖关系。多头注意力通过将隐藏层向量拆分到多个"头"中,并行计算注意力,可捕捉不同维度的依赖信息。

2.4.1 缩放点积注意力(辅助函数)

基础注意力计算模块,通过"查询(Q)-键(K)-值(V)"机制计算注意力权重,并引入缩放因子(√d_k)避免注意力分数过大导致的Softmax饱和问题。

def scaled_dot_product_attention(
    query: torch.Tensor,  # Q:(batch_size, ..., seq_len_q, d_k)
    key: torch.Tensor,    # K:(batch_size, ..., seq_len_k, d_k)
    value: torch.Tensor,  # V:(batch_size, ..., seq_len_k, d_v)
    mask: torch.Tensor = None  # 掩码:(seq_len_q, seq_len_k),True表示可关注
) -> torch.Tensor:
    d_k = query.shape[-1]
    # 1. 计算Q与K的点积(注意力分数),并除以√d_k缩放
    attention_scores = einsum(query, key, "... seq_len_q d_k, ... seq_len_k d_k -> ... seq_len_q seq_len_k") / (d_k ** 0.5)
    
    # 2. 应用掩码(如因果掩码,避免关注未来token)
    if mask is not None:
        attention_scores = attention_scores.masked_fill(~mask, float('-inf'))  # 掩码位置设为-∞,Softmax后权重为0
    
    # 3. Softmax归一化得到注意力权重,再与V加权求和
    attention_weights = softmax(attention_scores, dim=-1)
    output = einsum(attention_weights, value, "... seq_len_q seq_len_k, ... seq_len_k d_v -> ... seq_len_q d_v")
    
    return output

# 辅助函数:自定义Softmax(避免数值溢出)
def softmax(x: torch.tensor, dim: int):
    x_max = torch.max(x, dim=dim, keepdim=True).values  # 减去最大值,防止指数爆炸
    x_exp = torch.exp(x - x_max)
    sum_exp = torch.sum(x_exp, dim=dim, keepdim=True)
    return x_exp / sum_exp
2.4.2 多头自注意力模块

将输入向量通过线性层投影为Q、K、V,拆分到多个头中并行计算注意力,最后将所有头的结果拼接并投影回模型隐藏层维度(d_model)。

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, d_model: int, num_heads: int, 
                theta: float | None = None,
                max_seq_len: int | None = None,
                ):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # 每个头的维度(d_model需能被num_heads整除)
        self.d_v = d_model // num_heads  # 简化设计:V的维度与Q/K一致

        # 1. Q/K/V投影层:将d_model映射为 num_heads × d_k(或d_v)
        self.q_proj = Linear(d_model, num_heads * self.d_k)
        self.k_proj = Linear(d_model, num_heads * self.d_k)
        self.v_proj = Linear(d_model, num_heads * self.d_v)
        # 2. 输出投影层:将多个头的结果拼接后映射回d_model
        self.output_proj = Linear(num_heads * self.d_v, d_model)

        # 3. 若传入theta和max_seq_len,初始化RoPE模块
        if theta is not None and max_seq_len is not None:
            self.rope = ROPE(theta, self.d_k, max_seq_len)

    def forward(self, x: torch.Tensor, 
                mask: torch.Tensor | None = None,
                token_positions: torch.Tensor | None = None) -> torch.Tensor:
        # 输入:(batch_size, seq_len, d_model)
        *batch_dims, seq_len, _ = x.shape  # 提取批次维度(如batch_size)

        # 1. Q/K/V投影与多头拆分
        x_q = self.q_proj(x)  # (batch_size, seq_len, num_heads×d_k)
        x_k = self.k_proj(x)  # (batch_size, seq_len, num_heads×d_k)
        x_v = self.v_proj(x)  # (batch_size, seq_len, num_heads×d_v)
        
        # 拆分多头:(batch_size, num_heads, seq_len, d_k)
        x_q = rearrange(x_q, "... seq_len (num_heads d_k) -> ... num_heads seq_len d_k", 
                        num_heads=self.num_heads, d_k=self.d_k)
        x_k = rearrange(x_k, "... seq_len (num_heads d_k) -> ... num_heads seq_len d_k", 
                        num_heads=self.num_heads, d_k=self.d_k)
        x_v = rearrange(x_v, "... seq_len (num_heads d_v) -> ... num_heads seq_len d_v", 
                        num_heads=self.num_heads, d_v=self.d_v)

        # 2. 应用RoPE(若已初始化)
        if hasattr(self, "rope"):
            # 若未指定token_positions,默认按0~seq_len-1顺序编码
            if token_positions is None:
                token_positions = torch.arange(seq_len, device=x.device)
                #                # 扩展token_positions维度以匹配输入批次维度
                for _ in range(len(batch_dims)):
                    token_positions = token_positions.unsqueeze(0)
            # 对Q和K应用RoPE(V无需旋转)
            x_q = self.rope(x_q, token_positions)
            x_k = self.rope(x_k, token_positions)

        # 3. 生成掩码(默认使用因果掩码,防止关注未来token)
        if mask is None:
            # 因果掩码:上三角为False(不可关注),下三角及对角线为True(可关注)
            mask = torch.tril(torch.ones(seq_len, seq_len, dtype=torch.bool, device=x.device))
            # 扩展掩码维度以匹配批次和头数维度
            for _ in range(len(batch_dims) + 1):  # +1 是为了适配num_heads维度
                mask = mask.unsqueeze(0)
        else:
            # 扩展用户提供的掩码维度
            for _ in range(len(batch_dims) + 1):
                mask = mask.unsqueeze(0)

        # 4. 计算缩放点积注意力
        attn_output = scaled_dot_product_attention(x_q, x_k, x_v, mask)

        # 5. 拼接多头结果并投影
        attn_output = rearrange(attn_output, "... num_heads seq_len d_v -> ... seq_len (num_heads d_v)",
                              num_heads=self.num_heads, d_v=self.d_v)
        output = self.output_proj(attn_output)  # 投影回d_model维度

        return output


### 2.5 Transformer块(TransformerBlock)
单个Transformer块是模型的基本重复单元,由"多头自注意力+前馈网络"组成,并采用**预归一化**(Pre-normalization)设计——在注意力和前馈网络前应用归一化,而非之后,这已被证明能显著提升训练稳定性。

```python
class TransformerBlock(nn.Module):
    def __init__(self, d_model: int, num_heads: int, d_ff: int,
                 theta: float | None = None,
                 max_seq_len: int | None = None,):
        super().__init__()
        # 1. 归一化层(预归一化设计)
        self.ln1 = RMSNorm(d_model)  # 注意力层前的归一化
        self.ln2 = RMSNorm(d_model)  # 前馈网络前的归一化
        
        # 2. 前馈网络
        self.ffn = SwiGLU(d_model, d_ff)
        
        # 3. 多头自注意力(若指定theta和max_seq_len,则启用RoPE)
        if theta is not None and max_seq_len is not None:
            self.attn = MultiHeadSelfAttention(d_model, num_heads, theta, max_seq_len)
        else:
            self.attn = MultiHeadSelfAttention(d_model, num_heads)
    
    def forward(self, 
            x: torch.Tensor, 
            mask: torch.Tensor | None = None, 
            token_positions: torch.Tensor | None = None) -> torch.Tensor:
        # 残差连接 + 注意力:x = x + Attention(LN(x))
        x = x + self.attn(self.ln1(x), mask=mask, token_positions=token_positions)
        # 残差连接 + 前馈网络:x = x + FFN(LN(x))
        x = x + self.ffn(self.ln2(x))
        return x

2.6 完整语言模型(TransformerLM)

将所有组件组合成最终的Transformer语言模型,包含词嵌入层、多个Transformer块、最终归一化层和语言模型头(LM Head)。

class TransformerLM(nn.Module):
    def __init__(self, vocab_size: int,
                 context_length: int,  # 上下文窗口长度(最大序列长度)
                 num_layers: int,       # Transformer块数量
                 d_model: int,          # 模型隐藏层维度
                 num_heads: int,        # 注意力头数
                 d_ff: int,             # 前馈网络中间层维度
                 theta: float | None = None,):  # RoPE的基础频率
        super().__init__()

        self.vocab_size = vocab_size
        self.context_length = context_length
        
        # 1. 词嵌入层:将token ID映射为d_model维度向量
        self.token_embeddings = Embedding(vocab_size, d_model)
        
        # 2. 堆叠多个Transformer块
        self.layers = nn.ModuleList([
            TransformerBlock(d_model, num_heads, d_ff, theta, context_length)
            for _ in range(num_layers)
        ])
        
        # 3. 最终归一化层
        self.ln_final = RMSNorm(d_model)
        
        # 4. 语言模型头:将d_model维度映射到词汇表大小(输出logits)
        self.lm_head = Linear(d_model, vocab_size)

    def forward(self, inputs: torch.Tensor) -> torch.Tensor:
        # 输入:(batch_size, seq_len) → 输出:(batch_size, seq_len, vocab_size)
        # 1. 词嵌入
        x = self.token_embeddings(inputs)  # (batch_size, seq_len, d_model)
        
        # 2. 经过所有Transformer块
        for layer in self.layers:
            x = layer(x)  # 每层输出仍为 (batch_size, seq_len, d_model)
        
        # 3. 最终归一化 + 映射到词汇表
        x = self.ln_final(x)
        logits = self.lm_head(x)  # (batch_size, seq_len, vocab_size)
        
        return logits

三、训练工具实现:train.py

train.py 包含了模型训练所需的核心工具,包括损失函数、优化器、学习率调度、数据批处理、梯度裁剪和模型 checkpoint 管理等。

3.1 损失函数:交叉熵(Cross-Entropy)

语言模型的核心任务是预测下一个token,因此采用交叉熵损失函数,衡量预测分布与真实token的差距。实现中加入了数值稳定技巧(减去最大值),避免指数运算溢出。

def cross_entropy(inputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
    """
    计算批次上的平均交叉熵损失
    输入:
        inputs: (batch_size, ..., vocab_size) → 未归一化的logits
        targets: (batch_size, ...) → 真实token的索引
    输出:
        标量张量 → 批次平均损失
    """
    batch_size = inputs.shape[0]
    # 数值稳定:减去最大值,避免exp(x)溢出
    o_max = torch.max(inputs, dim=-1, keepdim=True).values
    o = inputs - o_max
    # 获取目标token对应的logit
    target_logits = o[torch.arange(batch_size), targets]
    # 计算log(sum(exp(o)))
    logsumexp = torch.log(torch.sum(torch.exp(o), dim=-1))
    # 单个样本损失:-target_logit + logsumexp
    loss = -target_logits + logsumexp
    # 返回批次平均值
    return loss.mean(dim=0)

3.2 优化器:AdamW

AdamW是Adam优化器的改进版,在Adam的基础上分离了权重衰减(Weight Decay)与梯度更新,有效提升模型泛化能力,是当前训练Transformer的主流优化器。

class AdamW(torch.optim.Optimizer):
    def __init__(
        self,
        params,
        lr=1e-3,          # 初始学习率
        betas=(0.9, 0.999),  # 动量参数(一阶矩和二阶矩的指数衰减率)
        eps=1e-8,         # 防止分母为0的微小值
        weight_decay=0.01  # 权重衰减系数
    ):
        # 参数合法性检查
        if lr < 0.0:
            raise ValueError(f"无效学习率: {lr}")
        if eps < 0.0:
            raise ValueError(f"无效epsilon值: {eps}")
        if weight_decay < 0.0:
            raise ValueError(f"无效weight_decay值: {weight_decay}")
        if not 0.0 <= betas[0] < 1.0 or not 0.0 <= betas[1] < 1.0:
            raise ValueError(f"无效betas参数: {betas}")

        defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
        super().__init__(params, defaults)

    def step(self, closure: Callable | None = None):
        # 可选:计算闭包(用于某些特殊场景)
        loss = None if closure is None else closure()
        
        # 遍历参数组(支持不同参数使用不同学习率)
        for group in self.param_groups:
            lr = group["lr"]
            beta1, beta2 = group["betas"]
            eps = group["eps"]
            weight_decay = group["weight_decay"]

            # 遍历参数
            for p in group["params"]:
                if p.grad is None:
                    continue  # 无梯度的参数跳过
                grad = p.grad.data  # 梯度
                state = self.state[p]  # 获取参数的状态字典

                # 初始化状态(首次更新时)
                t = state.get("t", 1)  # 迭代次数(初始为1)
                m = state.get("m", torch.zeros_like(grad))  # 一阶矩估计(动量)
                v = state.get("v", torch.zeros_like(grad))  # 二阶矩估计

                # 更新一阶矩和二阶矩(带偏差校正)
                m = beta1 * m + (1 - beta1) * grad
                v = beta2 * v + (1 - beta2) * grad ** 2

                # 学习率校正(偏差校正)
                lr_t = lr * (1 - beta2 ** t) ** 0.5 / (1 - beta1 ** t)

                # 参数更新:先减去梯度项,再应用权重衰减
                p.data = p.data - lr_t * m / (v ** 0.5 + eps)
                p.data = p.data - lr * weight_decay * p.data  # 权重衰减独立于梯度

                # 更新状态
                state["t"] = t + 1
                state["m"] = m
                state["v"] = v

        return loss

3.3 学习率调度:余弦退火(Cosine Schedule)

学习率调度对Transformer训练至关重要。我们实现了带预热的余弦退火调度:初始阶段线性提升学习率(预热),避免训练初期大学习率导致的不稳定性;随后按余弦曲线衰减至最小值,帮助模型收敛到更优解。

def lr_cosine_schedule(t: int, lr_max: float, lr_min: float, warmup_iters: int, cosine_cycle_iters: int):
    """
    带预热的余弦退火学习率调度
    参数:
        t: 当前迭代次数
        lr_max: 最大学习率(预热结束时达到)
        lr_min: 最小学习率(余弦衰减的下限)
        warmup_iters: 预热迭代次数
        cosine_cycle_iters: 余弦衰减的总迭代次数(含预热)
    返回:
        当前迭代的学习率
    """
    if t < warmup_iters:
        # 预热阶段:线性增长
        lr = t / warmup_iters * lr_max
    elif t < cosine_cycle_iters:
        # 余弦衰减阶段:从lr_max平滑衰减到lr_min
        # 计算当前相位(0到π之间)
        phase = (t - warmup_iters) / (cosine_cycle_iters - warmup_iters) * math.pi
        lr = lr_min + 0.5 * (lr_max - lr_min) * (1 + math.cos(phase))
    else:
        # 衰减结束:固定为lr_min
        lr = lr_min
    return lr

3.4 梯度裁剪(Gradient Clipping)

训练过程中,梯度可能因某些异常样本或大学习率而剧烈波动(梯度爆炸),导致模型不稳定。梯度裁剪通过限制梯度的L2范数,将梯度控制在合理范围内。

def gradient_clipping(parameters: Iterable[torch.nn.Parameter], max_l2_norm: float, eps: float = 1e-6):
    """
    裁剪梯度的L2范数以防止梯度爆炸
    参数:
        parameters: 需裁剪梯度的参数列表
        max_l2_norm: 梯度L2范数的上限
        eps: 防止分母为0的微小值
    """
    # 收集所有有梯度的参数
    grads = [p.grad for p in parameters if p.grad is not None]
    if not grads:
        return  # 无梯度可裁剪
    
    # 计算所有梯度的总L2范数
    l2_norm = 0.0
    for g in grads:
        l2_norm += torch.sum(g ** 2)  # 累加平方和
    l2_norm = torch.sqrt(l2_norm)  # 开平方得L2范数
    
    # 计算裁剪系数(若范数超过上限,则按比例缩小)
    clip_coef = min(1.0, max_l2_norm / (l2_norm + eps))
    
    # 应用裁剪
    for g in grads:
        g *= clip_coef

3.5 数据批处理:get_batch

从文本数据集中采样训练批次,生成输入序列(x)和对应的标签序列(y)——对于语言模型,y 是 x 向右偏移一位的序列(即预测下一个token)。

def get_batch(
    dataset: npt.NDArray, batch_size: int, context_length: int, device: str
) -> tuple[torch.Tensor, torch.Tensor]:
    """
    从数据集中采样批次数据
    参数:
        dataset: 1D numpy数组,存储文本的token ID序列
        batch_size: 批次大小
        context_length: 每个样本的序列长度
        device: 数据存放的设备(如'cpu'或'cuda')
    返回:
        x: (batch_size, context_length) → 输入序列
        y: (batch_size, context_length) → 标签序列(x向右偏移一位)
    """
    # 计算最大起始索引(确保序列不越界)
    max_start = len(dataset) - context_length - 1
    if max_start <= 0:
        raise ValueError("数据集长度小于指定的context_length")

    # 随机采样batch_size个起始索引
    starts = np.random.randint(0, max_start + 1, size=batch_size)

    x_batch = []
    y_batch = []
    for s in starts:
        # 截取序列:[s, s+context_length+1)
        seq = dataset[s : s + context_length + 1]
        x_batch.append(seq[:-1])  # 输入:前context_length个token
        y_batch.append(seq[1:])   # 标签:后context_length个token(偏移一位)

    # 转换为PyTorch张量并移动到指定设备
    x = torch.tensor(x_batch, dtype=torch.long, device=device)
    y = torch.tensor(y_batch, dtype=torch.long, device=device)
    
    return x, y

3.6 模型Checkpoint管理

训练大型模型时,需要定期保存训练状态(模型参数、优化器状态、当前迭代次数),以便中断后恢复训练或后续评估。

def save_checkpoint(
    model: torch.nn.Module,
    optimizer: torch.optim.Optimizer,
    iteration: int,
    out: str | os.PathLike | BinaryIO | IO[bytes],
):
    """保存训练状态到文件"""
    checkpoint = {
        'model_state': model.state_dict(),      # 模型参数
        'optimizer_state': optimizer.state_dict(),  # 优化器状态
        'iteration': iteration,                # 当前迭代次数
    }
    torch.save(checkpoint, out)

def load_checkpoint(
    src: str | os.PathLike | BinaryIO | IO[bytes],
    model: torch.nn.Module,
    optimizer: torch.optim.Optimizer
) -> int:
    """从文件加载训练状态并恢复模型和优化器"""
    checkpoint = torch.load(src)
    model.load_state_dict(checkpoint['model_state'])
    optimizer.load_state_dict(checkpoint['optimizer_state'])
    return checkpoint['iteration']  # 返回保存时的迭代次数

四、总结与扩展

通过本次作业,我们从零实现了一个完整的Transformer语言模型,涵盖了现代LLM的核心组件:

  • 采用预归一化设计的Transformer块,提升训练稳定性
  • 旋转位置编码(RoPE)解决位置信息编码问题,支持长度外推
  • 多头自注意力机制捕捉token间的依赖关系
  • SwiGLU激活函数增强前馈网络的表达能力
  • 配套实现了AdamW优化器、余弦学习率调度等训练工具

另外,adapter.py里的调用就不再一个个放了,考虑到上次有人在评论区里问,我放个例子吧:

def run_transformer_lm(
    vocab_size: int,
    context_length: int,
    d_model: int,
    num_layers: int,
    num_heads: int,
    d_ff: int,
    rope_theta: float,
    weights: dict[str, Tensor],
    in_indices: Int[Tensor, " batch_size sequence_length"],
) -> Float[Tensor, " batch_size sequence_length vocab_size"]:
    """Given the weights of a Transformer language model and input indices,
    return the output of running a forward pass on the input indices.

    This function should use RoPE.

    Args:
        vocab_size (int): The number of unique items in the output vocabulary to be predicted.
        context_length (int): The maximum number of tokens to process at once.
        d_model (int): The dimensionality of the model embeddings and sublayer outputs.
        num_layers (int): The number of Transformer layers to use.
        num_heads (int): Number of heads to use in multi-headed attention. `d_model` must be
            evenly divisible by `num_heads`.
        d_ff (int): Dimensionality of the feed-forward inner layer (section 3.3).
        rope_theta (float): The RoPE $\Theta$ parameter.
        weights (dict[str, Tensor]): 
            State dict of our reference implementation. {num_layers} refers to an
            integer between `0` and `num_layers - 1` (the layer index).
            The keys of this dictionary are:
            - `token_embeddings.weight`
                Token embedding matrix. Shape is (vocab_size, d_model).
            - `layers.{num_layers}.attn.q_proj.weight`
                The query projections for all `num_heads` attention heads.
                Shape is (num_heads * (d_model / num_heads), d_model).
                The rows are ordered by matrices of shape (num_heads, d_k),
                so `attn.q_proj.weight == torch.cat([q_heads.0.weight, ..., q_heads.N.weight], dim=0)`.
            - `layers.{num_layers}.attn.k_proj.weight`
                The key projections for all `num_heads` attention heads.
                Shape is (num_heads * (d_model / num_heads), d_model).
                The rows are ordered by matrices of shape (num_heads, d_k),
                so `attn.k_proj.weight == torch.cat([k_heads.0.weight, ..., k_heads.N.weight], dim=0)`.
            - `layers.{num_layers}.attn.v_proj.weight`
                The value projections for all `num_heads` attention heads.
                Shape is (num_heads * (d_model / num_heads), d_model).
                The rows are ordered by matrices of shape (num_heads, d_v),
                so `attn.v_proj.weight == torch.cat([v_heads.0.weight, ..., v_heads.N.weight], dim=0)`.
            - `layers.{num_layers}.attn.output_proj.weight`
                Weight of the multi-head self-attention output projection
                Shape is ((d_model / num_heads) * num_heads, d_model).
            - `layers.{num_layers}.ln1.weight`
                Weights of affine transform for the first RMSNorm
                applied in the transformer block.
                Shape is (d_model,).
            - `layers.{num_layers}.ffn.w1.weight`
                Weight of the first linear transformation in the FFN.
                Shape is (d_model, d_ff).
            - `layers.{num_layers}.ffn.w2.weight`
                Weight of the second linear transformation in the FFN.
                Shape is (d_ff, d_model).
            - `layers.{num_layers}.ffn.w3.weight`
                Weight of the third linear transformation in the FFN.
                Shape is (d_model, d_ff).
            - `layers.{num_layers}.ln2.weight`
                Weights of affine transform for the second RMSNorm
                applied in the transformer block.
                Shape is (d_model,).
            - `ln_final.weight`
                Weights of affine transform for RMSNorm applied to the output of the final transformer block.
                Shape is (d_model, ).
            - `lm_head.weight`
                Weights of the language model output embedding.
                Shape is (vocab_size, d_model).
        in_indices (Int[Tensor, "batch_size sequence_length"]) Tensor with input indices to run the language model on. Shape is (batch_size, sequence_length), where
            `sequence_length` is at most `context_length`.

    Returns:
        Float[Tensor, "batch_size sequence_length vocab_size"]: Tensor with the predicted unnormalized
        next-word distribution for each token.
    """
    model = TransformerLM(vocab_size,context_length, num_layers,d_model, 
                          num_heads, d_ff, rope_theta).to(device)
    model.load_state_dict(weights)

    return model(in_indices.to(device=device))
斯坦福 CS336 动手大语言模型 Assignment1 BPE Tokenizer & TransformerLM 所有代码更新至 https://github.com/WangYuHang-cmd/CS336/tree/main/assignment1-basics作业文件结构:├── tests/ # 测试文件目录│ ├── adapters.py # 适配器测试│ ├── conftest.py # pytest配置│ ├──.py # 包初始化。 阅读详情

相关推荐

深度学习之Encoder-Only原理与实战

【代码】深度学习之Encoder-Only原理与实战。

yumiwangzi30的博客 265

Stanford CS336 | Assignment 1 - BPE Tokenizer Training 实现

本次任务目标是在 TinyStories 数据集上训练一个字节级 BPE 分词器。如图所示,需要完成函数train_bpe。初始化:以所有可能的字节(0-255)作为初始词汇表;预处理:将文本分割为初始令牌(字节序列);合并:统计所有字节对的频率,合并最频繁的对并更新词汇表,重复此过程直至达到目标词汇量。

Bug_makerACE的博客 2251

深度学习之Seq2Seq原理与实战

【代码】深度学习之Seq2Seq原理与实战。

yumiwangzi30的博客 133

Standford CS336 | Assignment 1 - Implementing the tokenizer

本次作业的核心任务是实现一个Tokenizer类,该类基于训练得到的词汇表(vocab)和合并规则(merges),能够将文本编码为整数 ID 序列,以及将整数 ID 序列解码为文本。同时,需要支持用户提供的特殊符号。编码(encode):将输入的文本字符串转换为 token ID 列表。过程需先进行预分词,再应用 BPE 合并规则。解码(decode):将 token ID 列表转换为文本字符串。对于无效的 token ID,需用 Unicode 替换字符(U+FFFD)处理。支持从文件加载:实现。

Bug_makerACE的博客 1191

深度学习之TF-IDF原理与实战

【代码】深度学习之TF_IDF原理与实战。

yumiwangzi30的博客 187

NVIDIA NCCL 源码解析:从XML拓扑到高性能通信图的构建

本文深入解析了NVIDIA NCCL如何将XML硬件拓扑文件转化为高性能通信图的核心过程。详细阐述了从创建CPU、GPU、NIC等节点,到建立PCIe、NVLink连接,再到计算链路带宽并进行关键排序的完整建图流程。该图是NCCL实现最优通信路径规划、充分发挥NVLink等高速互联性能的基石,直接影响分布式深度学习训练的效率。

convnet3designer的博客 307

Stanford CS336 Language Models from Scratch-Assignment 1 OVerview

前些天计划看Stanford 新开的 CS336,顺便记录下,发到这里,大家一起交流。

nuowenyadelunwen的博客 835

OpenGL计算着色器工作组设置测试

先看看不同的显卡上工作组的数量情况。 glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, data); glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, data + 1); glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, data...

qq_26328385的博客 1484

Stanford CS336 | Assignment 2 - FlashAttention-v2 Pytorch & Triotn实现

算法层面:理解了分块计算和LogSumExp技巧如何将注意力的显存复杂度从O(T²d)降至O(Td),为处理长序列(如8k、16k)提供了可能;工程层面:掌握了Triton框架的核心用法——通过块指针高效访问内存、利用共享内存减少全局内存访问、设计合理的分块策略适配GPU硬件;性能层面:验证了FlashAttention在长序列场景下的显著优势,为Transformer模型的工程落地提供了关键优化手段。扩展方向支持多头注意力的融合计算(当前版本为单头,多头可通过维度拆分实现);

Bug_makerACE的博客 1312

从AMBA到USB:手把手教你评估和选择Synopsys DesignWare里的免费商用IP

本文详细介绍了如何评估和选择Synopsys DesignWare中的免费商用IP,从AMBA到USB等关键模块。通过技术参数核对、验证环境评估和集成注意事项,帮助工程师高效利用这些经过硅验证的商业级IP,提升SoC设计效率。文章还探讨了免费IP与付费IP的边界策略,避免常见陷阱。

weixin_42683394的博客 297

GD32F303串口DMA发送数据避坑指南:为什么你的发送函数会卡住?

本文深入解析GD32F303单片机串口DMA发送数据时常见的卡死问题,提供详细的避坑指南。从DMA传输完成中断时序、发送状态管理到双通道DMA的特别注意事项,全面剖析问题根源并给出解决方案,帮助开发者构建健壮的DMA发送机制。

weixin_27038245的博客 295

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 1: BPE Tokenizer

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 1: BPE Tokenizer

周同学的博客 1383

广工计组实验f2一位全加器

一位全加器实验

weixin_63692154的博客 2847

Stanford CS336 assignment1 | Transformer Language Model Architecture

摘要 本文介绍了Transformer语言模型架构中的基础模块实现方法。首先讲解了环境配置技巧,建议将虚拟环境激活命令写入.bashrc文件实现自动加载。接着详细说明了模型参数的初始化策略:线性层采用截断高斯分布N(0, 2/(din+dout)),嵌入层采用N(0,1)分布,均使用PyTorch的trunc_normal_函数实现。文章分别实现了Linear模块和Embedding模块的代码,包括初始化方法和前向传播逻辑,并提供了相应的测试方法。测试时使用pytest命令验证模块的正确性,测试通过后会有相

cx1的博客 1760

RK3588 平台部署实战:OpenCV + LibTorch + FFmpeg 环境搭建与深度学习推理全流程解析

现在,你已经掌握了在RK3588上搭建OpenCV+LibTorch+FFmpeg环境的完整流程,也完成了深度学习模型的部署测试。不妨从一个具体的应用场景入手(如检测自家宠物的品种),亲手打造属于你的第一个边缘智能应用——当你看到模型在RK3588上精准输出结果时,你会发现:嵌入式深度学习的门槛,其实并没有想象中那么高。这些依赖是编译OpenCV、FFmpeg的基础,安装过程中若提示权限不足,可在命令前加。若能输出预测的类别索引,则说明环境搭建成功,模型推理流程通畅。文件,实现模型加载、图像预处理与推理。

FJN110的博客 535

8.26华为OD机试真题 新系统 - 字符串回文判 (Java/Py/C/C++/Js/Go)

8.26华为OD机试真题 新系统 - 字符串回文判 (Java/Py/C/C++/Js/Go)

算法大师! 1236

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 1: Training Loop Implement

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 1: Training Loop Implement

周同学的博客 1167

2025年7月12日全国青少年信息素养大赛图形化(Scratch)编程小学低年级组复赛真题+答案解析

2025年7月12日全国青少年信息素养大赛图形化(Scratch)编程小学低年级组复赛真题+答案解析

伶俐角少儿编程 711

assignment1-basics:掌握编程基础,提升技能第一步

assignment1-basics:掌握编程基础,提升技能第一步 项目介绍 在计算机科学教育中,基础知识的掌握至关重要。assignment1-basics 是一个开源项目,旨在帮助学习者通过实际编码任务来深化对基础编程概念的理解。该项目作为CS336课程的第一个作业,提供了一个实践的平台,让学生在解决问题的过程中巩固Python编程的基础。 项目技术分析 assignment1-basics ...

gitblog_00657的博客 455

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

cs336 assignment1 作业环境配置

cs336 assignment1 前期环境配置

m0_46464899的博客 3004

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 1: BPE Tokenizer Implement

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 1: BPE Tokenizer Implement

周同学的博客 1690

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 4: Deduplication Implement

斯坦福大学 | CS336 | 从零开始构建语言模型 | Spring 2025 | 笔记 | Assignment 4: Deduplication Implement

周同学的博客 912

深度学习0:数学知识:正态分布1

步骤内容结果1引入e−x2e^{-x^2}e−x2钟形曲线2算它的面积I∫−∞∞e−x2dxI∫−∞∞​e−x2dx3平方变成二重积分I2I^2I24换极坐标简化计算5算出I2πI^2 = \piI2πIπIπ​6代入σ\sigmaσ和μ\muμ做变量替换得到σ2πσ2π​7除以它归一化得到正态分布公式2π2π​不是随便写的,它来自高斯积分∫−∞∞e−x2。

tellmewhoisi的博客 197

图解循环神经网络(RNN)

RNN(循环神经网络)就是“带记忆的网络”:让网络拥有一块“记忆”,在处理当前数据时,能同时参考之前看过的内容;它不一次性看完全部输入,而是像人读书一样,一个字一个字地读,每读一个字就更新一次脑子里的记忆,然后用这份记忆去理解当前这个字。但便签一直在被改写,新信息不断挤占旧信息,传到第 40 步时,“巴黎”的痕迹已经淡得几乎没了。读到“贵”的时候,网络并没有重新去查“好看”这个词——但“好看”的影响已经融在了便签里(那个 1.5)。这三个困难的共同点:答案不在当前这一个字里,而在“前面那串东西”里。

技术管理两手抓,两手都要硬. 366

【原创深度学习项目】基于深度学习的建筑工地防护装备检测系统|安全帽与反光衣检测系统(RT-DETR-L 模型)

本文设计并实现了一套基于RT-DETR-L的建筑工地防护装备检测系统,采用Django+Vue前后端架构,实现对安全帽、反光背心等7类装备及“无PPE”“未戴安全帽”等缺失状态的精准识别。系统支持图片、视频、摄像头实时检测,具备参数可调、数据可视化与统计分析功能,有效解决小目标漏检、误检及管理难问题,助力智慧工地安全管理落地。文末可获取源码。

qq_1445749146的博客 217

神经网络入门指南:从 CNN、RNN 到 Attention

神经网络的学习,本质是“预测—比较—纠错”的循环;CNN 通过局部连接与权重共享理解图像;RNN 通过隐藏状态传递上下文;LSTM 和 GRU 用门控机制管理记忆;Encoder–Decoder 把理解与生成拆成两个阶段;Attention 则让模型在每一步都能主动寻找最相关的信息。到这里,我们已经走完了从普通神经网络到 Attention 的完整路线。下一篇将正式进入 Transformer,看看它如何摆脱 RNN 的顺序计算,又如何让每个 Token 直接找到与自己相关的信息。

爱敲代码的小黄 356
上一篇: Standford CS336 | Assignment 1 - Implementing the tokenizer
下一篇: Stanford CS336 | Assignment 2 - FlashAttention-v2 Pytorch & Triotn实现
ACEEE-1222
博客等级 码龄4年 432粉丝 22原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值