[{"content":"该项目围绕真实桌面场景下的机器人操作展开，使用 LeRobot 的数据与训练流程，结合 pi0.5 策略框架，在 SO-101 机械臂上完成两个不同类型的任务：叠毛巾与清理桌面。项目重点不是单一动作演示，而是验证“采集数据、训练策略、实体执行、观察误差”的完整闭环。\n叠毛巾任务关注柔性物体操作，需要机械臂在接触不确定、布料形变明显的情况下完成抓取、拖拽、对齐与折叠。 清理桌面任务关注杂物场景中的目标定位和连续抓取，需要机械臂将桌面上的分散物体移动到指定区域。 两个任务共同覆盖刚性物体整理与柔性物体操作，能够更全面地检验策略的空间泛化、动作连续性和真实执行稳定性。 真实机械臂任务演示 # 叠毛巾 # 面向柔性物体的桌面操作任务，机械臂需要在布料形变和接触误差存在的情况下完成抓取、牵引和折叠动作。\n清理桌面 # 面向非结构化桌面的整理任务，机械臂根据目标物体位置执行连续抓取与放置，将分散物体移动到指定区域。\n技术要点 # 数据闭环：围绕真实机械臂采集任务数据，关注动作序列、末端执行器状态和视觉观测之间的对齐关系。 策略执行：基于 LeRobot / pi0.5 训练与推理流程，将桌面任务从离线样本推进到实体机械臂执行。 任务泛化：通过清理桌面和叠毛巾两个任务，分别覆盖刚性物体移动和柔性物体操作两类典型挑战。 ","date":"2026/06/01","externalUrl":null,"permalink":"/portfolio/lerobot-so101/","section":"从模型结构到真实系统的项目沉淀。","summary":"基于 LeRobot 与 pi0.5 策略框架，在 SO-101 机械臂上完成叠毛巾与清理桌面两个真实桌面操作任务，验证从数据采集、策略学习到实体执行的闭环。","title":"LeRobot pi0.5 + SO-101：双任务机械臂操作","type":"Embodied AI"},{"content":"RoboChallenge ICRA Competition 2026 的任务围绕零售超市场景展开，要求机器人完成指令理解、环境感知、自主导航和精细操作等综合能力评测。\n我的提交为 No.89757 / gr00t，在 Dexmal WBC Track 中取得第 8 名。该经历重点锻炼了具身智能系统中的任务拆解、动作规划和全身控制问题理解。\n","date":"2026/05/10","externalUrl":null,"permalink":"/competitions/robochallenge-icra-2026/","section":"比赛专项","summary":"官方榜单任务围绕零售超市场景，评测机器人对指令理解、环境感知、自主导航和精细操作的综合能力；我的提交为 No.89757 / gr00t。","title":"RoboChallenge ICRA Competition 2026","type":"competitions"},{"content":" CodeLab-LLaMA2 / 星语 MoE 以“把大模型核心链路跑通”为目标，围绕 LLaMA2 的结构、训练和应用做系统化拆解。\n从零实现 Attention、FFN、RMSNorm、Rotary Embedding 等核心模块。 支持预训练、SFT、LoRA 微调和推理流程，覆盖从结构实现到训练调试的完整链路。 延伸到 RAG、Agent 和 MoE-FFN 替换策略，帮助理解大模型系统如何从理论进入应用。 项目链接：GitHub，在线体验：ModelScope。\n项目详解 # 很高兴，前面的章节已经让我们完整的学习到了如何一步一步构建大模型，如何预训练，如何高效微调。\n这章，打算以最近特别火的MoE架构，来重新设计模型，我打算取名为星语。\n什么是MoE架构？ # MoE 是一种“稀疏激活”的神经网络架构：把模型拆成许多专门的子网络（称为 experts），然后用一个 gating（门/路由）网络根据输入选择其中一小部分专家来处理该输入。这样模型总参数可以非常大，但每次前向只激活少数专家，计算成本/延迟接近传统模型，从而实现“用常量计算成本得到更多参数规模”的目标。\n我们可以看看论文中的架构图：\n可以很明显的看到，就是把原来的MLP（FFN）替换为图中的MoE组件。\n对于MoE组件来说，可以简单的看做是原来大的FFN划分为多个小的并行的FFN。那么这些小的FFN就是所谓的专家。下面拆解一下MoE核心的组件\n专家（experts）：并行的子网络（通常是 FFN／前馈层的重复体）。\n门控/路由（gating）：对输入 x 输出权重或直接选择 top-k 个专家（如 top-1/top-2）。\n稀疏激活：每个 token/样本只经过少数专家（比如 1 或 2 个），保证计算效率。\n辅助损失（auxiliary loss）：常加入负载均衡损失，避免所有请求集中到少数专家造成“拥塞”\n对于这个架构来说是非常简单的。但是为什么要这样设计的，为什么可以设计？\n“为什么能用多个小 FFN（专家）去替换一个大 FFN，还能变得更强、更有效？”\n为什么MoE更加高效？ # “为什么能用多个小 FFN（专家）去替换一个大 FFN，还能变得更强、更有效？”\n原始 Transformer 里的 FFN 是什么 # 在标准 Transformer（如 GPT、BERT）中，每一层都有两部分：\n自注意力（Self-Attention）\n前馈网络（Feed Forward Network, FFN）\nFFN 的形式通常是：\n$ \\text{FFN}(x) = W_2 , \\sigma(W_1 x + b_1) + b_2$\n其中 ($\\sigma$) 是非线性激活（ReLU / GELU）。 这个 FFN 是 对每个 token 独立应用 的，即没有跨 token 的交互，只是逐 token 的变换。\n维度上一般是：\n输入维度 ( $d_\\text{model}$ )\n隐藏维度 ( $d_\\text{ff} = 4d_\\text{model}$ )（比如 4096 → 16384 → 4096）\n所以它的参数主要是 ( $2 \\times d_\\text{model} \\times d_\\text{ff}$ )，这是 Transformer 参数的主要来源之一。\nMoE 是怎么“替换” FFN 的 # MoE 层做的，就是：\n把原来唯一的 FFN 换成 N 个并行的专家（FFN），并用一个 gating 网络 决定每个 token 应该送到哪些专家。\n即：\n$ \\text{MoE}(x) = \\sum_{i=1}^{N} g_i(x) \\cdot \\text{FFN}_i(x)$\n其中：\n( $\\text{FFN}_i$ ) 是第 i 个专家（结构和原 FFN 一样，但参数独立）\n( $g_i(x)$ ) 是门控权重（由一个小网络根据 x 计算，常是 softmax 输出的 top-k 稀疏选择）\n通常只选 top-1 或 top-2 专家，也就是说：\n每个 token 只会经过少量（1～2个）专家；\n其余专家不参与该 token 的计算（稀疏激活）。\n为什么这样做“有效果”（即更强、更省算力） # 参数量大，但每次计算量几乎不变\n假设原 FFN 有 100M 参数；\n用 16 个专家，每个专家参数量也相同 → 总参数 = 1.6B；\n但每个 token 只用其中 1～2 个专家；\n所以推理计算量 ≈ 100M × (1 or 2) / 16 = 原来的 1×～2×，几乎没变。\n→ 实现了“稀疏扩展”：能增加模型容量但不增加计算量。\n每个专家可以“专门化” # 在训练中，gating 会学会根据输入特征（或语义）把不同 token 路由到不同专家。\n结果是：\n不同专家负责不同的任务分布；\n有的专家擅长数学、有的擅长对话、有的擅长编程、有的擅长多语言等。\n这种“自动分工”让模型整体更强，像一个协作团队。\n这其实让模型具备了**条件计算（Conditional Computation）**的能力：\n不同输入触发不同参数子集，而不是“所有参数都做相同的事”。\n保留了 Transformer 的结构优势 # MoE 只是替换 FFN 部分，而注意力部分不变。\n这意味着：\n全局上下文建模（attention）依旧；\nFFN 部分获得了“多专家并行”的灵活性；\n在训练时，MoE 层可以并行分布在不同设备上（称为 专家并行 expert parallelism），非常适合大规模训练。\n在达到相同的训练损失的情况下，MoE架构是花费最少的。这个通过实验可以证明。\n存在问题 # 负载不均衡（Load Imbalance） # 在训练时，gating 网络可能倾向于选择某几个专家，导致：\n某些专家负载过高（被频繁调用）；\n其他专家几乎没被用（梯度更新太少）。\n这会带来：\n训练不稳定；\n专家崩塌（collapse）；\nGPU 之间工作量差异大，效率下降。\n缓解负载不均衡的主要机制是 auxiliary loss（辅助负载平衡损失）\nMoE代码 # 代码地址：Code/MoEBlock.py\nimport torch import torch.nn as nn import torch.nn.functional as F import math from transformers import PretrainedConfig # ============================================================ # 模型配置类 # ============================================================ class ModelConfig(PretrainedConfig): *\u0026#34;\u0026#34;\u0026#34;* * 模型的全局配置参数类，用于统一管理 Transformer + MoE 层的结构参数。* * 继承自 HuggingFace 的 PretrainedConfig，方便保存与加载。* * \u0026#34;\u0026#34;\u0026#34;* * *model_type = \u0026#34;XY-MoE\u0026#34; def __init__(self, dim: int = 1024, # 模型主维度（token embedding 的维度） n_layers: int = 16, # Transformer 层数 n_heads: int = 16, # 多头注意力的头数 n_kv_heads: int = 8, # key/value 头数 vocab_size: int = 6144, # 词表大小 hidden_dim: int = None, # FFN 隐藏层维度（若为 None 会自动计算） multiple_of: int = 64, # FFN 隐藏层对齐倍数 norm_eps: float = 1e-5, # LayerNorm 的 epsilon max_seq_len: int = 256, # 最大序列长度 dropout: float = 0.0, # Dropout 概率 flash_attn: bool = True, # 是否启用 Flash Attention ######################################################## # 下方为 MoE（Mixture of Experts）配置参数 ######################################################## num_experts_per_tok: int = 1, # 每个 token 选择的专家数（top-k） n_routed_experts: int = 4, # MoE 专家总数 n_shared_experts: int = 1, # “共享”专家数（dense FFN，不经过 gating） gate_hidden_size: int = None, # gate 输入维度（若 None 默认等于 dim） aux_loss_alpha: float = 1e-2, # MoE 辅助损失权重，用于均衡专家负载 seq_aux: bool = False, # 辅助损失是否按序列统计 noise_std: float = 1.0, # gating logits 噪声强度 norm_topk_prob: bool = True, # 是否归一化 top-k 概率 capacity_factor: float = 1.25, # 每个专家的 token 容量上限因子 **kwargs, ): *\u0026#34;\u0026#34;\u0026#34;* * 初始化模型参数。这里主要保存为实例属性，供下游模块使用。* * \u0026#34;\u0026#34;\u0026#34;* * *# Transformer 主体参数 self.dim = dim self.n_layers = n_layers self.n_heads = n_heads self.n_kv_heads = n_kv_heads self.vocab_size = vocab_size self.hidden_dim = hidden_dim self.multiple_of = multiple_of self.norm_eps = norm_eps self.max_seq_len = max_seq_len self.dropout = dropout self.flash_attn = flash_attn # MoE 参数 self.num_experts_per_tok = int(num_experts_per_tok) self.n_routed_experts = int(n_routed_experts) self.n_shared_experts = int(n_shared_experts) self.gate_hidden_size = gate_hidden_size if gate_hidden_size is not None else dim self.aux_loss_alpha = aux_loss_alpha self.seq_aux = seq_aux self.noise_std = noise_std if noise_std is not None else 0.0 self.norm_topk_prob = bool(norm_topk_prob) self.capacity_factor = float(capacity_factor) super().__init__(**kwargs) # ============================================================ # SwiGLU 前馈网络（Feed-Forward Network） # ============================================================ class FFN(nn.Module): *\u0026#34;\u0026#34;\u0026#34;* * SwiGLU 变体的前馈网络层。* * 结构：* * x -\u0026gt; fc_gate -\u0026gt; SiLU() 激活* * x -\u0026gt; fc_up -\u0026gt; 线性映射* * 逐元素相乘 -\u0026gt; fc_down -\u0026gt; Dropout* * 输出维度与输入相同（残差连接用）。* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, dim: int, hidden_dim: int = None, multiple_of: int = 256, dropout: float = 0.0): super().__init__() # 若未显式指定 hidden_dim，则自动计算（与 LLaMA 一致） if hidden_dim is None: hidden_dim = 4 * dim hidden_dim = int(2 * hidden_dim / 3) # 约 2.67x dim hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) # 线性层定义 self.fc_gate = nn.Linear(dim, hidden_dim, bias=False) # 激活分支 self.fc_up = nn.Linear(dim, hidden_dim, bias=False) # 门控分支 self.fc_down = nn.Linear(hidden_dim, dim, bias=False) # 输出投影 self.dropout = nn.Dropout(dropout) def forward(self, x: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * x: [batch, seq_len, dim]* * return: [batch, seq_len, dim]* * \u0026#34;\u0026#34;\u0026#34;* * *gated = F.silu(self.fc_gate(x)) # 激活通道 up = self.fc_up(x) # 线性通道 hidden = gated * up # SwiGLU 核心操作 out = self.fc_down(hidden) # 映射回原维度 out = self.dropout(out) return out # ============================================================ # MoE 的 Gate 模块 # ============================================================ class MoEGate(nn.Module): *\u0026#34;\u0026#34;\u0026#34;* * Gating 网络：负责为每个 token 计算其对应的专家分配概率。* * 输入：* * x: [B, L, D] （token embedding）* * 输出：* * topk_idx: [B*L, K] 每个 token 对应的 top-k 专家索引* * topk_weight: [B*L, K] 对应的 top-k 概率权重* * aux_loss: 标量，用于负载均衡的辅助损失* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, config: ModelConfig): super().__init__() self.config = config # ---------------- 基本参数 ---------------- self.top_k = config.num_experts_per_tok # 每个 token 选择几个专家 self.n_experts = config.n_routed_experts # 专家总数 self.gating_dim = config.gate_hidden_size # gate 的输入维度 self.alpha = config.aux_loss_alpha # 辅助损失权重 self.seq_aux = config.seq_aux self.noise_std = config.noise_std self.norm_topk_prob = config.norm_topk_prob # ---------------- 权重定义 ---------------- # 线性层：输入 gating_dim → 输出 n_experts # 输出即每个专家的 logits（未归一化分数） self.linear = nn.Linear(self.gating_dim, self.n_experts, bias=False) # 初始化参数 self.reset_parameters() def reset_parameters(self): *\u0026#34;\u0026#34;\u0026#34;使用 Kaiming 初始化线性层权重。\u0026#34;\u0026#34;\u0026#34;* * *nn.init.kaiming_uniform_(self.linear.weight, a=math.sqrt(5)) def forward(self, x: torch.Tensor): *\u0026#34;\u0026#34;\u0026#34;* * Args:* * x: [B, L, D] token 表示* * Returns:* * topk_idx, topk_weight, aux_loss* * \u0026#34;\u0026#34;\u0026#34;* * *B, L, D = x.shape # 若输入维度与 gate_hidden_size 不一致，可以添加线性投影 if D != self.gating_dim: # 临时线性投影（非参数化版，为演示方便） proj = nn.Linear(D, self.gating_dim, bias=False).to(x.device) nn.init.kaiming_uniform_(proj.weight, a=math.sqrt(5)) x_flat = proj(x.view(-1, D)) # [B*L, gating_dim] else: x_flat = x.view(-1, D) # 线性层得到 logits logits = self.linear(x_flat) # [B*L, n_experts] # 训练时加噪声（Switch Transformer trick） if self.training and self.noise_std \u0026gt; 0: logits = logits + torch.randn_like(logits) * self.noise_std # softmax 得到每个专家的选择概率 scores = F.softmax(logits, dim=-1) # [B*L, n_experts] # 取 top-k 专家 topk_vals, topk_idx = torch.topk(scores, k=self.top_k, dim=-1, sorted=False) # [B*L, K] # 对 top-k 权重归一化（保持总和=1） if self.top_k \u0026gt; 1 and self.norm_topk_prob: denom = topk_vals.sum(dim=-1, keepdim=True) + 1e-20 topk_vals = topk_vals / denom # ---------------- 辅助损失（aux loss） ---------------- # 用于防止部分专家被频繁选中，保持负载均衡 if self.training and (self.alpha is not None) and (self.alpha \u0026gt; 0): # 各专家平均被选中的概率 expert_prob = scores.mean(dim=0) # 每个专家的平均 softmax 概率 # 每个专家实际被选中的比例 flat_topk_idx = topk_idx.view(-1) mask = F.one_hot(flat_topk_idx, num_classes=self.n_experts).float() expert_usage = mask.mean(dim=0) # 损失：鼓励 expert_prob 与 expert_usage 均匀 aux_loss = self.alpha * torch.sum(expert_prob * expert_usage * float(self.n_experts)) else: aux_loss = torch.tensor(0.0, device=x.device, dtype=x.dtype) return topk_idx, topk_vals, aux_loss # ============================================================ # MoE FeedForward（核心层） # ============================================================ class MOEFeedForward(nn.Module): *\u0026#34;\u0026#34;\u0026#34;* * MoE 前馈层，将多个专家的 FFN 与 Gate 结合。* * 流程：* * 1. gate 计算每个 token 的 top-k 专家及权重；* * 2. 将 token 分派到各自专家（dispatch）；* * 3. 专家独立计算；* * 4. 按权重加权求和回原序列；* * 5. 若存在 shared expert，则叠加输出。* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, config: ModelConfig): super().__init__() self.config = config n_experts = config.n_routed_experts # 1创建专家列表（每个专家一个 FFN） self.experts = nn.ModuleList([ FFN(config.dim, config.hidden_dim, config.multiple_of, config.dropout) for _ in range(n_experts) ]) # 2创建 gate 模块 self.gate = MoEGate(config) # 3可选的 shared experts（dense 路径） if getattr(config, \u0026#39;n_shared_experts\u0026#39;, 0) \u0026gt; 0: self.shared_experts = nn.ModuleList([ FFN(config.dim, config.hidden_dim, config.multiple_of, config.dropout) for _ in range(config.n_shared_experts) ]) else: self.shared_experts = None # 4capacity 系数 self.capacity_factor = getattr(config, \u0026#39;capacity_factor\u0026#39;, 1.25) self.aux_loss = None # 存储 aux loss def forward(self, x: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * Args:* * x: [B, L, D]* * Returns:* * y_out: [B, L, D]* * \u0026#34;\u0026#34;\u0026#34;* * *B, L, D = x.shape T0 = B * L # flatten token 数量 K = self.config.num_experts_per_tok # ========== Step 1: gating ========== topk_idx, topk_weight, aux_loss = self.gate(x) self.aux_loss = aux_loss # 暴露给外部使用（训练时可累加） # 展平成 (T0, D) x_flat = x.view(-1, D) # ========== Step 2: 复制 token ========== # 每个 token 对应 K 条路径（用于 top-k 专家） x_expanded = x_flat.repeat_interleave(K, dim=0) # (T0*K, D) topk_idx_flat = topk_idx.view(-1) # (T0*K,) topk_weight_flat = topk_weight.view(-1) # (T0*K,) device = x.device # ========== Step 3: capacity 控制 ========== # 每个专家最多处理的 token 数 tokens_per_expert = float(T0) / float(self.config.n_routed_experts) capacity = max(1, int(math.ceil(self.capacity_factor * tokens_per_expert))) # 记录每个专家当前分配的 token 数 counts = torch.zeros(self.config.n_routed_experts, dtype=torch.long, device=device) keep_mask = torch.zeros_like(topk_idx_flat, dtype=torch.bool, device=device) # 简单实现：顺序遍历 token，若专家未满则保留 for i in range(topk_idx_flat.size(0)): e = int(topk_idx_flat[i].item()) if counts[e] \u0026lt; capacity: keep_mask[i] = True counts[e] += 1 else: keep_mask[i] = False # 超出容量的 token 被丢弃 # 仅保留有效 token selected_idx = topk_idx_flat[keep_mask] selected_x = x_expanded[keep_mask] selected_weight = topk_weight_flat[keep_mask] # ========== Step 4: dispatch 到专家 ========== y_expanded = torch.zeros_like(x_expanded, device=device) keep_indices = torch.nonzero(keep_mask, as_tuple=False).view(-1) # 对应位置 # 按专家分组计算（易读实现） for expert_id in range(self.config.n_routed_experts): mask_expert = (selected_idx == expert_id) if mask_expert.any(): expert_in = selected_x[mask_expert] # 输入 token expert_out = self.experts[expert_id](expert_in) # 专家输出 expert_positions = keep_indices[mask_expert] # 写回位置 y_expanded[expert_positions] = expert_out # ========== Step 5: 合并 top-k 输出 ========== # 将 K 路专家输出按权重加权求和 y_expanded_view = y_expanded.view(T0, K, D) topk_weight_view = topk_weight.view(T0, K, 1) y_combined = torch.sum(y_expanded_view * topk_weight_view.to(y_expanded_view.dtype), dim=1) y_out = y_combined.view(B, L, D) # ========== Step 6: 加上 shared experts ========== if self.shared_experts is not None: for expert in self.shared_experts: y_out = y_out + expert(x) return y_out if __name__ == \u0026#34;__main__\u0026#34;: cfg = ModelConfig(dim=64, n_routed_experts=4, num_experts_per_tok=2) moe = MOEFeedForward(cfg) x = torch.randn(2, 8, 64) # batch=2, seq=8 y = moe(x) print(\u0026#34;输入形状:\u0026#34;, x.shape) print(\u0026#34;输出形状:\u0026#34;, y.shape) print(\u0026#34;辅助损失:\u0026#34;, moe.aux_loss.item()) 星语-MoE大模型 # 下面就到了我们自研的星语大模型了，我们将所有模块代码都整合到一个文件里面。方便我们优化。\n预训练数据集 # 高质量的数据集对模型表现来说至关重要。这里找到一个作者开源的开源的高质量数据集。\nhttps://www.modelscope.cn/datasets/gongjy/minimind_dataset/files\n微调数据集 # 预训练效果 # 微调效果 # ","date":"2026/04/15","externalUrl":null,"permalink":"/portfolio/xingyu-moe/","section":"从模型结构到真实系统的项目沉淀。","summary":"围绕 LLaMA2 内部原理与工程实现，系统拆解架构、组件、预训练、SFT、LoRA、推理与 RAG/Agent，形成从理论到应用的大模型学习与实现闭环。","title":"CodeLab-LLaMA2 / 星语 MoE：大模型训练实践","type":"Large Language Model"},{"content":"星语 Vision 以图像-文本联合推理为目标，验证从视觉编码、token 融合到文本生成的多模态系统链路。\n基于 CLIP 视觉编码器提取图像表征。 实现视觉 token 替换策略，将图像信息融入文本 Transformer 模型。 完成端到端图像-文本推理验证，并支持预训练、SFT 与 LoRA 微调流程。 在线体验：ModelScope。\n","date":"2026/05/01","externalUrl":null,"permalink":"/portfolio/xingyu-vision/","section":"从模型结构到真实系统的项目沉淀。","summary":"基于 CLIP 视觉编码器和文本 Transformer 的图像-文本联合推理系统，完成多模态最小可验证闭环。","title":"星语 Vision：自研多模态大模型","type":"Multimodal LLM"},{"content":"作为队长负责方案拆解、模型设计与队伍协作，完成从问题抽象、算法实现到论文组织的完整流程。\n该经历强化了对复杂问题的建模能力，也训练了在有限时间内组织实验、验证结果和表达方案的能力。\n","date":"2024/09/01","externalUrl":null,"permalink":"/competitions/graduate-math-modeling-2024/","section":"比赛专项","summary":"负责方案拆解、模型设计与队伍协作，完成从问题抽象、算法实现到论文组织的完整流程。","title":"第 21 届中国研究生数学建模竞赛","type":"competitions"},{"content":"该项目围绕 VLA 模型进行源码级理解和最小复现，目标是从“调用模型”深入到“实现模型”。\n系统拆解视觉编码、语言建模与动作预测模块，梳理多模态 token 组织与特征流动。 手动复现 attention、位置编码和多模态融合等关键组件。 跟踪 forward / loss / 推理流程，验证视觉-语言-动作建模的数据流。 项目链接：GitHub。\n","date":"2026/05/20","externalUrl":null,"permalink":"/portfolio/xy-pi0/","section":"从模型结构到真实系统的项目沉淀。","summary":"围绕 pi0 源码构建最小可运行 VLA 系统，拆解视觉编码、语言建模和动作预测模块。","title":"星语 pi0：VLA 模型源码拆解与最小复现","type":"Vision-Language-Action"},{"content":"CAAI 智新杯围绕人工智能创新应用展开，重点关注算法方案、实验验证和结果表达。\n比赛过程中主要参与方案设计与实验验证，关注模型效果与工程可行性之间的平衡。\n","date":"2024/08/01","externalUrl":null,"permalink":"/competitions/caai-zhixin-cup-2024/","section":"比赛专项","summary":"围绕 AI 算法应用进行方案设计与实验验证，强调模型效果、工程可行性和结果表达。","title":"CAAI 智新杯","type":"competitions"},{"content":"NxV2Net 是 V2Net 裂缝分割工作的科研代码仓库，围绕复杂真实场景中的裂缝分割鲁棒性展开。\n提出嵌套多尺度网络结构，增强裂缝细粒度几何特征表达。 通过 MCFA 融合注意力和多尺度特征学习提升复杂场景泛化。 README 中展示了 V2Net 动机、结构贡献、SUES-CRACK 数据集和多数据集结果。 项目链接：GitHub。\n","date":"2026/01/10","externalUrl":null,"permalink":"/portfolio/nxv2net/","section":"从模型结构到真实系统的项目沉淀。","summary":"面向复杂真实场景的鲁棒裂缝分割开源项目，围绕嵌套多尺度结构、MCFA 融合注意力和 SUES-CRACK 数据集提升泛化能力。","title":"NxV2Net：裂缝分割科研代码","type":"Semantic Segmentation"},{"content":"该比赛结合机器人任务场景进行感知、决策与系统方案验证。\n项目经历强调算法方案与现场任务约束之间的配合，也帮助积累机器人系统调试经验。\n","date":"2024/07/01","externalUrl":null,"permalink":"/competitions/robot-ai-competition-2024/","section":"比赛专项","summary":"结合机器人任务场景进行感知、决策与系统方案验证。","title":"机器人与人工智能竞赛","type":"competitions"},{"content":"HAFNet 是轻量化语义分割方向的科研代码仓库，核心是通过层级功能划分提升复杂场景下的分割效率与精度。\n浅层强化边界与纹理，中层聚合结构和轮廓，深层增强语义抽象。 在遥感、道路、裂缝等多场景数据集上进行评估。 与论文条目形成互链，便于读者从成果页跳转到代码页。 项目链接：GitHub。\n","date":"2026/02/10","externalUrl":null,"permalink":"/portfolio/hafnet/","section":"从模型结构到真实系统的项目沉淀。","summary":"轻量级多层级可定制语义分割网络，围绕图像语义理解中的边界、结构与高层语义协同建模。","title":"HAFNet：轻量化语义分割网络","type":"Lightweight Segmentation"},{"content":"作为队长负责项目推进与方案整合，训练从工程需求到实现验证的完整闭环。\n该经历更偏工程实践，重点在于系统设计、团队协作和现场调试。\n","date":"2023/06/01","externalUrl":null,"permalink":"/competitions/jiangxi-engineering-training-2023/","section":"比赛专项","summary":"负责项目推进与方案整合，训练从工程需求到实现验证的完整闭环。","title":"江西省工程训练竞赛","type":"competitions"},{"content":"参与机器人任务流程设计、调试与现场验证。\n该经历帮助理解机器人系统中的控制、任务执行和现场稳定性问题。\n","date":"2023/05/01","externalUrl":null,"permalink":"/competitions/robot-competition-2023/","section":"比赛专项","summary":"参与机器人任务流程设计、调试与现场验证。","title":"机器人大赛","type":"competitions"},{"content":"该项目从 Kaggle 树叶图像分类任务出发，整理成可复用的图像分类训练模板。\n支持替换主流网络、自定义数据集、训练日志监控和推理可视化。 以 176 类、18k+ 训练样本的细粒度树叶分类作为完整示例。 结合 Label Smoothing、Focal Loss 以及 AdamW + CosineAnnealing 策略训练。 项目链接：GitHub。\n","date":"2025/02/15","externalUrl":null,"permalink":"/portfolio/kaggle-leaf-classification/","section":"从模型结构到真实系统的项目沉淀。","summary":"面向研究和工程复用的 PyTorch 图像分类训练框架，支持替换主流网络、自定义数据集、SwanLab 日志监控和推理可视化，并以树叶分类作为完整示例。","title":"Classification：通用图像分类训练模板","type":"Competition"},{"content":"该任务是 176 类、18k+ 训练样本的细粒度图像分类项目。\n模型训练围绕 ResNet50、Deformable Convolution、ECA 注意力、Label Smoothing、Focal Loss 与 AdamW + CosineAnnealing 策略展开，最终取得 Top 10% / 98.27% Accuracy。\n","date":"2025/02/01","externalUrl":null,"permalink":"/competitions/kaggle-leaf-competition/","section":"比赛专项","summary":"基于 ResNet50 优化细粒度特征表达，结合 Label Smoothing、Focal Loss 与 AdamW + CosineAnnealing 策略。","title":"Kaggle 树叶图像分类","type":"competitions"},{"content":"该项目面向细胞核形态复杂、尺度差异明显的医学图像分割任务。\n在 U-Net 基础上引入 Deformable Convolution 和密集跳跃连接。 结合复合损失函数与强数据增强，提升核形态和尺度建模能力。 比赛 IoU 达到 57.21%，最终排名前 5%。 ","date":"2025/01/15","externalUrl":null,"permalink":"/portfolio/data-science-bowl-nuclei-segmentation/","section":"从模型结构到真实系统的项目沉淀。","summary":"改进 U-Net 进行细胞核实例分割，结合 Deformable Convolution、密集跳跃连接和强数据增强，最终排名前 5%。","title":"2018 Data Science Bowl：细胞核分割","type":"Competition"},{"content":"该项目面向细胞核形态复杂、尺度差异明显的医学图像分割任务。\n方法在 U-Net 基础上引入 Deformable Convolution、密集跳跃连接与复合损失，并配合强数据增强提升核形态和尺度建模能力，最终取得 Top 5% / 57.21% IoU。\n","date":"2025/01/01","externalUrl":null,"permalink":"/competitions/data-science-bowl-2018/","section":"比赛专项","summary":"改进 U-Net，引入 Deformable Convolution、密集跳跃连接与复合损失，提升核形态和尺度建模能力。","title":"2018 Data Science Bowl","type":"competitions"},{"content":"围绕应用场景进行项目立项、方案规划与阶段性验证。\n该经历强调项目管理、需求拆解和原型验证能力。\n","date":"2024/06/01","externalUrl":null,"permalink":"/competitions/shanghai-innovation-training-2024/","section":"比赛专项","summary":"围绕应用场景进行项目立项、方案规划与阶段性验证。","title":"上海市大学生创新创业训练计划","type":"competitions"},{"content":"WebSocket_Test 是一个围绕即时通信交互展开的 Web 工程项目，重点验证实时连接、消息推送和会话区分。\n群聊场景关注多用户消息广播和在线状态。 私聊场景关注点对点消息路由和基础会话隔离。 适合作为理解 WebSocket 通信模型和实时应用交互的入门项目。 项目链接：GitHub。\n","date":"2024/04/01","externalUrl":null,"permalink":"/portfolio/websocket-test/","section":"从模型结构到真实系统的项目沉淀。","summary":"基于 WebSocket 通信实现群聊与私聊功能，聚焦实时消息传递、会话区分和基础即时通信交互。","title":"WebSocket_Test：群聊与私聊实时通信","type":"Web Engineering"},{"content":"school_blog 是一个校园博客系统，围绕前后端分离、内容发布和基础用户交互展开。\n后端基于 Spring Boot，结合 MySQL 与 Redis 处理数据存储和缓存。 前端基于 Vue，覆盖 PC 网页端和移动网页端。 项目接入云服务器与对象存储，适合展示从开发到部署的完整 Web 项目经验。 项目链接：GitHub。\n","date":"2024/05/01","externalUrl":null,"permalink":"/portfolio/school-blog/","section":"从模型结构到真实系统的项目沉淀。","summary":"基于 Spring Boot、Vue、MySQL 与 Redis 的校园博客系统，包含 PC 网页端和移动网页端，并接入云服务器与对象存储。","title":"school_blog：前后端分离校园博客","type":"Full-stack Web"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/cv/","section":"Tags","summary":"","title":"CV","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/vision-transformer/","section":"Tags","summary":"","title":"Vision Transformer","type":"tags"},{"content":" 下面详细分析一下ViT，这个开启了cv的新时代。在cnn处理不好的地方，在vit却是可以很好的处理。\n论文的提出 # 直接使用ViT在大规模数据集中预训练的基础上，去小数据集上做微调，效果会更好。\n这个最重要的是提出了，怎么把一个图片变为一个一个token，也就是我们所说的打成patch\n像以前的工作是其实是有类似的工作了，但是他们是怎么处理这种这种序列长度的呢？一个是cnn抽特征，降低维度。一个是通过轴注意力，将HxW变成单独顺序做H和W。\n论文自己是怎么做的呢？\n就是将图像划分网格（打成一个一个patch）\n另外一个特点就是强调了将任务变为有监督训练。\n然后就是相对训练便宜。\n核心点 # 大规模预训练，再微调，效果好（原先cnn有两个先验知识，相邻特征相似性，平移不变性，所以说，在大规模训练的时候，就是学习这些先验知识）\n切分patch\n训练成本相对较低\n模型架构 # 流程 # 在 Vision Transformer \\(ViT\\) 中，首先将输入图像划分为固定大小的 patch（如 16×16）。每个 patch 被展平后输入到一个线性投射层（Linear Projection），映射为一个固定维度的特征向量，从而得到一系列 patch 的特征表示。\n随后，为了让模型能够识别这些 patch 的空间顺序信息，在每个 patch 的特征向量上加上一个可学习的位置编码（Positional Embedding）。此外，在序列的最前面会插入一个特殊的 \\[CLS\\]（classification）标记，用于在后续的自注意力（Self-Attention）过程中汇聚整张图像的全局信息。\n接着，这个带有位置信息的序列会被输入到标准的 Transformer Encoder 中进行特征提取。Transformer 的多头自注意力机制能够在不同 patch 之间进行全局信息交互，使 \\[CLS\\] 标记逐步融合来自整张图像的特征。\n最后，通过一个多层感知机（MLP）作用在 \\[CLS\\] 向量上，对其进行分类预测，从而输出图像所属的类别。\n代码分析 # 输入图像 → Patch Embedding → 加上 cls token → 加上位置编码 →\n多层 Transformer Encoder → 提取 cls 向量 → MLP Head 分类\nPatch Embedding操作是通过一个卷积来实现 # 加上 cls token # x 被 reshape 成 (batch, num_patches, hidden_dim)。\n加上一个 \\[CLS\\] token（分类占位符），代表整个图像的全局语义信息。\n拼接后序列长度变成 num_patches + 1。\n加上位置编码 # 多层 Transformer Encoder # 提取 cls 向量 # 里面有一个点很关键哦？cls = self.param\\('cls', nn.initializers.zeros, \\(1, 1, c\\))一个和隐藏层相等大小的可学习矩阵\nMLP Head 分类 # 为什么？cls或者是gap？ # 两者均可！！！！\n为什么？做实验结果，效果差不多。\ngap操作就是做一个全局的mean（求平均的操作）\n效果分析 # 如何做Loss # 笑死，万年不变的交叉损失！！！\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/vit/","section":"日常记录、技术札记与转载收藏。","summary":"下面详细分析一下ViT，这个开启了cv的新时代。在cnn处理不好的地方，在vit却是可以很好的处理。","title":"ViT","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/vit/","section":"Tags","summary":"","title":"ViT","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/categories/%E5%8D%9A%E5%AE%A2/","section":"Categories","summary":"","title":"博客","type":"categories"},{"content":" 背景引出 # 提出一个层级（多尺度）且不依赖位置编码的 Transformer 编码器（MiT），可以直接输出多尺度特征（1/4、1/8、1/16、1/32），避免了 ViT 在不同测试分辨率下必须插值位置编码带来的性能下降。\n设计轻量的 All-MLP 解码器：用简单的线性层（MLP）统一通道、上采样并拼接多尺度特征后再用 MLP 预测像素分类，避免了复杂、计算昂贵的解码器结构。\nMix-FFN（在 FFN 中加入 3×3 conv）用来提供位置信息，从而可以去掉显式的位置编码（PE），提高对不同测试分辨率的鲁棒性。\n提出若干工程上的高效做法：overlapped patch merging（重叠补丁合并）保持局部连续性，sequence-reduction（按阶段不同的降采样比）降低自注意力复杂度。论文在 ADE20K、Cityscapes、COCO-Stuff 上做了大量实验证明性能与效率\n模型架构 # 经过overlapped patch merging一个很重要的设计，原始的vit是直接切分patch，但是overlapped patch merging会通过卷积操作，让相邻的特征像素会重合部分，保证了特征的连续性质。同时也控制着特征大小的一步一步缩小，但是通道数放大，相当于是seqlen会进一步的放大。这个和卷积是一样的因为这一步就是卷积实现的。Mix-FFN：在 FFN 里插入 3×3（depthwise）卷积以获得位置信息，从而去掉固定 PE，在测试分辨率与训练分辨率不同的情况下更稳健。最后再将上面4层的特征都上采样到1/4再拼接在一起。\n流程细节 # overlapped patch merging # 本质上就是将步长变短了小于卷积核的大小。\nEfficient self-attention # 这个就是所谓的创新，直接用卷积下采样，就是框架图中的，特征图大小根据stride来确定的，通道数不变，通过控制步伐，这个操作就是卷积实现的。\nMix-FFN # 很直接，很粗暴，假设我们把 Transformer 的 token map 重新恢复成特征图 \\(H×W\\)， 那么每个位置经过 3×3 卷积后， 它的输出值会融合自己 + 上下左右 8 个相邻 patch 的信息：\n+\u0026mdash;+\u0026mdash;+\u0026mdash;+\n| a | b | c |\n+\u0026mdash;+\u0026mdash;+\u0026mdash;+\n| d | e | f | ← 卷积核中心在 e\n+\u0026mdash;+\u0026mdash;+\u0026mdash;+\n| g | h | i |\n+\u0026mdash;+\u0026mdash;+\u0026mdash;+\n但是这个东西真的会比位置编码好吗？其实理论上来说可能会更好，因为二维的空间特征，需要包括上下左右patch的位置，传统的位置编码基本相当于是一个线性的位置，而不是一个二维平面的。这个可以进一步有机会可以做做实验去验证。\n验证可以！！！\nDecode # 直接4个mlp，然后统一用双线性插值直接插，统一到1/4的特征大小，然后直接4个torch.cat\\(\\[_c4, _c3, _c2, _c1\\], dim=1\\)拼接在一起就刚好恢复了H*W再通过一个线性融合\n最后再通过一个卷积进行就行：\n实验效果 # 很强了，这个！！！！\n怎么做loss # 依然万年不变的交叉损失，因为本质是就是对每个像素点进行分类。\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/segformer/","section":"日常记录、技术札记与转载收藏。","summary":"提出一个层级（多尺度）且不依赖位置编码的 Transformer 编码器（MiT），可以直接输出多尺度特征（1/4、1/8、1/16、1/32），避免了 ViT 在不同测试分辨率下必须插值位置编码带来的性能下降。","title":"SegFormer","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/segformer/","section":"Tags","summary":"","title":"SegFormer","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/transformer/","section":"Tags","summary":"","title":"Transformer","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/%E8%AF%AD%E4%B9%89%E5%88%86%E5%89%B2/","section":"Tags","summary":"","title":"语义分割","type":"tags"},{"content":" 论文提出 # 核心 # Patch 分割和线性嵌入 \\(Patch Partition + Linear Embedding\\)\n输入图像首先被切割为 $4\\times4$（或其他）的小块（patches），然后通过一个线性层映射为一个维度为 C 的特征向量。\n例如论文中所述，“每个 patch 是 4×4×3=48维，然后投影为维度 C”。 CVF开放获取\n分层特征 \\(Hierarchical Feature Maps\\)\n与 ViT 只产生一个固定尺度的特征图不同，Swin 在不同阶段（stage）产生不同分辨率的特征图，类似 CNN 中的特征金字塔。\n在每个合并 \\(Patch Merging\\) 层，将邻近 2×2 的 tokens 进行合并，从而空间维度减半、通道维度增加。\n这种机制使得网络在浅层获取高分辨率细节，在深层获取大感受野特征，适合检测、分割任务。\n窗口内自注意力 + 窗口位移 \\(Window‑MSA \\\u0026 Shifted Window‑MSA\\)\n在某一层中，Swin 将特征图划分为不重叠的 M×M 窗口，在每个窗口内部执行多头自注意力 \\(W‑MSA\\)，从而复杂度从全局 O\\(N2\\)O\\(N^2\\)O\\(N2\\) 降至 O\\(NM2\\)O\\(NM^2\\)O\\(NM2\\)，当 M 固定时近似线性于 N（patch 数量）。\n然而，纯局部窗口会限制跨窗口的信息交互。于是 SWin 交替使用“普通窗口分区”与“窗口位移分区 \\(shifted windows\\)”的块：在第二个块中，窗口整体向右下（比如 ⌊M/2⌋\\lfloor M/2 \\rfloor⌊M/2⌋）位移，从而新的窗口跨越了前一个块中的窗口边界，促进跨窗口依赖。\n例如论文中提到：通过位移窗口，Swin‐T 在 ImageNet 上比未位移版本提升 ~1.1% top‑1 精度；在 COCO 检测上提升 +2.8 box AP。\n相对位置偏差 \\(Relative Position Bias\\)\n在 self‑attention 中引入相对位置偏差 \\(relative position bias\\) 来反映 patch 间的空间关系，更适合视觉任务。论文中指出加入该机制效果明显 模型架构 # 流程 # 输入图像 → Patch Partition → Linear Embedding → \\[Stage 1 → Stage 2 → Stage 3 → Stage 4\\] → Patch Merging \\(在各 stage 之间\\) → 最终输出（分类或分割 head）\n聚集代码 # Patch partition # 就是一个打成4*4patch\n输入：x → [B, 3, 224, 224]\npatch_size = 4\n输出：[B, 96, 56, 56]（因为 224 / 4 = 56）\n同时也做了Linear Embedding的操作。\nLinear Embedding # → [B, 56*56, 96] = [B, 3136, 96]\n🔹 这一步就完成了 Patch Partition： 相当于把图片划分成 56×56 = 3136 个小 patch，每个 patch 有 96 维嵌入特征。 （也就是 “把 2D 图像 → 1D patch token 序列”）\nSwin Transformer block # 通过窗口的形式去做注意力\nSwin 把整张特征图划分成 不重叠窗口（7×7），在每个窗口内单独做自注意力， 因此复杂度变成 O(M^2 × N)，其中：\nM = 每个窗口 token 数（7×7 = 49）\nN = 窗口数量（比如 56×56/49 ≈ 64）\n计算量从平方级降到线性级。\n**1\u0026ndash;》**原先我们不是把图打成44吗？相当于是224224的图像划分为很多个4*4的小图像，然后行列都是56个小图像。这个时候，相当于是行，列都有56个token。最终这个图像有56×56 = 3136 个 token\n1如果是 SW-MSA，则 shift # 这个就是做不同窗口之间做计算，加强不同patch之间的特征交互。也就是框架图里面这个。\n先做一次窗口没有移动的注意力，然后做一个窗口移动了的注意力，这样就是为什么在模型架构里面都需要*2都是偶数\nx 的 shape 一般是 [B, H, W, C]\nshift_size = window_size // 2 = 7 // 2 = 3\ntorch.roll 会对张量进行循环平移（cyclic shift）\n也就是说：\n整个特征图沿着高度方向（dim=1）和平行方向（dim=2）各平移 3 个像素，超出部分循环到另一侧。\n2窗口划分 # 这个设计的为77的窗口大小。其实不一定为77因为这个操作本质上是为了减少计算量，那么到底是怎么减少的呢？刚刚我们讲到一个图片都被划分为44小的图像，所以说不管图像多大，一次计算都是一个窗口内77个（4*4）的特征token。\n3计算注意力 # 这个时候就是在每个窗口中独立的做注意力计算。但是特别要注意的\nShifted Window Attention（SW-MSA）特殊情况\n在偶数层做 SW-MSA，会先 对整个特征图做 cyclic shift\n再划分窗口 → 窗口内部注意力 → 合并窗口\n最后再 逆向 shift，保证信息能跨窗口传递\n这个时候还需要mask操作，如图：\n4拼回原图 # 这个代码就是将刚刚对每个窗口计算注意力后进行拼接会原来的特征图大小\n5反向平移 # 在 Swin Transformer 的 SW-MSA（Shifted Window Multi-Head Self-Attention） 中，所谓的 反向平移（reverse shift / reverse cyclic shift），指的是把先前为了让窗口跨界互相通信而做的 cyclic shift 给恢复回原来的位置。\n6残差连接\u0026amp;\u0026amp;MLP # 做完这些就和普通的vit没有区别的。\nPatch Merging # 相当于是将4个token变为一个token，因为是H W上各个取一个，所以就是H W减半然后C变成4了，所以这样时候就要一个线性成将通道减半变为2*c\n所以这个操作也可以是看成不同的patch中做token之间的特征交互。所以通过滑动窗口和Patch Merging 形成了局部与全局的信息交互。这样大大节约的计算量！！！！\n效果 # 怎么做Loss # 基本都是这个哈哈哈哈哈！！！\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/swin-transformer/","section":"日常记录、技术札记与转载收藏。","summary":"Patch 分割和线性嵌入 \\(Patch Partition + Linear Embedding\\)","title":"Swin Transformer","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/swin-transformer/","section":"Tags","summary":"","title":"Swin Transformer","type":"tags"},{"content":" 论文提出 # 问题 1：每像素独立分类忽略了区域层级的语义一致性 # 📖 论文中指出：语义分割任务的目标是识别具有语义一致性的区域（segment），而不是单独的像素。 然而 per-pixel 分类并没有显式地建模这些区域之间的关系。\n具体表现：\n每个像素单独预测类别 → 缺乏“整体 mask”概念；\n导致边界模糊、语义不连贯；\n对大物体或纹理复杂的类别（如天空、建筑）常出现碎片化。\n问题 2：无法统一语义分割、实例分割和全景分割（Panoptic） # 换句话说，像素级分类缺少“对象级”的表达能力。\n📖 论文指出：不同任务的分割方法在架构和训练方式上割裂。\n这些方法需要不同的头部结构、不同的损失函数，无法共享模型结构。 MaskFormer 希望用一个统一的框架同时解决这三类任务。\n模型架构 # 核心一句话 # MaskFormer 提出把语义分割从“每像素分类”转为“mask 分类（预测若干二进制 mask 并给每个 mask 一个类别）”，并用 Transformer decoder + per-pixel embedding 的设计实现这一范式；该方式能同时统一语义/实例/全像素（panoptic）分割，并在大类目数场景下优于传统 per-pixel 方法（如在 ADE20K 上取得 SOTA）\n向前传播 # Backbone # 这个就是一个cnn网络来抽取特征，我们来看看代码里面是怎么做的，在论文中主要是使用了ResNet，和swin Transformer。本质上就是提前特征\n会根据配置文件，输出的是\nC2: \\[B, 256, H/4, W/4\\]C3: \\[B, 512, H/8, W/8\\]C4: \\[B, 1024, H/16, W/16\\]C5: \\[B, 2048, H/32, W/32\\]，然后这个特征大小，一部分会送入Transformer decoder，一部分送入pixel decoder\nPixel Decoder # 这个就是对特征的解码形成mask\nencoder_out = [\n\\[B, 256, H/32, W/32\\],\n\\[B, 256, H/16, W/16\\],\n\\[B, 256, H/8, W/8\\],\n\\[B, 256, H/4, W/4\\],\n]\n最后输出\n\\[B, 32, H, W\\]空白的mask用于和transformer找到的类别做点积\nTransformer decoder # 在 MaskFormer 中，Transformer 主要用于 将 N 个 learnable queries 与图像特征做交互，从而生成最终的 mask predictions 和类别预测。也就是说，它承担了 mask 配对 \\(mask classification/matching\\) 的核心任务。\n刚刚我们不是得到了backbone的图像特征（2048 × H/32 × W/32）这个时候这个就是充当我们KV，然后用N个Query去学习不同的类别mask。输出还是 N 个 query，但是这些 query 已经被更新过了，包含了从图像特征中聚合的信息。\nMLP # 类别预测 MLP # 输入：query embedding (B, N, D)\n输出：每个 query 对应类别 logits (B, N, num_classes)\n每个 query 最终会预测它对应 mask 的类别\n这里怎么去做loss呢？\n其实这个是专门为实列分割设计的，语义分割也是一样的一个实例一个query，或者一个类别一个类别一个query，这样就可以直接从标签数据中获取。\nMask embedding MLP # 输入：query embedding (B, N, D)\n输出：每个 query 对应的 mask embedding (B, N, D_mask)\nD_mask 可以是同维度 D 或者降维后的向量\n后续会与 Backbone feature map 做点积或卷积，生成 **mask logits **(B, N, H, W)\nQuery embedding 就像 “我想找的图案模板”。\nBackbone 特征就像 每个像素的描述信息。\n点积就是 把模板放到每个像素上，看匹配度。\n匹配得好的地方就是 mask。\nPixel-level embedding # Pixel-level embedding = 每个像素的特征向量\nQuery embedding = “想找到的 mask 特征向量”\n点乘 = 把 query 放到每个像素上，看看匹配度\n匹配度高的地方就属于这个 query 对应的 mask\n相当于是说，在通道特征的角度上来说，找相似的模板，然后再填上图像信息。！！！！！！！\nbinary mask loss # binary mask loss 是专门用来匹配 mask 的\nTransformer decoder 输出 N 个 query，每个 query 对应一个 预测 mask\n每张图像有 M 个真实实例 mask（M ≤ N）\n问题：模型不知道哪个 query 对应哪个 GT mask\nMaskFormer 使用 Hungarian Matching 来解决这个问题：\n计算匹配成本：\n每个 query mask 和每个 GT mask 的相似度\n成本可结合：\nMask 相似度（Dice / BCE loss）\n类别匹配（cross-entropy loss）\n找最优匹配：\nHungarian 算法在 N 个 query 和 M 个 GT mask 之间做一一匹配\n匹配后：\n匹配到 GT 的 query → 对应 mask 的 ground truth\n没匹配到的 query → 背景 / 无实例\nSemantic Segmentation # MaskFormer 使用的是 线性融合（或点乘） 的思想：\nMask logits\n每个 query 的 segment embedding 与 pixel-level features 点乘 → (H, W) mask\nshape: (B, N, H, W)\nClass logits\n每个 query 的类别预测向量（经过 softmax） → (B, N, num_classes) 组合成最终 semantic segmentation：\n每个 query 的 mask logits 与它的类别概率向量 点乘（或者广播乘法） Mask 和类别是 可分离的特征：\nmask logits 描述 空间位置\nclass probability 描述 类别信息\n点乘/广播后，可以同时得到 每个像素属于每个类别的概率\n适合 semantic segmentation，因为：\n不区分实例\n每个类别的 mask 是所有 query 的叠加\n效果分析 # 怎么做Loss # 总结手绘 # ","date":"2026/06/15","externalUrl":null,"permalink":"/blog/maskformer/","section":"日常记录、技术札记与转载收藏。","summary":"具体表现：","title":"MaskFormer","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/maskformer/","section":"Tags","summary":"","title":"MaskFormer","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/%E5%9B%BE%E5%83%8F%E5%88%86%E5%89%B2/","section":"Tags","summary":"","title":"图像分割","type":"tags"},{"content":" 模型提出 # 核心 # MaskFormer 让分割任务变成 “预测一组掩码 + 类别”； Mask2Former 让模型 “只看自己负责的掩码区域，更聪明地预测掩码”。\nMask2Former 的 Masked Attention 是最核心的改进。\n传统 MaskFormer 的 cross-attention 是：\n每个 query 对所有像素做 attention，计算复杂且容易受背景干扰。\n而 Mask2Former 改成：\n每个 query 仅在它的 mask 区域内做 attention（即 masked attention）。\n优点：\n显著减少计算量；\n强化 query 对局部区域的关注；\n提升掩码预测的准确性与边界细节。\n类似 Per-Pixel Transformer Decoder + FPN式多尺度融合\n模型架构 # Transformer Decoder（Mask Transformer Decoder） # 在 Mask2Former 中，一个重要启发是：既然 query 最终要看重某些像素（其 mask），那么在 cross-attention 中让 query 只与其“关注区域”交互，比让 query 和所有像素交互更高效、更精确。为此使用masked attention：用上一层（或同层的初步）掩码预测来约束本层 cross-attention 的权重。\n同一个图像特征输入的kv怎么做到不同的query有不同的mask屏蔽 # 虽然所有 query 都共享相同的像素特征（即同一组 K/V）， 但每个 query 都有一个 独立的掩码预测结果（mask logits / probabilities）。\n在计算 cross-attention 时，这个掩码被 动态地用作 attention 权重或屏蔽项， 使每个 query 的注意力分布只集中在它自己 mask 区域。\nMask2Former 的 decoder 是多层结构（一般 6 层）， 每层都会根据上层输出的 mask 来更新 attention 掩码。\n流程是这样的：\n1️⃣ 第一层没有 mask，做全局 cross-attention（等价于标准 DETR）。\n2️⃣ 输出的 mask_probs 被用作第二层的 masked attention 引导。\n3️⃣ 第二层输出新的 mask embedding → 新的 mask_probs，\n供第三层使用。\n⋯\n6️⃣ 最后一层输出最终的 mask。\n这就是 Mask2Former 的 逐层掩码细化 \\(iterative mask refinement\\) 机制。\n前面我们做了Mask相当于是只关注了局部的信息，然后这个又有一个self-attention，相当于是拿完整的KV没有mask掉信息去做一次全局的信息交互。\nPixel Decoder # 第一层：query 与 1/32 特征交互，学习语义大致区域；\n第二层：query 与 1/16 特征交互，细化边界；\n第三层：query 与 1/8 特征交互，进一步精细化；\n每层的输出 query 会作为下一层输入，逐步 refine。\nMask2Former 中：\nTransformer Decoder 的 6 层 并不是一一对应 Pixel Decoder 的 3 个尺度；\n而是 每两层 decoder 共享一个尺度的 pixel feature（K/V）。\n也就是说：\nPixel Decoder 提供 3 个尺度特征（1/32、1/16、1/8），Transformer Decoder 有 6 层，每两个 decoder 层使用同一尺度的 K/V。\n效果 # 怎么做Loss # 总结手绘 # ","date":"2026/06/15","externalUrl":null,"permalink":"/blog/mask2former/","section":"日常记录、技术札记与转载收藏。","summary":"MaskFormer 让分割任务变成 “预测一组掩码 + 类别”； Mask2Former 让模型 “只看自己负责的掩码区域，更聪明地预测掩码”。","title":"Mask2Former","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/mask2former/","section":"Tags","summary":"","title":"Mask2Former","type":"tags"},{"content":" 模型提出 # 在 DINO 出现之前，自监督视觉表示学习主要有两类主流方法：\n对比学习（Contrastive Learning）：如 SimCLR、MoCo → 通过拉近同一图像的不同视角表示、推远不同图像的表示来学习。 → 问题：需要大量负样本、较大batch size。\n基于生成或重建的方法（如 MAE、BYOL、SimSiam）： → 不依赖显式负样本，但往往需要设计复杂的目标或避免“坍塌问题”（collapse）。\nDINO 的提出非常关键，它展示了：\n不使用标签、也不使用对比损失（不需要负样本），仅靠自蒸馏（Self-Distillation）也能学到非常好的视觉特征。\n模型架构 # DINO 支持多种 Backbone：\nViT-S/16、ViT-B/16、ViT-B/8\nResNet-50、ResNet-101\n其中最具代表性的是 Vision Transformer \\(ViT\\) 版本。\nViT + DINO 的关键特性：\nDINO 的自监督训练使 ViT 学到 显著的语义分割能力（Emergent segmentation）： → 不需任何标签，ViT 的 attention map 就能自动分离出目标物体区域！ 怎么训练 # 对同一张图像生成多个视角（multi-crop），用 Student 网络去预测 Teacher 的输出分布（softmax over projection head logits）；Teacher 参数用 Student 的 EMA（滑动平均）更新，Teacher 的输出做 centering + 温度 sharpening，用交叉熵（KL）把 Student 输出对齐到 Teacher 输出。Student 用反向传播更新。\n具体来说 # Backbone：ViT 或 ResNet（ViT 在 DINO 中表现特别好）。\nProjection head（学生/教师）：MLP，把 backbone 特征映射到 logits（再 softmax）。\n温度（temperature）：Student 温度 TsT_sTs 较大，Teacher 温度 TtT_tTt 较小（Teacher 输出更“尖锐”）。\nCentering：对 Teacher 的 logits 做减均值（center），并随时间用 EMA 更新 center，避免分布偏移导致 collapse。（上一个Teacher的平均值）\nEMA momentum：Teacher 参数以 θt←mθt+\\(1−m\\)θs\\theta_{t} \\leftarrow m \\theta_{t} + \\(1-m\\)\\theta_{s}θt←mθt+\\(1−m\\)θs 更新，m 从较小值逐渐接近 1（常见做法）。\nMulti-crop：常见做法 2 个全局 crops + 6-8 个局部 crops（global 224x224，local 96x96）。\n2 个 global crops：随机裁剪到大尺寸（例如 224×224），并进行常规增强（颜色抖动、随机灰度、Gaussian blur、随机水平翻转等）。\nk 个 local crops（6~8）：更小分辨率（96×96），做相似但更强的裁剪增强。\nTeacher 只接收 global crops（2），Student 接收所有 crops（global + local）。\n这样 Student 从局部视角也要匹配 teacher 的全局语义，促使模型学习尺度不变性的语义表示。\n伪代码 # 模型效果 # 怎么做Loss # 我们令：\nStudent 输出的 logits 为 zsz_szs\nTeacher 输出的 logits 为 ztz_tzt\n然后通过 softmax 得到各自的概率分布：\nps=softmax\\(zs/Ts\\),pt=softmax\\(\\(zt−c\\)/Tt)p_s = \\text{softmax}\\(z_s / T_s\\), \\quad p_t = \\text{softmax}\\(\\(z_t - c\\) / T_t)ps=softmax\\(zs/Ts\\),pt=softmax\\(\\(zt−c\\)/Tt)\n其中：\nTsT_sTs：Student 的温度（通常 0.1）\nTtT_tTt：Teacher 的温度（通常较小，如 0.04）\nccc：Center 向量，用于防止 collapse（防止 teacher 输出分布单一）\n目标：让 Student 的输出分布 psp_sps 逼近 Teacher 的输出分布 ptp_tpt。\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/dino/","section":"日常记录、技术札记与转载收藏。","summary":"在 DINO 出现之前，自监督视觉表示学习主要有两类主流方法：","title":"DINO","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/dino/","section":"Tags","summary":"","title":"DINO","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/%E8%87%AA%E7%9B%91%E7%9D%A3%E5%AD%A6%E4%B9%A0/","section":"Tags","summary":"","title":"自监督学习","type":"tags"},{"content":" Sam1 # sam设计的出发点：\n可提示分割任务\n支持灵活提示，实时交互使用\n多样化，大规模的数据源\n重头戏：Mask Decoder（怎么把提示 + 图像特征变成掩码） # 设计宗旨：既要能灵活融合提示，又要极快。因此 SAM 采用一个轻量的改造版 Transformer 解码器，并用“动态头”生成掩码。\n输入与“输出 token” # 输入：图像嵌入（ViT 提供的二维网格）、提示嵌入（点/框/稠密掩码），以及一个学习到的输出 token（类似 ViT 的 [class]，用来“承载”本次要生成的掩码）。ar5iv “双向”交互的 Two-Way Transformer # 每个解码层有四步，关键在双向交叉注意力（two-way）——既让提示看图像，也让图像回看提示：\n对提示 tokens 做自注意力（整合提示内部信息）；\n提示 → 图像：提示作 query，跨注意力读取图像嵌入；\n逐点 MLP 更新提示 tokens；\n图像 → 提示：图像作 query，跨注意力读取提示，把提示信息写回图像特征。 默认两层这样的解码器（数据引擎的某个版本曾用过 3 层）。注意力里会加位置编码，并把原始提示 token（含位置信息）残差相加以保几何信息。ar5iv\n直觉：先让提示“问”图像在哪、长啥样；再让图像特征“带着提示的意图”被改写，这样下游生成掩码时更“对题”。\n上采样 + 动态线性分类器（生成掩码） # 两层转置卷积把解码后的图像特征上采样到更细网格；\n用一个小 MLP 把输出 token映射成一组动态权重，对上采样后的每个像素特征做点积（可理解为“动态线性分类器”），得到每个位置为前景的概率（logit） → 形成整幅掩码。ar5iv\n多掩码与 IoU 置信度（处理歧义） # 一个提示可能对应多个有效目标（比如点击在衣服上，既可能分出衣服，也可能分出人）。SAM 设计为一次输出多张掩码（默认 3 张）来“包容歧义”。训练时对三张分别算损失，只回传最小损失那张（multi-output 最小化技巧）。\n同时再加一个小头，预测每张掩码的置信度（估计 IoU）用于排序。多提示时通常歧义减少，模型只输出一张（通过额外的输出 token 设置实现）。ar5iv\n训练细节（和 Decoder 直接相关的） # 损失：掩码用 Focal + Dice 的线性组合；IoU 预测头用 MSE，与掩码损失并和。\n高效设置：解码器通道维度约 256，注意力 8 头；跨注意力里会把 q/k/v 的维度减半提速；上采样的转置卷积通道（例如 64 → 32）。这些选择让 decoder 计算量远小于大 ViT encoder。ar5iv\n数据引擎，这个务必学会\nSam2 # 从标题上来看，sam2就是比sam1多一个视频功能，下面我们详细阅读原文\n摘要：提出几个关键：更快，更准，加入视频分割，减少交互\n仔细看，有一个记忆注意力机制\n相关工作：\n交互式视频分割：交互式视频对象分割已经成为在用户引导下有效地获得视频（小掩模）中的对象分割的关键任务，所述用户引导通常以涂写、点击或边界框的形式\n视频对象分割：\n它的方法是PVS\n如何实现记忆模块：self attention ， cross attention\nSAM 2 模型架构总览 # SAM 2 是 Meta 发布的新一代基础分割模型，统一处理图像和视频任务，通过 prompt 控制（如点、框、mask）进行分割，具备零样本泛化能力和实时性能。\n核心组件包括： # Image Encoder：采用分层设计（如 Hiera），能在不同尺度抽取丰富特征，支持多尺度融合与视频帧的实时处理 LearnOpenCVSupervisely。\nPrompt Encoder：类似于 SAM1，用于编码用户的点、框、mask 提示 Supervisely。\nMemory 模块（新增）：包含 memory encoder、memory bank 和 memory attention，用于视频中的时序记忆管理：存储过去帧和 prompt 信息，帮助跨帧保持对象一致性，即便对象被遮挡或消失也能追踪回来 Ultralytics DocsSupervisely。\nMask Decoder：一个轻量级而强大的 promptable 解码器，本文重点聚焦其如何结合 prompt、memory 和当前帧特征生成掩码。\nMask Decoder：SAM 2 的核心解码器 # 解码流程结构（图像 \u0026amp; 视频一致、但视频加入 memory 融合）： # 步骤一：Image Embedding + Memory Attention\n首先将当前帧通过 image encoder 得到特征图（frame embedding）。\n通过 memory attention 模块，融合来自 memory bank 的先前帧特征与 prompt，让当前帧的 embedding 带有时序 context 信息。这种 attention 类似 Transformer 的 cross-attention，用来比对 past memories，有助于处理目标遮挡、重现等挑战 arXivSupervisely。\n步骤二：Prompt Cross-Attention\n融合了 memory 信息的帧 embedding，随后接 prompt encoder 编码结果做 cross-attention，再结合 prompt 信息对目标定位提供引导 arXivSupervisely。 步骤三：Mask 生成\n最终利用 decoder 输出生成 segmentation mask。这部分在 SAM1 中用的是轻量 Transformer + 动态 classifier 设计，SAM 2 的思路也是类似，只不过加入了 memory-conditioned embedding。具体生成步骤类似「frame embedding + prompt → decoder → mask logits」。\nSAM 2 还支持不在当前帧出现目标的判断（presence head），专门用于视频中判断目标是否存在当前帧 Supervisely。\nSAM 2 Mask Decoder 的优势 # 推理示例：SAM 2 的 Mask Decoder 工作流 # 输入：当前帧图像 + 用户 prompt（点/框/mask）。\n编码：Image Encoder → 特征图；Prompt Encoder → 提示 embedding。\n融合：memory attention 融合过去帧特征；prompt attention 引导当前帧器官级识别。\n解码：Mask Decoder 基于融合 embedding 输出 segmentation mask 和 presence 置信度。\n记忆更新：当前帧 mask 加入 memory bank，供下帧使用。\n解决方案 # 大规模造数据问题（语义分割） # 对于分割来说，人工标注数据集的成本是高昂的，如何基于少量标注，通过一种机制快速获取大量高标注的数据集呢\nSAM3 # 一、什么是 SAM 3（Segment Anything Model 3） # SAM 3 是 Meta AI 发布的第三代视觉基础模型（Vision Foundation Model），用于在图像与视频中进行 开放词汇（open-vocabulary）可提示分割。它不仅能检测与分割对象，还能通过文本、示例图像或视觉提示来定位、掩码与跟踪匹配的所有实例。\n核心目标可以归结为：\n将“交互式视觉分割工具”升级为“语义概念驱动的通用视觉理解引擎”。\n二、技术能力与核心功能 # Open-Vocabulary 概念分割（Promptable Concept Segmentation） # SAM 3 的旗舰能力是 Promptable Concept Segmentation \\(PCS\\)，即通过自然语言短语（如“黄色出租车”）或示例图像来指定一个概念，并自动识别场景中 所有匹配该概念的实例。这与传统的固定类别分割模型（如 COCO 的 80 类）不同，它可以处理任意用户描述的概念类别。\n特点：\n支持 文本提示（Text Prompts）：直接用自然语言描述想要的对象。\n支持 示例提示（Exemplar Prompts）：给出一个参考样例图像或区域，检测所有相似对象。\n支持 视觉提示（Visual Prompts）：传统的点、框、遮罩等提示用于精细控制。\n统一图像 + 视频检测、分割与跟踪 # SAM 3 能对图像和视频序列做端到端处理：\n图像分割：返回所有匹配概念的像素级掩码（masks）。\n视频跟踪：一旦实例被识别，它即可跨帧跟踪对象，输出一致的实例 ID。\n开放词汇检测：支持广泛语义，而不是固定类表。\n架构要素 # 根据 Meta 提供的技术细节与论文摘要，SAM 3 的架构包含以下模块：\na. 共享视觉编码器 # 一个统一的 backbone（视觉编码器）负责提取图像／视频的基础视觉特征。\nb. 检测器（Detector） # 基于 DETR 风格结构，用于根据提示（文本 + 视觉）定位所有符合概念的对象。\n引入 Presence Token 机制以提高对相似/細粒度概念的区分能力。\nc. 跟踪器（Tracker） # 内置记忆机制，可跟踪视频中多个匹配对象的轨迹。 d. 大数据训练引擎 # 支撑超过 400 万独特概念标签 的大规模训练数据集，用于提升开放词汇泛化能力。 三、与 SAM1 / SAM2 的对比 # 总结对比逻辑：\nSAM 1：只支持静态图像，通过人工交互（点/框）生成单个实例的掩码。\nSAM 2：新增视频跟踪能力，但仍然依赖视觉提示，并且每次通常只分割一个实例。\nSAM 3：真正引入自然语言和示例提示，实现 一次性自动分割多个概念实例，并支持图像与视频双模态处理。\n四、性能与开放词汇泛化 # 独立基准测试显示 SAM 3 在开放词汇目标检测和概念分割任务上显著领先其他模型：如在 SA-Co/Gold 基准上，它的 cgF1 分数远高于现有模型，并在多个视频概念分割指标中表现优异。\n此外，据 Meta 的评估，SAM 3 在 SA-Co 基准上可以达到 接近人类水平（约 75-80%） 的表现。\n五、实际应用场景 # SAM 3 的技术能力使它适配多种现实应用，包括但不限于：\n数据标注与自动标注流水线 快速生成高质量掩码，用于训练下游分割/检测模型。\n视频理解与监控分析 利用文本提示跟踪场景中的指定对象，例如“所有红色车”。\n机器人感知与自动驾驶 可用于动态环境中开放词汇对象识别与跟踪。\n内容编辑与增强现实 基于语义掩码可以进行替换、模糊、特效、背景移除等操作。\n医疗与科研场景分割 可根据医生描述的病灶类型进行自动分割，为辅助诊断提供高效工具。\n六、局限性与未来拓展方向 # 虽然 SAM 3 在开放词汇分割任务上取得了巨大进步，但当前研究也指出如下发展方向：\n自然语言指令的丰富理解：单短语提示虽强，但对复杂语义指令仍需结合更强语言模块（例如 SAM3-I 研究方向）。\n领域特化表现：在特定领域（如医学图像）尚需域内微调提升精度。\n部署资源需求：大规模模型推理对计算资源要求较高，在边缘设备上部署仍是工程挑战。\n七、总结要点 # SAM 3 是 Meta 发布的最新一代视觉基础模型，支持开放词汇分割。\n核心能力是 Promptable Concept Segmentation：利用文本与示例提示自动识别、分割所有相关实例。\n它统一了视觉对象检测、分割、跟踪三个基本视觉任务。\n相较先前版本（SAM 1/2），新增开放词汇理解与多实例输出能力，标志着视觉基础模型向更强语义理解迈进。\nSAM3核心 # Detection-style Queries（实例查询）\n好，这里我彻底换一种“工程直觉优先、数学最少”的方式来讲，而且只讲一件事：\nDetection-style Instance Queries 到底是什么？ 它在代码和模型里“具体干了什么”？\n你可以把下面的解释直接讲给一个做过 YOLO / Mask R-CNN，但没碰过 Transformer 的工程师，对方是能听懂的。\n一句话先给结论（非常重要） # Detection-style Instance Query = 一组“可学习的空槽位”， 每个槽位都在图像里“试着找一个完整的对象/实例”。\n它不是类别，不是 anchor，也不是像素。\n它更像是：\n“我假设图里有 N 个东西，每个 Query 负责去找一个。”\n先把“错误直觉”全部清掉 # 很多人第一次听 Query，会误解成下面这些（都不对）：\n❌ Query = 一个类别\n❌ Query = 一个 anchor box\n❌ Query = 一块图像 patch\n❌ Query = 文本 embedding\n正确的是：\nQuery 本身一开始什么都不是 它只是一个可学习的向量。\n用一个极通俗的比喻 # 把模型想成“多个人同时在图里找东西” # 图像特征 = 一整张复杂的现场\nQuery = 100 个“检查员”\n每个检查员的任务：\n“我看看这张图，有没有一个完整的东西值得我负责？”\nQuery 在代码里的真实形态（非常具体） # # 假设有 100 个 instance queries num_queries = 100 d_model = 256 # 可学习参数 instance_queries = nn.Embedding(num_queries, d_model) 这就是全部。\nshape = (100, 256)\n不对应任何图像位置\n不绑定任何类别\nQuery 是怎么“看图像”的？ # 靠的是这一步（核心）👇\nCross-Attention（Query 看 Image） # Query 作为 Q Image feature 作为 K, V 直觉版：\nQuery 会问图像每一个位置： “你像不像我正在找的那个‘整体结构’？”\n用一句白话解释 Cross-Attention # 对每个 Query：\n“我扫一遍整张图， 把和我最匹配的像素特征收集起来， 然后更新我自己。”\n一轮下来发生了什么？ # 初始时（完全随机） # Query #17：乱看\nQuery #42：乱看\n训练若干轮后 # 模型学会了：\n⚠️ 但它们从未被明确告知这是“裂缝”或“人”\n那“Detection-style”是什么意思？ # 这是和传统分割的本质区别。\n传统分割 # 每个像素：你是什么类别？ Detection-style Query # 每个 Query：你能不能负责一个实例？ 👉 这是 “从像素问类别” → “从实例问像素”\n为什么它和 Detection 很像？ # 你熟悉的检测流程：\nQuery 怎么知道“我该负责哪个 GT 实例”？ # 这一步非常关键👇\nHungarian Matching（训练时） # 模型做的是：\n“预测的 100 个 Query 和 GT 的 M 个实例 一一配对，最优匹配。”\n匹配依据通常包括：\nmask IoU\nbox IoU（如果有）\nmask loss\n结果： # Query #7 ↔ GT #2\nQuery #23 ↔ GT #1\n剩下的 Query → 背景（no-object）\n这一步为什么对泛化很重要？ # 因为：\nQuery 从未被绑定成“某一类” 它只学会： “只要结构和条件对，我就负责你。”\nQuery 最终是怎么变成 mask 的？ # 流程极简版：\nQuery feature ↓ 和 dense image feature 做点积 ↓ 得到一个 mask 直觉解释：\nQuery 学会了一个“模板”， 模板和图像中每个像素算相似度， 相似的地方亮，不相似的地方暗。\n那文本 / Concept 是怎么进来的？ # 非常关键的一句：\n文本不是“告诉 Query 是什么类别”， 而是“调 Query 的关注方向”。\nQuery 仍然是实例槽\n文本只是改变“什么算匹配”\n一个极其直观的总结图（文字版） # [Query 1] ----\\ [Query 2] -----\\ 看整张图 [Query 3] ------\u0026gt; Image Feature Map [Query 4] -----/ 谁像我？ [Query 5] ----/ 最后：\n有的 Query 找到了实例\n有的 Query 什么都没找到\n用一句“能写进论文的通俗解释” # Detection-style instance queries are learnable slots that independently search the entire image for coherent object-level regions via cross-attention, rather than assigning class labels to individual pixels.\n和你研究的直接连接（非常关键） # 你做工业裂缝、缺陷，其实天然适合 Query：\n裂缝 ≠ 固定类别\n裂缝 = 一段“结构连续区域”\nQuery 正是“结构优先”的机制\n如果你愿意，下一步我可以： # 用你贴的 TransformerEncoderLayer 代码逐行对 Query 流程做“工程级解读”\n对比 YOLO anchor / DETR query / SAM prompt 的本质差异\n给你一个“工业裂缝 Query 分割”的最小可实现框架\n你想从 代码级 还是 和你论文工作结合 的角度继续？\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/sam/","section":"日常记录、技术札记与转载收藏。","summary":"sam设计的出发点：","title":"SAM","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/sam/","section":"Tags","summary":"","title":"SAM","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/%E5%9F%BA%E7%A1%80%E6%A8%A1%E5%9E%8B/","section":"Tags","summary":"","title":"基础模型","type":"tags"},{"content":" 问题引出 # 在做DETR之前，目标检测都是下面两种任务方式：\n1.1 Two-stage（Faster R-CNN 系） # RPN 产生候选框\nRoI Pooling + 分类 / 回归\n大量人工设计：Anchor、NMS、RoI 对齐规则\n1.2 One-stage（YOLO / SSD / RetinaNet） # 密集预测（Dense Prediction）\nAnchor-based\n同样高度依赖：\nAnchor 尺寸与比例设计\n后处理 NMS\n他们都存在同一个问题：都需要大量的人工后处理，非端到端。\nDETR是怎么做的 # 将目标检测建模为一个“集合预测（Set Prediction）问题”，\n使用 Transformer 的 Query–Key–Value 机制，\n通过一组固定数量的 Object Queries，直接预测一组不重复的目标实例。\n模型架构 # 具体的工作流程是这样做的，先用一个预训练的cnn模型去抽取特征，将抽取到的特征用transformer的encode去做自注意力，这个过程就是为了做全局信息的交互，然后下一个就是做decoder的，query是一组可学习的向量，kv还是来原于之前的encode。每一个query去找一个目标，论文里面设置的是100个。也就是说最后会输出100个box。Set Prediction \u0026amp; 匈牙利匹配在预测集合与 GT 集合之间做 一对一最优匹配未匹配的预测 → no-object。\n代码定位 # Backone # 原始 DETR 使用 ResNet-50 / ResNet-101\n输出特征：\n通常是 stride=32 的特征图\n通道数 C=2048 → 1×1 Conv 降到 d_model=256\n注意： DETR 并不做 FPN，也没有多尺度（这是早期性能瓶颈的来源之一）。\nTransformer-encoder # 多层 self-attention\n每个位置可以“看到”全图\n建立长程依赖\n这一阶段不关心“目标是什么”， 只是在构建全局一致的视觉语义表示。\nTransformer-decoder # 在 Decoder 中，每一层包含：\n\\(1\\) Self-Attention（Query–Query） # Query 之间交互\n作用：\n避免多个 Query 预测同一个目标\n学习“互斥关系”\n\\(2\\) Cross-Attention（Query–Image） # Query 作为 Q\nEncoder 输出作为 K / V\n作用：\nQuery 从全图中“聚焦”到一个目标区域 Aux Loss（训练技巧） # if self.aux_loss:\nout\n\\['aux_outputs'\\] = \u0026hellip;\n对每一层 Decoder 都加监督\n缓解收敛慢问题\n本质是 Deep Supervision 深度监控\n# 训练 # ETR 模型代码在 forward() 阶段输出的，并不是“已经确定对应 GT 的检测结果”， 而是一个「无序的、固定大小的候选预测集合（set of predictions）」； 真正的“谁对应哪个 GT”，只在训练时由 SetCriterion + Hungarian Matching 在损失中临时决定。\n固定 100 个 Query 的预测集合 + 匈牙利匹配 + 集合级监督\nSetCriterion\n推理 # Input Image\n↓\nBackbone\n↓\nTransformer Encoder\n↓\nTransformer Decoder（100 Queries）\n↓\npred_logits, pred_boxes\n↓\nsoftmax + threshold\n↓\n最终检测结果\n过滤背景 Query # score 低的自然是 no-object\n可以加阈值（如 0.5）\n📌 没有 NMS\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/detr/","section":"日常记录、技术札记与转载收藏。","summary":"在做DETR之前，目标检测都是下面两种任务方式：","title":"DETR","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/detr/","section":"Tags","summary":"","title":"DETR","type":"tags"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/%E7%9B%AE%E6%A0%87%E6%A3%80%E6%B5%8B/","section":"Tags","summary":"","title":"目标检测","type":"tags"},{"content":" 问题提出 # 原来大DETR有几个缺点：\n收敛慢（问题出在注意力中，通常的QK参数初始化是服从均值为0方差为1）\n对feature map尺寸限制\nDeformable DETR做到是：\nAttention modules只关注参考点周围点一小部分采样点\n多尺度特征\n结果：\n小物体检测好，训练只要原来的1/10\nattention的创新：\n就是类似cnn的先验知识\n模型框架 # 多尺度 # 然后在拼接在一起concat\ndeformable attention # 很明显，key的位置坐标，是通过query的位置坐标+偏移位置坐标得到的。其中偏移是来自mlp（query）\n位移偏置由 query embedding 通过线性映射预测，作为相对于 reference point 的连续坐标偏移，用于在多尺度特征图上进行稀疏可学习采样；其学习完全由下游检测损失通过反向传播隐式约束，本质上是将 Deformable Convolution 的空间自适应机制迁移到 Transformer Attention 中。\n在多尺度上，是在多个特征尺度上的特征，每一层取3个，然后这三个加权融合，每一层的特征最后在组合在一起，映射输出。\n","date":"2026/06/15","externalUrl":null,"permalink":"/blog/deformable-detr/","section":"日常记录、技术札记与转载收藏。","summary":"原来大DETR有几个缺点：","title":"Deformable DETR","type":"blog"},{"content":"","date":"2026/06/15","externalUrl":null,"permalink":"/tags/deformable-detr/","section":"Tags","summary":"","title":"Deformable DETR","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/diffusion/","section":"Tags","summary":"","title":"Diffusion","type":"tags"},{"content":" 背景搭建，这个是扩散模型的基本。\nDiffusion本质：学一个“从噪声还原数据”的过程 # Diffusion建模的是：\n从纯噪声 $x_T \\sim \\mathcal{N}(0, I)$ 一步步恢复成真实数据 $x_0$\n怎么加噪声 # 把真实数据逐步加噪，直到变成高斯噪声。\n逐步加噪（马尔可夫链） # 每一步：\n$q(x_t | x_{t-1}) = \\mathcal{N}(x_t; \\sqrt{1-\\beta_t}x_{t-1}, \\beta_t I)$\n含义：\n保留一部分原始信息\n加一点高斯噪声\n实际训练不会一步步加，而是直接采样：\n$x_t = \\sqrt{\\bar{\\alpha}_t} x_0 + \\sqrt{1 - \\bar{\\alpha}_t} \\epsilon$\n其中：\n$\\epsilon \\sim \\mathcal{N}(0, I)$\n$\\bar{\\alpha}_t = \\prod (1-\\beta_i)$\n直觉理解 # 这个公式非常关键，你可以这样理解：\nx_t = 原始数据 × 保留比例 + 噪声 × 扰动比例\n随着 t 增大：\n原始信息 ↓\n噪声 ↑\n最终：\nx_T ≈ 纯噪声\n怎么“去噪”（Reverse Process） # 目标：\n从 $x_t$推回 $x_{t-1}$\n理论形式\n真实分布：\n$q(x_{t-1} | x_t)$\n但它不可直接求，所以：\n👉 用神经网络近似：\n$p_\\theta(x_{t-1} | x_t)$\n实际做法：预测“噪声” # DDPM做了一个关键简化：\n👉 不预测 $x_0$，而是预测噪声 $\\epsilon$\nϵθ(xt,t)\n反推 x₀（核心公式） # $x_0 = \\frac{1}{\\sqrt{\\bar{\\alpha}_t}}(x_t - \\sqrt{1-\\bar{\\alpha}_t} \\epsilon)$\n逐步去噪（推理） # 每一步：\n$x_t$→ 预测 ε → 推回 $x_{t-1}$\n循环：\nT → T-1 → \u0026hellip; → 0\n👉 一般需要：\n50步（优化版）\n1000步（原始DDPM）\n怎么做“监督”（最关键） # 训练目标（标准DDPM） # $\\mathcal{L} = \\mathbb{E}_{x_0, \\epsilon, t} \\| \\epsilon_\\theta(x_t, t) - \\epsilon \\|^2$\n训练流程 # # 1. 采样真实数据 x0 # 2. 采样时间 t ~ Uniform(1, T) # 3. 采样噪声 ε ~ N(0, I) # 4. 构造带噪数据 xt = sqrt(alpha_bar)*x0 + sqrt(1-alpha_bar)*ε # 5. 模型预测噪声 ε_pred = model(xt, t) # 6. 做MSE loss = ||ε_pred - ε||^2 问题 # 路径是“随机的” # Forward：\n是 stochastic process（随机过程） Reverse：\n也是 stochastic 👉 不稳定\n不是直接学“变化方向” # Diffusion学的是：\n噪声 ε\n而不是：\n从 $x_t → x_{t-1}$ 的方向\nFlow Matching(场流动力学) # 不再用随机扩散 # 而是直接定义一条路径：\n$x_t = (1 - t)x_0 + t x_1$\n👉 从噪声 → 数据\n$\\frac{d x_t}{dt} = x_1 - x_0$\n训练目标变成 # $\\mathcal{L} = \\mathbb{E} \\| v_\\theta(x_t, t) - (x_1 - x_0) \\|^2$\n训练流程 # # 假设： # data_action: 真实专家动作 x1 # noise_action: 随机噪声动作 x0 # t: 随机时间 # xt: 路径上的中间状态 # model: 预测速度场 v_theta(xt, t, cond) for batch in dataloader: # 1. 取真实数据（专家动作） x1 = batch[\u0026#34;action\u0026#34;] # shape: [B, D] # 2. 取条件信息（图像、文本、状态等） cond = batch[\u0026#34;condition\u0026#34;] # 3. 采样噪声起点 x0 = randn_like(x1) # shape: [B, D] # 4. 采样时间 t t = Uniform(0, 1).sample([B, 1]) # shape: [B, 1] # 5. 构造中间状态 xt xt = (1 - t) * x0 + t * x1 # shape: [B, D] # 6. 构造监督目标：真实速度 target_v = x1 - x0 # shape: [B, D] # 7. 模型预测速度 pred_v = model(xt, t, cond) # shape: [B, D] # 8. 计算损失 loss = mse_loss(pred_v, target_v) # 9. 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() 采样时间t\n意思是：\n每个样本随机选一个时刻\n训练时不走完整条路径\n只监督路径上的一个随机位置\n这是 Flow Matching 很高效的地方。\nFlow Matching 训练时，先采样一个真实样本 $x_1$ 和一个噪声样本$x_0$，再在它们之间随机插值得到中间状态$x_t$，并用路径的真实速度 $x_1-x_0$作为监督信号，训练模型预测该中间状态下的速度场。\n对比总结 # 总览 # 传统 Diffusion # 先把真实数据逐步加噪成高斯噪声，再训练模型学习“怎么一步步去掉噪声”。\nFlow Matching # 不再真正做“逐步加噪”，而是直接在“噪声样本”和“真实数据”之间连一条路径，训练模型学习这条路径上的运动方向。\n第一部分：加噪方式对比 # Diffusion # Diffusion 的 forward process 是：\n$q(x_t|x_{t-1})=\\mathcal{N}(\\sqrt{1-\\beta_t}x_{t-1}, \\beta_t I)$\n意思是：\n上一步的样本是 $x_{t-1}$\n这一时刻 $x_t$ 在保留一部分原信息的同时\n再加上一点高斯噪声\n如果把很多步合起来，训练时常写成闭式：\n$x_t=\\sqrt{\\bar{\\alpha}_t}x_0+\\sqrt{1-\\bar{\\alpha}_t}\\epsilon,\\quad \\epsilon\\sim\\mathcal{N}(0,I)$\n这里：\n$x_0$ 是真实数据\n$\\epsilon$ 是随机噪声\n$x_t$ 是“被污染后的数据”\n直观理解 # Diffusion 是真的在做：\n真实数据 -\u0026gt; 加一点噪声 -\u0026gt; 再加一点噪声 -\u0026gt; 再加一点噪声 -\u0026gt; 最后变成纯噪声\n所以它的“加噪”是一个逐步扩散过程。\nFlow Matching # Flow Matching 常见写法是：\n$x_t=(1-t)x_0+t x_1$$\\quad t\\in[0,1]$\n这里：\n$x_0$噪声样本\n$x_1$：真实数据\n$x_t$：两者之间的插值点\n它不是：\n真实数据一步步被污染\n而是：\n先拿一个噪声起点 x0\n再拿一个真实终点 x1\n然后在两者之间随便取一个中间点 xt\n所以 Flow Matching 没有真正意义上的“逐步加噪”。\n它更准确地说是：构造噪声到数据的中间状态\n加噪方式本质区别 # Diffusion # 从真实数据出发\n逐步加入随机高斯噪声\n$x_t$ 是一个随机变量\n本质上定义的是一个条件分布\nFlow Matching # 直接采样一个噪声起点和一个真实终点\n通过插值得到中间状态\nxtx_txt 是一条路径上的确定点\n本质上定义的是一条轨迹\n第二部分：建模对象对比 # Diffusion 建模的是什么 # Diffusion 表面上是在预测噪声：\n$\\epsilon_\\theta(x_t,t)$\n但本质上，它建模的是：\n数据分布的逆扩散过程\n也就是：\n如何从带噪样本 xt 恢复到更干净的样本 xt-1\n所以它是分布建模。\n更严格地说，它对应的是一个随机过程，通常写成 SDE（随机微分方程）。\nFlow Matching 建模的是什么 # Flow Matching 直接学习一个向量场：\n$v_\\theta(x_t,t)$\n它希望模型输出：\n在当前时刻 t、当前状态 $x_t$ 下，应该朝哪个方向移动\n所以它建模的不是“噪声分布”，而是：\n噪声到数据的连续运动规律\n这本质上是轨迹建模 / 动力学建模。\n更严格地说，它对应一个 ODE（常微分方程）：\n$\\frac{dx}{dt}=v_\\theta(x,t)$\n一句话 # Flow Matching 学的是：\n“当前这个点，下一步该往哪里走”\nDiffusion # 建模对象：逆扩散分布\n数学本质：SDE\n学的是：噪声 / score / 等价参数化\nFlow Matching # 建模对象：连续轨迹上的速度场\n数学本质：ODE\n学的是：速度 / 方向\n第三部分：去噪方式对比 # Diffusion 怎么去噪 # Diffusion 推理时从纯噪声开始：\n$x_T\\sim \\mathcal{N}(0,I)$\n然后一步一步往回推：\nxT -\u0026gt; xT-1 -\u0026gt; xT-2 -\u0026gt; \u0026hellip; -\u0026gt; x0\n每一步都要：\n输入当前 $x_t$\n模型预测噪声$\\epsilon_\\theta(x_t,t)$\n用解析公式计算更干净的 $x_{t-1}$\n典型形式可以理解为：\n$x_{t-1}=\\text{denoise}(x_t,\\epsilon_\\theta(x_t,t))$\n特点 # 是多步迭代\n每一步都在“去掉一点噪声”\n生成过程比较慢\n常常需要很多步\nFlow Matching 怎么“去噪” # Flow Matching 推理时也是从噪声开始，但它不是“去噪预测”，而是“沿着速度场前进”。\n假设初始是：\n$x(0)∼N(0,I)$\n然后按时间积分：\n$\\frac{dx}{dt}=v_\\theta(x,t)$\n离散化后，比如 Euler：\n$x_{t+\\Delta t}=x_t+\\Delta t \\cdot v_\\theta(x_t,t)$\n或者如果时间方向反着定义，也可能写成减号，取决于起点终点约定。\n直观理解 # 它不是：\n当前有多少噪声，我减掉多少\n而是：\n当前在路径的这个位置，我该沿哪个方向走\n特点 # 也是多步\n但每一步是“积分运动”\n不是“预测噪声并去掉”\n更像连续控制\n去噪方式本质区别 # Diffusion # 方式：逐步去噪\n每一步：预测噪声，再还原\n本质：逆扩散采样\nFlow Matching # 方式：沿向量场积分\n每一步：预测速度，再更新位置\n本质：解 ODE\n第四部分：训练监督方式对比 # ","date":"2026/06/14","externalUrl":null,"permalink":"/blog/flow-matching/","section":"日常记录、技术札记与转载收藏。","summary":"从 Diffusion 的加噪、去噪与监督目标出发，逐步理解 Flow Matching 如何学习从噪声到数据的连续流场。","title":"Flow Matching","type":"blog"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/flow-matching/","section":"Tags","summary":"","title":"Flow Matching","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/","section":"Tags","summary":"","title":"深度学习","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E7%94%9F%E6%88%90%E6%A8%A1%E5%9E%8B/","section":"Tags","summary":"","title":"生成模型","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/agibot/","section":"Tags","summary":"","title":"AgiBot","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/go-1/","section":"Tags","summary":"","title":"GO-1","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/vla/","section":"Tags","summary":"","title":"VLA","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E5%85%B7%E8%BA%AB%E6%99%BA%E8%83%BD/","section":"Tags","summary":"","title":"具身智能","type":"tags"},{"content":" 要点 # GO-1整体采用“理解-规划-执行”的分层结构。首先通过InternLM2构建的VLM将视觉和语言信息编码为多层KV cache作为环境语义表示；随后Latent Planner通过少量latent query token对这些KV进行cross-attention，提取与动作相关的规划信息并生成新的planner KV；最后Action Expert在planner KV条件下，通过Transformer与diffusion机制逐步生成连续动作序列。值得注意的是，论文中提出的LAM模块在当前代码中并未显式实现，其作用被隐式融合进VLM表示与latent planner的结构瓶颈中，从而实现跨模态动作语义的建模。\n论文背景 # 这个是：平台 + 数据 + 模型的系统工作。论文核心做了两件事：\n构建 AgiBot World Colosseo：一个大规模真实机器人操作平台 （大规模构建数据）\n提出 GO-1：一个分层的通用机器人策略模型，用于把视觉语言理解转成连续动作控制 （怎么统一异构数据）\n论文想解决的问题是：\n机器人通用策略之所以难，不只是模型问题，更是因为缺少大规模、高质量、标准化、真实世界的操作数据。\n数据构成 # 单纯的人视频数据，没有机器人action\n各种不同型号机器人动作数据，有机器人action数据\n文本，没有机器人数据\ngo1要做的本质就是：不同来源、不同形式、不同标注方式的“视觉序列 +（可选）动作信息”，最后被统一成“帧序列 + 对齐信号”（action token）的形式。\n怎么解决异构数据 # 提出Latent Action Model，一个高维度的特征空间统一表达。（这里是被隐式的收藏入Latent Planner结构中）\n论文里面的故事 # LAM（统一动作表示）I_t, I_{t+H} → z\n把所有序类的变化变为：“变化” → latent action\n然后通过一个Latent Planner 做cross attention 得到规划后的4个action token（这个就非常像是多模态里面的桥接器Q-former）本质上就是特征压缩\n代码真实操作 # 文本与图像统一到序列里面\n对于action state \u0026ndash;》token\nstate是这样变化的 # self.state_adaptor = nn.Linear(self.state_dim, self.action_hidden_size)\n到\nstate: [B, T, state_dim]\n→ Linear\n→ [B, T, hidden_size]\nAction是这样变化 # action_trajs = self.action_adaptor(noisy_action)\n到\naction_trajs = [B, T, hidden_size]\n时间信息 # [B, 1, hidden_size]\n控制频率 # [B, 1, hidden_size]\nstate+action+timestep+freq # 分两步骤\n1 state_action_trajs\n[B, 1 + T + T, hidden_size]\n2 state_action_trajs_w_tfps\n[B, 1 + (1 + T + T), hidden_size]\n最终维度\n[ timestep ]\n[ freq ]\n[ state_1, state_2, \u0026hellip;, state_T ]\n[ action_1, action_2, \u0026hellip;, action_T ]\n在当前 GO-1 的代码实现中，VLM 只接收图像和文本输入，用于构建统一的语义 KV 表示，并不接收任何动作信息；动作（以及状态、时间步等控制变量）是在后续 Action Expert 阶段通过 adaptor 映射为 token 序列输入，并在 attention 中与 VLM 的 KV memory 交互，从而实现感知与控制的解耦。\n本质上和pi0相似都是：VLM + 条件 Transformer + Diffusion 动作生成\n但但 GO-1 多了一层“Latent Planner（结构性规划瓶颈）”，而 pi0 是直接从 VLM 到 action\ngo1 # VLM # 这里有自己的一个模型。go1/internvl/model/go1/modeling_internlm2_go1.py\n里面还是经典的VLM模型\n实例化 # 输入 # 图像\nvit_embeds = self.extract_feature(pixel_values)\n文本\ninput_embeds = self.language_model.get_input_embeddings()(input_ids).clone()\n一起融合\nvlm_outputs = self.language_model(\u0026hellip;)\nvlm_key_values = vlm_outputs.past_key_values\n输出 # 这里的输出还是KV对\n抽取每一层的kv 这里可能用到了cv里面对多尺度的理解\n大概长这个样子\nvlm_key_values = [ (K1, V1), (K2, V2), ... (KL, VL) ] kv就是包含了上下文语义信息的（通过self-attention进行关联）\nLatent Planner（隐式LAM latent action model操作） # 原理 # Latent Planner 的位置在 VLM 和 Action Expert 中间。它不直接输出动作，而是把 VLM 的语义记忆进一步加工成更适合动作生成的 planner KV。（个人觉得这里非常像Q-former，就是一个桥接器，做特征压缩）\n输入 # 显式输入只有两个：\nvlm_key_values_downsample\nattention_mask\n里面的kv就是VLM里面的，经过投影层变换\nfor vlm_key_value, k_proj, v_proj in zip(vlm_key_values, self.k_proj_layers, self.v_proj_layers): vlm_key_values_downsample.append((k_proj(vlm_key_value[0]), v_proj(vlm_key_value[1]))) mask是做无关特征mask包括padding\n核心 # 核心就算构造4个造 4 个 latent query token\n在 LatentPlannerModel.forward() 里：\nlatent_state = torch.ones((B, 1, 1), dtype=torch.bfloat16, device=attention_mask.device) * -1 然后在 latent_action_process() 里：\nH = self.latent_token_nums\naction = torch.cat([state] * H, dim=1) # [B, 4, 1]\n也就是先造出 4 个值为 -1 的占位 token。\n然后将把这 4 个 token 映射到hidden空间\nstate_action_trajs = self.latent_state_action_adaptor(action) 变成query，维度为[B, 4, hidden_size]\n然后做crossattention\noutputs = self.latent_action_expert(state_action_trajs, attention_mask, vlm_key_values_downsample) 所以 planner 不是直接输出动作，而是先从 VLM 语义空间里抽取“动作相关信息”。\n然后这个应该就是变成kv了给action expert使用\n输出 # 接上上面的就是输出\nreturn ( latent_vlm_key_values_downsample, outputs_latent, ) Latent Planner 不是直接生成动作，而是把 VLM 的通用语义记忆，压缩/提炼成更偏“动作规划”的 KV memory。\n这个就是论文里面所谓的核心点，动作规划（讲故事）\nAction expert # 输入 # state token + action token + 时间步 token + 控制频率 token 然后再结合上游的KV memory\n然后代码里面有配置是否开启planner，也就是说维度对齐了，这个开不开都行，这也就是go air版本\nstate / action / timestep / freq 组成序列的\nstate_action_trajs = torch.cat([state_trajs, action_trajs], dim=1)\nstate_action_trajs_w_tfps = torch.cat(\n[timestep_tokens, freq_tokens, state_action_trajs], dim=1\n)\n[ timestep ][ freq ][ state tokens ][ action tokens ]\n融合KV，这里很粗暴，直接拼接在里面\nkey_states = torch.cat([vlm_key_values[0], key_states], dim=2)\nvalue_states = torch.cat([vlm_key_values[1], value_states], dim=2)\n把上游语义 memory 直接并入自己的 attention memory 里。\n输出 # ActionExpertModel.forward() 最后输出的是：\nBaseModelOutputWithPast(\nlast_hidden_state=hidden_states,\npast_key_values=next_cache,\n\u0026hellip;\n)\n在主干里只取它的 hidden states：\nstate_action_output_tokens = outputs[0]\naction_output_tokens = state_action_output_tokens[:, -self.action_chunk_size :, \u0026hellip;]\naction_logits = self.final_layer(action_output_tokens)\n所以最终：\nAction Expert 先输出 hidden sequence\n再取最后那段 action token 对应的 hidden\n再过 final_layer\n得到 action_logits。\n概括 # VLM 负责把图像和文本编码成统一的多层 KV memory；Latent Planner 用 4 个 latent query token 从 VLM 的 KV 中提取动作规划信息，并生成新的 planner KV；Action Expert 再把 timestep、freq、state、noisy_action 组成 token 序列，在 planner KV 条件下通过 transformer + diffusion 生成连续动作。当前主干代码里，最终显式监督只落在动作端，即用 MSE 将 action_logits 回归到 action_gts，而 VLM 和 Latent Planner 没有在这条主路径中显式单独加 loss。\n# ","date":"2026/06/14","externalUrl":null,"permalink":"/blog/agibot-world-go1/","section":"日常记录、技术札记与转载收藏。","summary":"拆解 AgiBot World 与 GO-1 的平台、数据和模型设计，重点关注理解-规划-执行分层结构、Latent Planner 与 Action Expert 的实现路径。","title":"智元 GO-1 / AgiBot World GO-1","type":"blog"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E6%9C%BA%E5%99%A8%E4%BA%BA/","section":"Tags","summary":"","title":"机器人","type":"tags"},{"content":" 背景 # 当前VLA的三种方式\n（a）让 VLM 先生成一些中间子任务subtasks到浅层空间，再结合浅层提示和vlm的kv cache做cross attention\n再linear映射到标量做为动作输出，这里有两种监督，一个是浅空间vlm 文本指令类监督做交叉熵损失，另外一个是动作监督做mse均方误差·监督，通过联合任务建模，提升动作信号在图像文本里面关联建模融合（pi fast核心自回归action token）（典型模型pi0.5）（里面核心也是用视觉，文本特征引导动作生成）\n（b）世界模型典型的worldModel类模型。核心由编码器、动力学模型、解码器和奖励模型组成，编码器将图像压缩为潜变量，动力学模型基于当前潜变量和动作预测未来状态，解码器将潜变量重建为图像以验证预测的准确性，奖励模型预测每个状态转移的奖励。就是也是两种建模，一个是未来下一个状态图像重建建模，未来下一个动作轨迹预测建模。训练的时候，做第一个loss的时候判断预测图像和真实图像是否尽可能接近，第二个loss时候判断轨迹路线是否尽可能拟合，可以用KL或者mse损失（里面核心也是说用视觉特征引导动作生成）\n（c）这个就是典型的pi0模型，本质上就是vlm做对齐工作讲视觉文本指令对齐到action上面，在通过参考的action构造噪声action融合，做vt训练得到局部的去噪方向，再推理中多步去去噪，逼近真实action。（这里核心就是多模态对齐工作，和flowmatching去噪）\n但是\n仍停留在输入空间，而不是输出动作空间。\n更具体说，VLM 的预训练知识偏语义理解，擅长“看懂、说懂”，不天然擅长低层控制；世界模型预测的是未来视觉状态，本质仍是视觉表征；可机器人最后要执行的是连续低层动作，二者之间存在一个文中反复强调的 **semantic-kinematic gap。**就是说没有一个过渡的粗略动作状态空间，去平滑VLM和action expert\n故，项目的核心围绕着Explicit Action Reasoner 和 Implicit Action Reasoner。构建合成粗粒度运动轨迹以提供直接运动线索和利用交叉注意力建模从 VLM 骨干内部表征中提取潜在动作先验去做：动作层面的粗粒度参考。\n核心思路 # 把推理环节推理发生在动作空间浅空间（故事成分多感觉，本质是pi0.5推理环节也是发生在浅空间做subtasks环节，只是作者觉得这里的监督方式还是监督文本指令，而不是action推理（大概就是说的粗略action））\nExplicit Action Reasoner, EAR 显式动作推理器 负责生成一条粗粒度参考动作轨迹\nImplicit Action Reasoner, IAR 隐式动作推理器 负责从 VLM 内部表征里提取潜在动作先验\nAction-Guided Prediction, AGP 动作引导预测头 把 EAR 和 IAR 的结果一起喂给最终动作头，帮助做动作预测\n这个设计其实非常有意思，因为作者把“动作推理”拆成了两个互补来源：\n显式动作信息：像示范轨迹那样，能直接写成动作序列 （粗动作，应该这里是有真实的监督信号）\n隐式动作信息：语言里的“抓取”“伸手”，视觉里的 affordance、交互意图，这些虽然不是机械臂轨迹，本质上就是前在的语义空间，（就是vlm里面的kv cache，通过和pi0.5一样的在pre-train环节大规模多模态数据训练得到的语义分布，同时又包含了action token自回归的训练，attention交互融合后，概率分布空间隐含了动作关联视觉文本信息），但隐含可行动作分布。\n做两套动作层面的 reasoning：\n一套是可执行的粗参考动作\n一套是潜在动作分布/行为先验\n然后再共同约束最终动作预测。一起联合做监督\n模型架构 # 整体 # VLM Backbone # 模型配置 # 实例化 # ACoT-VLA是实现于pi0.5 之上的，代码上看还兼容pi0。\n它采用：\nSigLIP作为视觉编码器\nGemma 2B 作为 LLM backbone\n也就是说，ACoT-VLA 并没有推翻pi0.5 的主干，而是在pi0.5上，额外插入动作空间reasoning模块（EAR，IAR）外挂上去。\ncoarse action expert：负责 EAR 对应的粗动作参考轨迹生成\naction expert：负责最终动作预测\n**IAR：**直接从 VLM 的 KV cache 上抽隐式动作先验\n总体来看的话，数据流向：\n共享前缀 # 1，把三路图像都变成视觉 token每张图先走 SigLIP，得到 image tokens。\n2，把 prompt 变成语言 token调用 LLM 的 embed 方法得到 token embedding。然后把所有 token 沿序列维拼起来，形成统一 prefix。\n后续 KV cache 的全部来源\nExplicit Action Reasoner (EAR) # 显式动作推理器 负责生成一条粗粒度参考动作轨迹\n输入：多模态上下文 + noisy coarse action\n输出：reference trajectory\n训练：flow matching\n推理：先 denoise 生成 coarse action，再引导最终动作头\n如何外挂加进去？ # 粗动作推理支路使用一套单独的表示空间它不直接和最终action expert共享输入投影，和论文中讲的EAR是单独轻量transformer是一样的。\n实例化 # 输入 # 初始来源\n生成的：\ncoarse_actions\n这个是对ERA这个小模型的真正GT。\n也就是说原始的输入就是一个一条粗粒度动作轨迹。\n怎么来的，应该就是在原始的suffix中对真实纯净的action进行采样，采样出一个粗略的action 也就是coarse_action\n输出 # 输出的就是参考粗略轨迹动作\n做监督loss # 这个和pi0 一样action expert 用的是flow matching\n这个说明这个模型也是联合loss做监督，一个是粗动作loss， 一个是Action-Guided Prediction\n下面两个loss = loss + loss\nImplicit Action Reasoner (IAR) # 隐式动作推理器负责从 VLM 内部表征里提取潜在动作先验（就是vlm 里面的kv cache特征信息）\n实例化 # 这三种本质上是怎么 KV里读信息这个核心点上做了三种不同设计\n交互模块\n把提取出来的implicit action tokens，和当前的action expert tokens做cross-attention对齐\n所以IAR是做两步：\n第一段：self.implicit_action_reasoner\n从 KV cache提取 $Z^{im}$\n第二段：self.implicit_action_reasoner_interact\n让最终动作token去读取$Z^{im}$ ，得到对齐后的$S^{im}$\n这里就是所谓的潜在动作先验\n输入 # IAR extractor的输入\nIAR interact的输入\n输出 # 这个输出就是隐式动作相关特征。\nAction-Guided Prediction（AGP） # 这个就是模型最后的动作预测头了：EAR（显式轨迹）+ IAR（隐式先验）转成对最终 action expert 的条件约束，并参与 flow matching 训练。\n这这个地方接收所有的输入：（就是pi0 里面的action expert）\n核心（两次 cross attention） # EAR # 大概就是当前token应该去query当前应该做什么动作\nIAR # 从高层语义kv中获取关联度更高的动作 action。\n论文原图中：两个cross attention地方\n融合 # 先liner 在concat融合。再做self attention做query互斥（各种学不同特征表达（相当于是学习不同的范式））学习融合。\n最终监督 # AGP 接收 EAR 的 coarse trajectory 和 IAR 的 latent prior，通过两次 cross-attention 将它们对齐到当前 action token，再融合成最终动作表示，最后通过 flow matching loss（u_expert_t vs v_expert_t）进行监督。\n总结 # ACoT-VLA 的本质是：在 pi0.5 的基础上，引入动作空间的中间推理（coarse trajectory + latent prior），并通过 cross-attention 将其注入到最终动作生成过程中，从而显著提升长时序任务与泛化能力。\n存疑问？ # 在pi0.5下，IAR是有必要的吗？pi0.5包含了pi-fast拥有自回归训练action token，理论上说，action动作信息已经融合进去vlm里面了。已经在同一个概率分布空间里面，拥有先验的推理预测能力。\n","date":"2026/06/14","externalUrl":null,"permalink":"/blog/acot-vla/","section":"日常记录、技术札记与转载收藏。","summary":"围绕 ACoT-VLA 的背景、模型架构、显式/隐式动作推理与动作引导预测，梳理 VLA 中语义理解到低层动作控制的过渡设计。","title":"ACoT-VLA","type":"blog"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/action-reasoning/","section":"Tags","summary":"","title":"Action Reasoning","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E5%A4%9A%E6%A8%A1%E6%80%81/","section":"Tags","summary":"","title":"多模态","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/llama/","section":"Tags","summary":"","title":"LLaMA","type":"tags"},{"content":" SFT（Supervised Fine-Tuning） 是指在已有的大语言模型（LLM，如 GPT-3、LLaMA）基础上，利用人工标注的监督数据进行微调，使模型能够更好地完成特定任务或符合特定风格。\n通俗理解：\n原始大模型就像一个“万能工具箱”，能干很多事情，但不一定专精。\nSFT就是拿一些明确示例（输入→输出），教模型“这样回答是对的”，让模型在特定任务上更精准。\n关键特点：\n监督学习：有明确的输入和对应正确输出（训练标签）。\n微调：不是从零训练，而是在大模型已有知识上微调。\n输出通常是 生成式文本，也可用于分类、问答等任务。\n如果按照数据类型分类 # 指令-响应型（Instruction-Response）\n数据格式：输入（指令） → 输出（回答）\n特点：常用于 ChatGPT 类模型训练\n示例：\nInput: 翻译“Hello”成中文 Output: 你好 问答型（Question-Answer）\n类似指令型，但通常是明确问题 → 明确答案\n用于 FAQ、知识问答系统\n对话型（Conversation / Multi-turn Dialogue）\n数据包含多轮上下文\n训练模型理解上下文关系\n示例：\nUser: 你好，今天天气如何？ Model: 今天天气晴，温度25℃。 User: 明天呢？ Model: 明天有小雨。 分类型（Text Classification）\n输入文本 → 标签输出\n用于情感分析、主题分类等任务\n生成型（Text Generation / Summarization）\n输入是文本或文章 → 输出是摘要、改写、翻译等 按训练策略分类 # 全量微调（Full Fine-Tuning）\n更新模型所有参数\n优点：表达能力最强\n缺点：算力消耗大，容易过拟合\n低秩微调（LoRA / Adapter + SFT）\n只训练部分参数或附加模块\n优点：节省显存，容易管理\n缺点：可能略低于全量微调性能\n混合策略（Hybrid SFT）\n先用全量 SFT，再加低秩/Adapter 微调特定任务\n常用于多任务或多领域微调\n上面就是简单的讲解了一下具体都有哪些的东西。下面会详细的讲解全量微调（Full Fine-Tuning）与 低秩微调LoRA。\n在任务开始之前，我们先来讲解一下我们的微调数据集。\n微调数据集 # 我们之前已经做过了预训练，那么这个时候到底什么是预训练呢？我们不是已经训练了吗？为什么还有一个微调？这个又是一个什么训练？\n预训练是干嘛？ # 想象你要培养一个人，让他成为一个“智能助理”。你不可能一开始就教他写公文、做客服，你得先让他理解语言本身。\n这就是「预训练」的目的。\n在预训练阶段，我们不会告诉模型“你要翻译”“你要总结”“你要写代码”。\n它得到的唯一任务是：\n——在海量文本中，给定前面的内容，预测下一个词是什么。\n例如，看到\n“今天天气很好，我想去___”\n模型就学习去预测下一个词：“散步”、“公园”、“外面”等。\n通过这种预测，它慢慢学会语言的统计规律、语法结构、常见表达方式、词义之间的关系、甚至隐含的世界知识。\n如果它看够多的文本，就能在参数中编码出非常丰富的语言理解能力。\n比如，当它看到“北京是中国的”，它知道接下来的词概率最大的是“首都”；当它看到“水在零度以下会”，它知道可能接“结冰”。\n预训练的核心是“语言建模”，不是任务学习。\n它像一个人，从小到大通过阅读几亿本书，掌握语言和常识，但他并不知道你要他“干什么”。\n他能理解语言，但还不会像人一样“照指令行动”。\n他可以续写文章，但你让他“写一份律师函”时，他可能写不出规范格式的内容。\n微调是干嘛的？ # 这就到了第二个阶段——微调。\n微调，就是在预训练模型的基础上，教它“怎么做事”。\n你不再让它盲目预测下一个词，而是用人类准备好的「输入 → 输出」示例，告诉它“当别人这么问时，你应该这么回答”。\n比如：\n用户：请帮我写一封辞职信。 模型：尊敬的领导，您好……（写出完整辞职信） 通过成千上万这样的样本，模型学会了遵循人类指令、完成特定任务、采用适合的语气。\n从训练机制上看，这个阶段依然是优化交叉熵损失（预测正确答案的概率），但训练的数据不同了：\n预训练时的数据是无标签的自然文本；\n微调时的数据是有明确目标输出的人工标注数据。\n因此，微调不是“再训练一次语言模型”，而是“在语言理解能力的基础上，矫正行为模式”。\n它让模型不再只是复述知识，而是学会根据任务需求输出特定形式的答案。\n这就像一个人：\n他在预训练阶段读了所有百科、小说、报纸，语言能力极强；\n但如果你要他去做医生、律师、客服，他还需要在那些领域接受“专业训练”——这就是微调。\n举个真实例子。\nOpenAI 训练 GPT 系列模型时，首先用互联网上几万亿个 token 的语料进行预训练，耗时数月，成本巨大。\n这个阶段得到的模型（比如 GPT-3）已经能理解语言，能续写文本，但回答问题常常胡说八道，甚至不遵守指令。\n于是，他们在 GPT-3 的基础上，用几万条人工编写的「指令-回答」数据进行监督微调（SFT）。\n这让模型开始能理解“问题的上下文”和“人类期望的回答方式”。\n后来又加上强化学习（RLHF）——让人类对回答进行打分，再进一步调整模型的输出偏好。\n经过这一系列步骤，才有了现在你熟悉的 ChatGPT，它不仅懂语言，还能对话、分析、推理、保持礼貌、避免不当内容。\n所以，当你问“我们不是已经训练了吗？为什么还要微调？” 答案是：预训练教模型懂语言，而微调教模型做任务。 预训练相当于给模型“认知能力”，微调是给它“行为准则”。\n没有预训练，模型不懂语言，根本无法对输入做出有意义的反应；\n没有微调，它虽然懂语言，却无法理解人类的意图，也不会照你的要求去做。\n预训练是打地基，让模型拥有语言智能；\n微调是装修，让它具备实际用途。\n这里大家可以联想一下做CV任务，是不是有很多骨干都是在ImageNet数据集上进行了训练？，这样是不是可以理解了。大家有兴趣可以去看看GPT1这篇报道，里面就很详细的写了，GPT任务是怎么来的，也是现在的大模型的鼻祖。\n数据集 # 我们使用的是BelleGroup/train_3.5M_CN。链接：\nBelleGroup/train_3.5M_CN 这个数据集，说白了就是一大堆中英文聊天记录，主要是“人问一句、AI 回一句”这样的指令对话，用来教模型怎么听懂人话、怎么按要求回答。里面一共有大概三百多万条这样的对话，每条都是 JSON 格式的，内容一般是一个用户提问（标记成 human）和一个模型回答（标记成 assistant），有的只有一问一答，有的能聊上好几百轮。\n它被广泛用来做 监督微调（SFT），也就是在模型预训练之后，用这些有“问题—答案”配对的数据再教模型“怎么好好说话、怎么听指令”。不过它的数据来源比较杂，没说清楚是人工写的还是机器生成的，质量参差不齐，可能有重复、废话或者不太准确的内容，而且它的授权是 GPL-3.0，这种许可证在商用时要特别注意合规问题。实际使用前，一般要先清洗过滤、分出验证集，再根据任务需要挑选或裁剪数据。简单讲，这个数据集就是一个能让大模型学会中文对话和执行指令的“大型练习册”，但用之前最好先打磨干净再训练。\n那么这个数据集到底是怎么样的呢？到底长什么样子。\n我们将数据下载下来，大概是5G的数据集。我们仍然是使用之前我们写的读取大文件的预览代码，将文件读取一下，看看内部具体是什么样子的：\n代码地址：preview_jsonl.py\nimport json file_path = \u0026#34;/root/autodl-tmp/Dataset/BelleGroup/train_3.5M_CN.json\u0026#34; k = 1000 def stream_json_objects(file_path, max_items=1000): with open(file_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: buf = [] depth = 0 in_string = False escape = False count = 0 while True: ch = f.read(1) if not ch: break buf.append(ch) if escape: escape = False continue if ch == \u0026#34;\\\\\u0026#34;: escape = True continue if ch == \u0026#39;\u0026#34;\u0026#39;: in_string = not in_string continue if not in_string: if ch == \u0026#39;{\u0026#39;: depth += 1 elif ch == \u0026#39;}\u0026#39;: depth -= 1 if depth == 0: raw = \u0026#39;\u0026#39;.join(buf) buf = [] try: obj = json.loads(raw) yield obj count += 1 if count \u0026gt;= max_items: return except json.JSONDecodeError: print(\u0026#34;解析失败：\u0026#34;, raw[:100]) # 使用 for item in stream_json_objects(file_path, k): print(item) 但是，这个数据格式不是我们想要的，我们需要写一个代码转换一下。那么我们想要一个什么格式？\n对于原始的数据集来说：\n它是长什么样子呢？\n{ \u0026#34;id\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;conversations\u0026#34;: [ {\u0026#34;from\u0026#34;: \u0026#34;human\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;问题文本\u0026#34;}, {\u0026#34;from\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;回答文本\u0026#34;} ] } 那么我们需要什么样子的呢？\n[ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个AI助手\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;...\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;...\u0026#34;}, ... ] 我们通过代码来处理一下：\nimport json from tqdm import tqdm sft_data = \u0026#39;/root/autodl-tmp/Dataset/BelleGroup/train_3.5M_CN.json\u0026#39; output_sft_data = \u0026#39;/root/autodl-tmp/Dataset/BelleGroup/BelleGroup_sft.jsonl\u0026#39; def convert_message(data): *\u0026#34;\u0026#34;\u0026#34;* * 将原始数据转换为标准格式* * \u0026#34;\u0026#34;\u0026#34;* * *message = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个AI助手\u0026#34;}, ] for item in data: if item[\u0026#39;from\u0026#39;] == \u0026#39;human\u0026#39;: message.append({\u0026#39;role\u0026#39;: \u0026#39;user\u0026#39;, \u0026#39;content\u0026#39;: item[\u0026#39;value\u0026#39;]}) elif item[\u0026#39;from\u0026#39;] == \u0026#39;assistant\u0026#39;: message.append({\u0026#39;role\u0026#39;: \u0026#39;assistant\u0026#39;, \u0026#39;content\u0026#39;: item[\u0026#39;value\u0026#39;]}) return message with open(output_sft_data, \u0026#39;a\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as sft: with open(sft_data, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: data = f.readlines() for item in tqdm(data, desc=\u0026#34;Processing\u0026#34;, unit=\u0026#34;lines\u0026#34;): item = json.loads(item) message = convert_message(item[\u0026#39;conversations\u0026#39;]) sft.write(json.dumps(message, ensure_ascii=False) + \u0026#39;\\n\u0026#39;) 我们可以再次的预览一下：\n这个就是我们想要的。\n数据加载器 # 现在我们是不是得写一个数据加载器。和我们预训练一样。\n代码地址：SFTMapDataset.py\nimport json import torch from torch.utils.data import Dataset class SFTMapDataset(Dataset): *\u0026#34;\u0026#34;\u0026#34;* * 高效 map-style SFT Dataset* * 支持 role/content 格式数据，X/Y/loss_mask 对齐* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, data_path, tokenizer, max_length=256, padding=0): super().__init__() self.data_path = data_path self.tokenizer = tokenizer self.max_length = max_length self.padding = padding # 构建文件行偏移表 self.offsets = [] cur = 0 with open(data_path, \u0026#34;rb\u0026#34;) as f: for line in f: self.offsets.append(cur) cur += len(line) self._len = len(self.offsets) self._fp = None def __len__(self): return self._len def _ensure_file_open(self): if self._fp is None: self._fp = open(self.data_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) def _read_line(self, index): self._ensure_file_open() self._fp.seek(self.offsets[index]) return self._fp.readline().rstrip(\u0026#34;\\n\u0026#34;) def generate_loss_mask(self, input_ids): # 生成 loss mask, 0 表示不计算损失, 1 表示计算损失 mask = [0] * len(input_ids) a_sequence = self.tokenizer(\u0026#34;\u0026lt;|im_start|\u0026gt;assistant\\n\u0026#34;)[\u0026#39;input_ids\u0026#39;] # \u0026lt;|im_start|\u0026gt;assistant\\n a_length = len(a_sequence) n = len(input_ids) i = 0 while i \u0026lt;= n - a_length: # 检查当前位置是否匹配目标子序列 match = True for k in range(a_length): if input_ids[i + k] != a_sequence[k]: match = False break if match: # 从子序列结束的位置开始查找第一个 4 (eos_token_id) j = None for idx in range(i + a_length, n): if input_ids[idx] == self.tokenizer.eos_token_id: j = idx break if j is not None: start = i + a_length end = j # 结束位置设为j（包含4） # 标记区间为1（包括start到end） if start \u0026lt;= end: for pos in range(start, end + 1): if pos \u0026lt; len(mask): mask[pos] = 1 # 跳过当前子序列，避免重叠匹配 i += a_length else: i += 1 return mask def __getitem__(self, index): line = self._read_line(index) sample = json.loads(line) text = self.tokenizer.apply_chat_template(sample, tokenize=False, add_generation_prompt=False) input_ids = self.tokenizer(text).data[\u0026#39;input_ids\u0026#39;][:self.max_length] # padding pad_len = self.max_length - len(input_ids) if pad_len \u0026gt; 0: input_ids += [self.padding] * pad_len loss_mask = self.generate_loss_mask(input_ids) # X/Y/loss_mask 对齐 X = torch.tensor(input_ids[:-1], dtype=torch.long) Y = torch.tensor(input_ids[1:], dtype=torch.long) loss_mask = torch.tensor(loss_mask[1:], dtype=torch.long) return X, Y, loss_mask def __del__(self): if hasattr(self, \u0026#34;_fp\u0026#34;) and self._fp is not None: try: self._fp.close() except: pass # ===== 测试 ===== if __name__ == \u0026#34;__main__\u0026#34;: from torch.utils.data import DataLoader from transformers import AutoTokenizer import os test_file = \u0026#34;test_sft.jsonl\u0026#34; sample_data = [[ {\u0026#39;role\u0026#39;: \u0026#39;system\u0026#39;, \u0026#39;content\u0026#39;: \u0026#39;你是一个AI助手\u0026#39;}, {\u0026#39;role\u0026#39;: \u0026#39;user\u0026#39;, \u0026#39;content\u0026#39;: \u0026#39;根据以下文本，对此事件进行分类：中国队在足球比赛中赢得了冠军。\u0026#39;}, {\u0026#39;role\u0026#39;: \u0026#39;assistant\u0026#39;, \u0026#39;content\u0026#39;: \u0026#39;这个事件可以被分类为体育比赛。具体地，中国队在足球比赛中致胜并赢得了冠军。\u0026#39;} ]] with open(test_file, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: for item in sample_data: f.write(json.dumps(item, ensure_ascii=False) + \u0026#34;\\n\u0026#34;) tokenizer = AutoTokenizer.from_pretrained(\u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/tokenizer_k\u0026#34;) dataset = SFTMapDataset(test_file, tokenizer, max_length=128) dataloader = DataLoader(dataset, batch_size=2, shuffle=False) for X, Y, loss_mask in dataloader: print(\u0026#34;input_ids:\\n\u0026#34;, X) print(\u0026#34;labels:\\n\u0026#34;, Y) print(\u0026#34;loss_mask:\\n\u0026#34;, loss_mask) break os.remove(test_file) 这个和我们之前的预训练有什么不一样吗？核心逻辑是什么\n假设输入数据\n[ {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;我爱南巷的花猫\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;我也很喜欢这只猫，它非常可爱。\u0026#34;} ] 构建 Chat 模板\n\u0026lt;|im_start|\u0026gt;user 我爱南巷的花猫 \u0026lt;|im_end|\u0026gt;\u0026lt;|im_start|\u0026gt;assistant 我也很喜欢这只猫，它非常可爱。 \u0026lt;|im_end|\u0026gt; Tokenization\n假设 tokenizer 把文本分成 token IDs（用数字代替）：\n[3, 1001, 1002, 1003, 1004, 4, 3, 2001, 2002, 2003, 2004, 4]\n3 = \u0026lt;|im_start|\u0026gt;\n4 = \u0026lt;|im_end|\u0026gt;\n1001… = 用户输入\n2001… = AI回答\n最大长度 max_length = 12（假设） Padding 不需要，因为刚好满。\n生成 X, Y, loss_mask\nX = 输入序列去掉最后一个 token X = [3, 1001, 1002, 1003, 1004, 4, 3, 2001, 2002, 2003, 2004]\nY = 输入序列去掉第一个 token Y = [1001, 1002, 1003, 1004, 4, 3, 2001, 2002, 2003, 2004, 4]\nloss_mask 通过 generate_loss_mask 生成 generate_loss_mask 逻辑：\n找 \u0026lt;|im_start|\u0026gt;assistant\\n\u0026gt; 的起始位置\n从子序列结束位置到 \u0026lt;|im_end|\u0026gt; (token_id = 4) 之间的 token 标记为 1，其余为 0\n假设 \u0026lt;|im_start|\u0026gt;assistant\\n\u0026gt; 对应 token [3] 开头的第 7 个 token：\nmask = [0,0,0,0,0,0,0,1,1,1,1,1] # 用户部分为0，助手回答部分为1\n去掉第一个 token 与 X/Y 对齐：\nloss_mask = [0,0,0,0,0,0,1,1,1,1,1] # 长度 = len(X) = 11\n最终返回\nX = tensor([3, 1001, 1002, 1003, 1004, 4, 3, 2001, 2002, 2003, 2004]) Y = tensor([1001, 1002, 1003, 1004, 4, 3, 2001, 2002, 2003, 2004, 4]) ``loss_mask = tensor([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1])\n解释：\nX 是模型的输入序列\nY 是预测目标（模型要预测的下一个 token）\nloss_mask 只对助手输出部分计算损失，用户输入部分不参与损失计算\n本质上和我们之前绘制的那个图是一样的，我们可以回顾一下。\n这个loss mask 实现起来也是非常非常简单，直接就：loss * loss_mask\n下面就开始我们的全参微调 # 直接上代码 # 这个怎么说呢，没有任何好讲的，大家发现没有，我们数据构建一模一样，我们全参微调训练方法和预训练方法一模一样！！！\n没错，是一模一样，没有任何需要更改的地方！！！！\n但是我们依然将代码附上！\n代码地址：Code/SFTtrain.py\n# -*- coding: utf-8 -*- import os import argparse import time import math import torch from torch import optim from torch.utils.data import DataLoader, DistributedSampler import torch.distributed as dist from contextlib import nullcontext from transformers import AutoTokenizer from ModelConfig import ModelConfig from LLaMA2 import Transformer from SFTMapDataset import SFTMapDataset import swanlab # ------------------- 工具函数 ------------------- def Logger(msg: str, rank=0): *\u0026#34;\u0026#34;\u0026#34;* * 多卡训练中只在主卡打印日志* * rank=0 表示主进程* * \u0026#34;\u0026#34;\u0026#34;* * *if rank == 0: print(msg) def get_lr(it, total_iters, args): *\u0026#34;\u0026#34;\u0026#34;* * 计算学习率：* * - 线性预热阶段：从0线性增长到目标学习率* * - 余弦退火阶段：按余弦衰减到最小学习率* * - 超过总迭代次数：保持最小学习率* * \u0026#34;\u0026#34;\u0026#34;* * *warmup_iters = args.warmup_iters min_lr = args.learning_rate / 10 if it \u0026lt; warmup_iters: return args.learning_rate * it / warmup_iters elif it \u0026gt; total_iters: return min_lr else: decay_ratio = (it - warmup_iters) / (total_iters - warmup_iters) coeff = 0.5 * (1 + math.cos(math.pi * decay_ratio)) return min_lr + coeff * (args.learning_rate - min_lr) def save_model(model, save_dir, step, lm_config, rank=0): *\u0026#34;\u0026#34;\u0026#34;* * 保存模型：* * - DDP 多卡训练时，只让主卡(rank=0)保存模型* * - 处理 DataParallel 或 DDP 包装的模型* * \u0026#34;\u0026#34;\u0026#34;* * *if rank == 0: os.makedirs(save_dir, exist_ok=True) state_dict = model.module.state_dict() if hasattr(model, \u0026#39;module\u0026#39;) else model.state_dict() path = f\u0026#34;{save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step + 1}.pth\u0026#34; torch.save(state_dict, path) Logger(f\u0026#34;模型保存: {path}\u0026#34;, rank) # ------------------- 模型初始化 ------------------- def init_model(args, lm_config, rank, gpu_id): *\u0026#34;\u0026#34;\u0026#34;* * 初始化模型 + tokenizer + DDP 包装* * \u0026#34;\u0026#34;\u0026#34;* * *def count_parameters(model): *\u0026#34;\u0026#34;\u0026#34;计算可训练参数量\u0026#34;\u0026#34;\u0026#34;* * *return sum(p.numel() for p in model.parameters() if p.requires_grad) # 加载 tokenizer tokenizer = AutoTokenizer.from_pretrained(\u0026#39;/root/StudyLLM/HappyLLM/Tokenizer/tokenizer_k\u0026#39;) # 创建 Transformer 模型并移动到当前 GPU model = Transformer(lm_config).to(args.device) # 加载预训练权重 ckp = \u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/ModelZoom/base-CodeLab-50M/pretrain_512_16_6144_step67200.pth\u0026#39; state_dict = torch.load(ckp, map_location=args.device) unwanted_prefix = \u0026#39;_orig_mod.\u0026#39; for k, v in list(state_dict.items()): if k.startswith(unwanted_prefix): state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k) model.load_state_dict(state_dict, strict=False) # DDP 包装模型，每个进程只处理自己对应的 GPU model = torch.nn.parallel.DistributedDataParallel( model, device_ids=[gpu_id], output_device=gpu_id, ) Logger(f\u0026#39;LLM总参数量: {count_parameters(model) / 1e6:.3f} M\u0026#39;, rank) return model, tokenizer # 训练单个epoch，其实对于大模型来说，基本都是只会训练一次，因为这个数据量实在是非常非常庞大 def train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, rank): *\u0026#34;\u0026#34;\u0026#34;* * 训练一个 epoch* * - 支持梯度累积* * - 支持混合精度* * - 支持 DDP* * \u0026#34;\u0026#34;\u0026#34;* * *# 记录开始时间 start_time = time.time() for step, (X, Y, Attention_mask) in enumerate(train_loader): # 将数据迁移到显卡 X, Y, Attention_mask = X.to(args.device), Y.to(args.device), Attention_mask.to(args.device) # 一个简易的学习率调度器 lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args) for param_group in optimizer.param_groups: param_group[\u0026#39;lr\u0026#39;] = lr # 使用混合精度前向传播 with ctx: out = model(X, Y) # 模型前向 # 除以梯度累积步数 loss = out.last_loss / args.accumulation_steps # 展平 mask loss_mask_flat = Attention_mask.view(-1) # 忽略 padding loss = torch.sum(loss * loss_mask_flat) / loss_mask_flat.sum() # 反向传播获取梯度 scaler.scale(loss).backward() # 进行梯度更新 if (step + 1) % args.accumulation_steps == 0: scaler.unscale_(optimizer) # 梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) # 优化器更新 scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) # ------------------- 日志 ------------------- if step % args.log_interval == 0: elapsed_time = time.time() - start_time Logger( f\u0026#34;Rank[{rank}] Epoch[{epoch + 1}/{args.epochs}] \u0026#34; f\u0026#34;Step[{step}/{iter_per_epoch}] \u0026#34; f\u0026#34;Loss:{loss.item() * args.accumulation_steps:.4f} \u0026#34; f\u0026#34;LR:{lr:.7f} Elapsed:{elapsed_time:.1f}s\u0026#34;, rank ) if args.use_swanlab and rank == 0: swanlab.log({\u0026#34;loss\u0026#34;: loss.item() * args.accumulation_steps, \u0026#34;lr\u0026#34;: lr}) # ------------------- 模型保存 ------------------- if (step + 1) % args.save_interval == 0: save_model(model, args.save_dir, step, lm_config, rank) # ------------------- 主函数 ------------------- if __name__ == \u0026#34;__main__\u0026#34;: parser = argparse.ArgumentParser() # ------------------- 基础训练参数 ------------------- parser.add_argument(\u0026#34;--out_dir\u0026#34;, type=str, default=\u0026#34;../ModelZoom/SFT-CodeLab-50M\u0026#34;, help=\u0026#34;模型输出目录\u0026#34;) parser.add_argument(\u0026#34;--epochs\u0026#34;, type=int, default=1, help=\u0026#34;训练轮数\u0026#34;) parser.add_argument(\u0026#34;--batch_size\u0026#34;, type=int, default=64, help=\u0026#34;模型训练批次大小\u0026#34;) parser.add_argument(\u0026#34;--learning_rate\u0026#34;, type=float, default=2e-4, help=\u0026#34;训练的学习率\u0026#34;) parser.add_argument(\u0026#34;--device\u0026#34;, type=str, default=\u0026#34;cuda:0\u0026#34; if torch.cuda.is_available() else \u0026#34;cpu\u0026#34;, help=\u0026#34;训练设备\u0026#34;) parser.add_argument(\u0026#34;--dtype\u0026#34;, type=str, default=\u0026#34;bfloat16\u0026#34;) parser.add_argument(\u0026#34;--use_swanlab\u0026#34;, action=\u0026#34;store_true\u0026#34;, default=True, help=\u0026#34;是否使用SwanLab进行实验跟踪\u0026#34;) parser.add_argument(\u0026#34;--num_workers\u0026#34;, type=int, default=8, help=\u0026#34;数据加载的工作进程数\u0026#34;) parser.add_argument(\u0026#34;--data_path\u0026#34;, type=str, default=\u0026#34;/root/autodl-tmp/Dataset/BelleGroup/BelleGroup_sft.jsonl\u0026#34;, help=\u0026#34;训练数据路径\u0026#34;) parser.add_argument(\u0026#34;--accumulation_steps\u0026#34;, type=int, default=8, help=\u0026#34;梯度累积步数\u0026#34;) parser.add_argument(\u0026#34;--grad_clip\u0026#34;, type=float, default=1.0, help=\u0026#34;梯度裁剪阈值\u0026#34;) parser.add_argument(\u0026#34;--warmup_iters\u0026#34;, type=int, default=0, help=\u0026#34;学习率预热迭代次数\u0026#34;) parser.add_argument(\u0026#34;--log_interval\u0026#34;, type=int, default=100, help=\u0026#34;日志记录间隔\u0026#34;) parser.add_argument(\u0026#34;--save_interval\u0026#34;, type=int, default=1200, help=\u0026#34;模型保存间隔\u0026#34;) # DDP 本地 rank parser.add_argument(\u0026#34;--local_rank\u0026#34;, type=int, default=0, help=\u0026#34;DDP local rank\u0026#34;) args = parser.parse_args() # ------------------- 初始化 DDP \u0026amp; 自动适配 GPU ------------------- dist.init_process_group(backend=\u0026#39;nccl\u0026#39;) local_rank = int(os.environ.get(\u0026#34;LOCAL_RANK\u0026#34;, 0)) gpu_id = local_rank torch.cuda.set_device(gpu_id) args.device = f\u0026#39;cuda:{gpu_id}\u0026#39; # 获取总 GPU 数量 ngpus = torch.cuda.device_count() # 检查 local_rank 是否超出可用 GPU 范围 if gpu_id \u0026gt;= ngpus: raise RuntimeError(f\u0026#34;local_rank {gpu_id} 超出可用 GPU 范围 (0-{ngpus - 1})\u0026#34;) Logger(f\u0026#34;[Rank {args.local_rank}] 使用 GPU {gpu_id} / 总 GPU 数量 {ngpus}\u0026#34;, args.local_rank) # ------------------- SwanLab ------------------- if args.use_swanlab and args.local_rank == 0: swanlab.login(api_key=\u0026#34;你的 API key\u0026#34;) swanlab.init(project=\u0026#34;CodeLab-LLM\u0026#34;, experiment_name=\u0026#34;SFT-50M\u0026#34;, config=args) # ------------------- 模型配置 ------------------- lm_config = ModelConfig(dim=512, n_layers=16) max_seq_len = lm_config.max_seq_len args.save_dir = os.path.join(args.out_dir) os.makedirs(args.out_dir, exist_ok=True) torch.manual_seed(42) # ------------------- 混合精度上下文 ------------------- ctx = nullcontext() if \u0026#34;cpu\u0026#34; in args.device else torch.amp.autocast(device_type=\u0026#34;cuda\u0026#34;, dtype=torch.bfloat16) # ------------------- 模型 \u0026amp; 数据 ------------------- model, tokenizer = init_model(args, lm_config, args.local_rank, gpu_id) train_ds = SFTMapDataset(args.data_path, tokenizer, max_length=max_seq_len) # DDP 必须使用 DistributedSampler train_sampler = DistributedSampler(train_ds) train_loader = DataLoader( train_ds, batch_size=args.batch_size, sampler=train_sampler, num_workers=args.num_workers, pin_memory=True ) # ------------------- 优化器 \u0026amp; 混合精度 scaler ------------------- scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in [\u0026#39;float16\u0026#39;, \u0026#39;bfloat16\u0026#39;])) optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate) # ------------------- 开始训练 ------------------- iter_per_epoch = len(train_loader) for epoch in range(args.epochs): train_sampler.set_epoch(epoch) # DDP 每轮必须重置 epoch 保证 shuffle train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, args.local_rank) if args.local_rank == 0: Logger(\u0026#34;训练完成！\u0026#34;) dist.destroy_process_group() 我们可以看见，模型正常收敛，说明是有效果的。\n基于LORA的微调 # LORA详解 # 当然可以！我们用矩阵方式一步步解释 LoRA 的思想和来源，这样会更直观。下面我把原理完全矩阵化。\n原始线性层 # 假设我们有一个线性层：\n$ y = W_0 x$\n$x \\in \\mathbb{R}^{k \\times 1}$ ——输入向量\n$W_0 \\in \\mathbb{R}^{d \\times k}$ ——原始权重矩阵\n$y \\in \\mathbb{R}^{d \\times 1}$ ——输出向量\n例如：\n输出：\nLoRA 的低秩增量矩阵 # LoRA 假设我们只需要对 $W_0$做 低秩调整：\n$ \\Delta W = A B$\n$A \\in \\mathbb{R}^{d \\times r}$\n$B \\in \\mathbb{R}^{r \\times k}$\n$r \\ll d, k$ （低秩）\n矩阵维度示意：\n$ \\underbrace{W_0}_{d \\times k} + \\underbrace{A}_{d \\times r} \\cdot \\underbrace{B}_{r \\times k} = \\underbrace{W}_{d \\times k}$\n输出：\n$ y = W x = W_0 x + (A B) x = W_0 x + A (B x)$\n矩阵内的直观解释 # 假设：\n$d = 4, k = 4, r = 2$ 计算：\n先计算 (B x) **再计算 **$A (B x)$ 这样，低秩矩阵完成了原矩阵大小的调整，但只需训练$d \\cdot r + r \\cdot k = 16$** 个参数，而不是 (16) 个参数**（示例很小，实际模型差异更大）。\n矩阵化的训练思路 # 训练 LoRA 的矩阵目标是最小化损失$L(y_\\text{pred}, y_\\text{true})$：\n$ \\min_{A,B} L\\Big( (W_0 + AB)x, y_\\text{true} \\Big)$\n固定 (W_0)，只优化 (A) 和 (B)\n训练参数量小，计算量小\n输出仍然是原模型维度 $d \\times 1$\nLoRA 的直观矩阵理解 # 原矩阵 (W_0) ：已经学会通用知识\n低秩矩阵 (AB) ：学习“任务特定方向”，在一个小低维子空间里调整输出\n输出叠加：\n$y = W_0 x + A (B x)$\n(B) 将输入 (x) 投影到低维空间\n(A) 将低维空间映射回输出维度\n低秩矩阵就像给模型加了一个“小补丁”\n我们使用peft工具\npip install peft 这个工具只需要对原先的代码进行很小的改动就行。\n1、包装模型 # from peft import LoraConfig, get_peft_model, TaskType # LoRA 配置 lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, # 自回归语言模型 r=8, # LoRA rank lora_alpha=16, # LoRA alpha lora_dropout=0.1, # Dropout target_modules=[\u0026#34;q_proj\u0026#34;, \u0026#34;v_proj\u0026#34;] # 哪些权重使用 LoRA ) # 包装模型 model = get_peft_model(model, lora_config) 2、优化器修改 # optimizer = optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=args.learning_rate ) 3、保存与加载 LoRA 权重 # 保存 LoRA 权重： from peft import PeftModel if rank == 0: model.save_pretrained(args.save_dir) 加载 LoRA 权重： from peft import PeftModel base_model = Transformer(lm_config).to(args.device) model = PeftModel.from_pretrained(base_model, lora_path) 下面就将我们的训练代码改一下，改为基于我们的方法进行LORA微调\n# -*- coding: utf-8 -*- import os import argparse import time import math import torch from torch import optim from torch.utils.data import DataLoader, DistributedSampler import torch.distributed as dist from contextlib import nullcontext from transformers import AutoTokenizer from ModelConfig import ModelConfig from LLaMA2 import Transformer from SFTMapDataset import SFTMapDataset from peft import LoraConfig, get_peft_model, TaskType import swanlab # ------------------- 工具函数 ------------------- def Logger(msg: str, rank=0): if rank == 0: print(msg) def get_lr(it, total_iters, args): warmup_iters = args.warmup_iters min_lr = args.learning_rate / 10 if it \u0026lt; warmup_iters: return args.learning_rate * it / warmup_iters elif it \u0026gt; total_iters: return min_lr else: decay_ratio = (it - warmup_iters) / (total_iters - warmup_iters) coeff = 0.5 * (1 + math.cos(math.pi * decay_ratio)) return min_lr + coeff * (args.learning_rate - min_lr) def save_model(model, save_dir, step, lm_config, rank=0): *\u0026#34;\u0026#34;\u0026#34;* * 保存 LoRA 微调模型（兼容 DDP 和多卡）* * 文件名和原始预训练保存一致：* * pretrain_{dim}_{n_layers}_{vocab_size}_step{step}.pth* * Args:* * model: 当前模型，可能是 DDP 包装的* * save_dir: 保存目录* * step: 当前训练步数* * lm_config: 模型配置（dim, n_layers, vocab_size）* * rank: 当前进程 rank，只让主卡保存* * \u0026#34;\u0026#34;\u0026#34;* * *if rank != 0: return # 只有主卡保存 os.makedirs(save_dir, exist_ok=True) # 如果是 DDP 包装的模型，需要取 module model_to_save = model.module if hasattr(model, \u0026#39;module\u0026#39;) else model # 构建完整的保存路径 path = os.path.join( save_dir, f\u0026#34;pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step + 1}\u0026#34; ) # 使用 HuggingFace 的 save_pretrained 保存 LoRA 权重 model_to_save.save_pretrained(path) Logger(f\u0026#34;LoRA 模型已保存: {path}\u0026#34;, rank) # ------------------- 模型初始化 ------------------- def init_model(args, lm_config, rank, gpu_id): def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) tokenizer = AutoTokenizer.from_pretrained(\u0026#39;/root/StudyLLM/HappyLLM/Tokenizer/tokenizer_k\u0026#39;) model = Transformer(lm_config).to(args.device) # 加载原始预训练权重 ckp = \u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/ModelZoom/base-CodeLab-50M/pretrain_512_16_6144_step67200.pth\u0026#39; state_dict = torch.load(ckp, map_location=args.device) unwanted_prefix = \u0026#39;_orig_mod.\u0026#39; for k, v in list(state_dict.items()): if k.startswith(unwanted_prefix): state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k) model.load_state_dict(state_dict, strict=False) # ------------------- LoRA 包装 ------------------- lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=4, lora_alpha=16, lora_dropout=0.1, target_modules=[\u0026#34;wq\u0026#34;, \u0026#34;wv\u0026#34;], # 根据 Transformer 实现选择 ) model = get_peft_model(model, lora_config) # DDP 包装 model = torch.nn.parallel.DistributedDataParallel( model, device_ids=[gpu_id], output_device=gpu_id, ) Logger(f\u0026#39;LLM 可训练参数量: {count_parameters(model)/1e6:.3f} M\u0026#39;, rank) return model, tokenizer # ------------------- 训练 ------------------- def train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, rank): start_time = time.time() for step, (X, Y, Attention_mask) in enumerate(train_loader): X, Y, Attention_mask = X.to(args.device), Y.to(args.device), Attention_mask.to(args.device) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args) for param_group in optimizer.param_groups: param_group[\u0026#39;lr\u0026#39;] = lr with ctx: out = model(X, Y) loss = out.last_loss / args.accumulation_steps loss_mask_flat = Attention_mask.view(-1) loss = torch.sum(loss * loss_mask_flat) / loss_mask_flat.sum() scaler.scale(loss).backward() if (step + 1) % args.accumulation_steps == 0: scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) if step % args.log_interval == 0: elapsed_time = time.time() - start_time Logger( f\u0026#34;Rank[{rank}] Epoch[{epoch+1}/{args.epochs}] \u0026#34; f\u0026#34;Step[{step}/{iter_per_epoch}] \u0026#34; f\u0026#34;Loss:{loss.item()*args.accumulation_steps:.4f} \u0026#34; f\u0026#34;LR:{lr:.7f} Elapsed:{elapsed_time:.1f}s\u0026#34;, rank ) if args.use_swanlab and rank == 0: swanlab.log({\u0026#34;loss\u0026#34;: loss.item()*args.accumulation_steps, \u0026#34;lr\u0026#34;: lr}) if (step + 1) % args.save_interval == 0: save_model(model, args.save_dir, step, lm_config, rank) # ------------------- 主函数 ------------------- if __name__ == \u0026#34;__main__\u0026#34;: parser = argparse.ArgumentParser() parser.add_argument(\u0026#34;--out_dir\u0026#34;, type=str, default=\u0026#34;../ModelZoom/SFT-CodeLab-50M-LORA\u0026#34;) parser.add_argument(\u0026#34;--epochs\u0026#34;, type=int, default=2) parser.add_argument(\u0026#34;--batch_size\u0026#34;, type=int, default=64) parser.add_argument(\u0026#34;--learning_rate\u0026#34;, type=float, default=2e-4) parser.add_argument(\u0026#34;--device\u0026#34;, type=str, default=\u0026#34;cuda:0\u0026#34;) parser.add_argument(\u0026#34;--dtype\u0026#34;, type=str, default=\u0026#34;bfloat16\u0026#34;) parser.add_argument(\u0026#34;--use_swanlab\u0026#34;, action=\u0026#34;store_true\u0026#34;, default=True) parser.add_argument(\u0026#34;--num_workers\u0026#34;, type=int, default=8) parser.add_argument(\u0026#34;--data_path\u0026#34;, type=str, default=\u0026#34;/root/autodl-tmp/Dataset/BelleGroup/BelleGroup_sft.jsonl\u0026#34;) parser.add_argument(\u0026#34;--accumulation_steps\u0026#34;, type=int, default=8) parser.add_argument(\u0026#34;--grad_clip\u0026#34;, type=float, default=1.0) parser.add_argument(\u0026#34;--warmup_iters\u0026#34;, type=int, default=0) parser.add_argument(\u0026#34;--log_interval\u0026#34;, type=int, default=100) parser.add_argument(\u0026#34;--save_interval\u0026#34;, type=int, default=1200) parser.add_argument(\u0026#34;--local_rank\u0026#34;, type=int, default=0) args = parser.parse_args() # DDP 初始化 dist.init_process_group(backend=\u0026#39;nccl\u0026#39;) local_rank = int(os.environ.get(\u0026#34;LOCAL_RANK\u0026#34;, 0)) gpu_id = local_rank torch.cuda.set_device(gpu_id) args.device = f\u0026#39;cuda:{gpu_id}\u0026#39; ngpus = torch.cuda.device_count() if gpu_id \u0026gt;= ngpus: raise RuntimeError(f\u0026#34;local_rank {gpu_id} 超出可用 GPU 范围 (0-{ngpus-1})\u0026#34;) Logger(f\u0026#34;[Rank {args.local_rank}] 使用 GPU {gpu_id} / 总 GPU 数量 {ngpus}\u0026#34;, args.local_rank) if args.use_swanlab and args.local_rank == 0: swanlab.login(api_key=\u0026#34;你的API KEY\u0026#34;) swanlab.init(project=\u0026#34;CodeLab-LLM\u0026#34;, experiment_name=\u0026#34;SFT-50M-LORA\u0026#34;, config=args) lm_config = ModelConfig(dim=512, n_layers=16) max_seq_len = lm_config.max_seq_len args.save_dir = os.path.join(args.out_dir) os.makedirs(args.out_dir, exist_ok=True) torch.manual_seed(42) ctx = nullcontext() if \u0026#34;cpu\u0026#34; in args.device else torch.amp.autocast(device_type=\u0026#34;cuda\u0026#34;, dtype=torch.bfloat16) model, tokenizer = init_model(args, lm_config, args.local_rank, gpu_id) train_ds = SFTMapDataset(args.data_path, tokenizer, max_length=max_seq_len) train_sampler = DistributedSampler(train_ds) train_loader = DataLoader( train_ds, batch_size=args.batch_size, sampler=train_sampler, num_workers=args.num_workers, pin_memory=True ) scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in [\u0026#39;float16\u0026#39;, \u0026#39;bfloat16\u0026#39;])) optimizer = optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), # 只训练 LoRA 参数 lr=args.learning_rate ) iter_per_epoch = len(train_loader) for epoch in range(args.epochs): train_sampler.set_epoch(epoch) train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, args.local_rank) if args.local_rank == 0: Logger(\u0026#34;LoRA 微调完成！\u0026#34;) dist.destroy_process_group() 在微调完成后，我们可以写一个简单的控制台交互聊天的代码，进行对话。\nfrom contextlib import nullcontext import torch from transformers import AutoTokenizer from peft import PeftModel from ModelConfig import ModelConfig from LLaMA2 import Transformer import torch.nn.functional as F class TextGenerator: def __init__(self, base_model_path=\u0026#39;\u0026#39;, # 基础模型 checkpoint，可选 lora_checkpoint=\u0026#39;\u0026#39;, # LoRA checkpoint，可选 tokenizer_model_path=\u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/tokenizer_k\u0026#39;, seed=42, device=None, dtype=\u0026#34;bfloat16\u0026#34;): self.device = device or (\u0026#39;cuda:0\u0026#39; if torch.cuda.is_available() else \u0026#39;cpu\u0026#39;) self.device_type = \u0026#39;cuda\u0026#39; if \u0026#39;cuda\u0026#39; in self.device else \u0026#39;cpu\u0026#39; self.dtype = dtype self.seed = seed torch.manual_seed(seed) if \u0026#39;cuda\u0026#39; in self.device: torch.cuda.manual_seed(seed) torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True ptdtype = {\u0026#39;float32\u0026#39;: torch.float32, \u0026#39;bfloat16\u0026#39;: torch.bfloat16, \u0026#39;float16\u0026#39;: torch.float16}[self.dtype] self.ctx = nullcontext() if self.device_type == \u0026#39;cpu\u0026#39; else torch.amp.autocast( device_type=self.device_type, dtype=ptdtype ) # ----------------------------- # 加载基础模型 # ----------------------------- print(\u0026#34;加载基础模型...\u0026#34;) self.model = Transformer(ModelConfig(dim=512, n_layers=16)) if base_model_path: checkpoint_dict = torch.load(base_model_path, map_location=\u0026#39;cpu\u0026#39;) self.model.load_state_dict(checkpoint_dict, strict=False) # ----------------------------- # 加载 LoRA 权重 # ----------------------------- if lora_checkpoint: print(\u0026#34;加载 LoRA 权重...\u0026#34;) self.model = PeftModel.from_pretrained(self.model, lora_checkpoint, device_map={\u0026#39;\u0026#39;: self.device}) self.model.eval() self.model.to(self.device) # ----------------------------- # 分词器 # ----------------------------- self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_model_path) # ----------------------------- # 打印参数量 # ----------------------------- num_params = sum(p.numel() for n, p in self.model.named_parameters() if p.requires_grad) num_lora_params = sum(p.numel() for n, p in self.model.named_parameters() if \u0026#39;lora\u0026#39; in n) print(f\u0026#34;总可训练参数量: {num_params / 1e6:.6f} M\u0026#34;) print(f\u0026#34;LoRA 参数量: {num_lora_params / 1e6:.6f} M\u0026#34;) # ----------------------------- # 简单 chat 模板 # ----------------------------- def chat_template(self, prompt): message = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个AI助手，你的名字叫小明。\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: prompt} ] return self.tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=True) # ----------------------------- # 逐步生成函数 # ----------------------------- def sft_sample(self, start=\u0026#34;Hello!\u0026#34;, num_samples=1, max_new_tokens=128, temperature=0.7, top_k=50, stop_token_id=None): *\u0026#34;\u0026#34;\u0026#34;* * 基于模型 forward 逐步生成 token* * \u0026#34;\u0026#34;\u0026#34;* * *start_text = self.chat_template(start) start_ids = self.tokenizer(start_text).data[\u0026#39;input_ids\u0026#39;] idx = torch.tensor(start_ids, dtype=torch.long, device=self.device)[None, ...] # [1, seq_len] stop_token_id = stop_token_id or self.tokenizer.eos_token_id outputs = [] with torch.no_grad(): with self.ctx: for _ in range(num_samples): cur_idx = idx.clone() generated = [] for _ in range(max_new_tokens): logits = self.model(cur_idx).logits[:, -1, :] # [batch, vocab] logits = logits / temperature if top_k is not None: v, _ = torch.topk(logits, min(top_k, logits.size(-1))) logits[logits \u0026lt; v[:, [-1]]] = -float(\u0026#39;Inf\u0026#39;) probs = F.softmax(logits, dim=-1) next_token = torch.multinomial(probs, num_samples=1) # [batch, 1] if next_token.item() == stop_token_id: break generated.append(next_token.item()) cur_idx = torch.cat([cur_idx, next_token], dim=1) outputs.append(self.tokenizer.decode(generated)) return outputs # ----------------------------- # 控制台聊天函数 # ----------------------------- def chat_console(self): print(\u0026#34;输入 \u0026#39;exit\u0026#39; 退出对话。\u0026#34;) while True: user_input = input(\u0026#34;你: \u0026#34;).strip() if user_input.lower() in [\u0026#39;exit\u0026#39;, \u0026#39;quit\u0026#39;]: break answers = self.sft_sample(start=user_input, num_samples=1, max_new_tokens=128, temperature=0.8, top_k=50) print(f\u0026#34;小明: {answers[0]}\u0026#34;) # ----------------------------- # 测试 # ----------------------------- if __name__ == \u0026#34;__main__\u0026#34;: generator = TextGenerator( base_model_path=\u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/ModelZoom/base-CodeLab-50M/pretrain_512_16_6144_step67200.pth\u0026#39;, lora_checkpoint=\u0026#39;/root/StudyLLM/NX_LLM/第四章_SFT/ModelZoom/SFT-CodeLab-50M-LORA/pretrain_512_16_6144_step4800\u0026#39;, tokenizer_model_path=\u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/tokenizer_k\u0026#39; ) generator.chat_console() 结果如下：\n是不是有点无厘头。\n确确实实是模型容量太小了。\n等我有很多显卡，我训练一个大的，然后上传模型到仓库里面。\n","date":"2026/06/14","externalUrl":null,"permalink":"/blog/llama2-sft-supervised-finetuning/","section":"日常记录、技术札记与转载收藏。","summary":"系统整理 SFT 监督微调的目标、数据格式、训练方式与代码流程，帮助模型从通用能力走向任务对齐。","title":"LLaMA 2 SFT 监督微调","type":"blog"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/sft/","section":"Tags","summary":"","title":"SFT","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/","section":"Tags","summary":"","title":"大模型","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E7%9B%91%E7%9D%A3%E5%BE%AE%E8%B0%83/","section":"Tags","summary":"","title":"监督微调","type":"tags"},{"content":" 数据集构建 # 我们在前一章节，已经初步的对我们的训练数据进行分块，同时我们也训练好了我们的词表。\n那，数据集要怎么样才能送入模型，进行预训练呢？\n我们可以联想一下做CV任务，这个时候是不是需要将图像读取后，转化为tensor格式就行。是的，我们这个大概的流程也是这样的，我们可以写一个数据加载器。\n那么这个代码应该怎么构建呢？\n原理 # 索引化（offsets）\n思想：文件通常很大，不想一次把所有文本读到内存。遍历一次文件只记录每一行在文件中的字节起点（偏移量），这样以后按索引就能 seek 到那一行读取，而不占用大量内存。\n产物：一个偏移量数组（每行一个数字），和样本总数 N。\n按需读取（worker-safe 文件句柄）\n思想：DataLoader 可能会用多个 worker 进程并行读取数据。每个 worker 应该自己打开文件，避免多个进程共享同一个文件对象引起的问题。\n产物：在当前进程/worker 中打开文件句柄，并根据偏移读取指定行文本。\n文本解析（JSON / raw）\n思想：文件每行可能是 JSON（包含 text 字段）或直接一行原始文本。解析后取到实际文本字符串供分词器使用。\n产物：纯文本字符串（例如 \u0026quot;我喜欢南巷的花猫\u0026quot;）。\n分词/编码（tokenizer）\n思想：把自然语言字符串转为整数 id 序列（模型能理解的输入）。一般调用 tokenizer，但让 tokenizer 不要自动添加 special tokens（为了我们能精确控制 BOS/EOS/PAD 的位置）。\n产物：一个整数序列 ids = [id_0, id_1, ...]（长度可变）。\n插入 special token（可选 BOS/EOS）\n思想：为了标记序列开始或结束，手动在 ids 前/后加上 bos_id / eos_id（如果需要）。这样统一了序列的起点语义。\n产物：可能加了 BOS/EOS 的 ids（长度变更）。\n截断与填充（固定长度）\n思想：模型训练通常要求固定的 sequence 长度（或在 batch 内尽量最少 padding）。这里把每个样本截断到 ****max_length，若不足则在末尾用 pad_id 补齐到长度 max_length。\n产物：长度恰好为 max_length 的 id 序列 seq（整型向量）。\n构造自回归输入/标签（shift）\n思想：自回归语言模型的训练目标是「给定前面 tokens，预测下一个 token」。常见做法是把 seq 右移/左移一格得到训练对：\n输入 X = seq[0 : max_length-1]（模型看到的 tokens）\n标签 Y = seq[1 : max_length]（模型要去预测的下一个 token）\n产物：X 和 Y，长度均为 max_length - 1。\nattention mask（告诉模型哪些位置是真实 token）\n思想：基于 seq 中哪些位置是 pad_id 来生成 attention_mask（1 表示有效 token，0 表示 padding）。注意要把 mask 对齐到 X 的长度（通常也是 max_length - 1）。\n产物：attention_mask 与 X 对齐。\n把 padding 的 label 标记为忽略（-100）\n思想：我们不希望模型因为去预测 padding token 的误差而被惩罚，所以把那些对应 padding 的 labels 用特殊值（例如 -100）替换，让损失函数（CrossEntropyLoss(ignore_index=-100)）跳过这些位置。\n产物：处理过的 labels，padding 位置为 -100。\n返回训练样本（字典/张量）\n思想：把 input_ids（X）、labels（Y）、attention_mask 等封装为 tensor 交给 DataLoader/训练循环。 是不是看着挺复杂的，其实大部分都是优化，最核心的东西，我绘制一个图就懂了\nInput ids (seq)：这是完整的 token id 列表（包含 BOS, token, PAD），长度固定为 max_length。它是我们对原始文本做分词、添加特殊 token、并填充/截断后的结果。\nX：模型实际输入，等于 seq 的前 max_length-1 个元素（seq[:-1]）。模型在每个位置根据到当前位置的上下文去预测下一个 token。\nY：目标标签，等于 seq 的后 max_length-1 个元素（seq[1:]）。训练时我们让模型在位置 i 预测 Y[i]。\nLoss mask / attention_mask：\nattention_mask 根据 seq != PAD 生成，1 表示该位置为真实 token（参与 attention），0 表示该位置为 padding（不参与 attention）。\n为了不让 padding 导致损失计算，把对应位置的 labels 设置为 -100（CrossEntropyLoss(ignore_index=-100) 会跳过这些位置）。\n这里我们同时对 labels 做两种忽略：labels[attention_mask == 0] = -100 与 labels[labels == PAD] = -100，得到上面的最终 labels。\nOK，让我们开始\n代码 # 代码地址：Code/PretrainMapDataset.py\nimport os import json from typing import List import torch from torch.utils.data import Dataset, get_worker_info class PretrainMapDataset(Dataset): *\u0026#34;\u0026#34;\u0026#34;* * Map-style 数据集（按行 jsonl），不会一次性加载整个文件。* * 返回：* * tuple: (input_ids, labels, attention_mask)* * - input_ids: (L,) torch.long, 包含 BOS token* * - labels: (L,) torch.long, padding -\u0026gt; -100* * - attention_mask: (L,) torch.long, 1/0* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, path: str, tokenizer, max_length: int = 256): super().__init__() self.path = path self.tokenizer = tokenizer self.max_length = int(max_length) # 强制使用 pad_id=0，BOS/EOS 用 tokenizer 对应 id self.pad_id = 0 self.bos_id = getattr(self.tokenizer, \u0026#34;bos_token_id\u0026#34;, None) self.eos_id = getattr(self.tokenizer, \u0026#34;eos_token_id\u0026#34;, None) # 构建文件行偏移表 self._build_offsets() self._fp = None def _build_offsets(self): self.offsets: List[int] = [] cur = 0 with open(self.path, \u0026#34;rb\u0026#34;) as f: for line in f: self.offsets.append(cur) cur += len(line) self._len = len(self.offsets) def __len__(self): return self._len def _ensure_file_open(self): if self._fp is None: self._fp = open(self.path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) def _read_line_by_index(self, idx: int) -\u0026gt; str: self._ensure_file_open() self._fp.seek(self.offsets[idx]) line = self._fp.readline() return line.rstrip(\u0026#34;\\n\u0026#34;) def _tokenize_to_ids(self, text: str) -\u0026gt; List[int]: if text is None: text = \u0026#34;\u0026#34; # 获取 token ids ids = self.tokenizer(text).data[\u0026#39;input_ids\u0026#39;][:self.max_length] ids = [int(i) for i in ids] # 添加 BOS token if self.bos_id is not None: ids = [self.bos_id] + ids # 截断 if len(ids) \u0026gt; self.max_length: ids = ids[:self.max_length] return ids def __getitem__(self, index: int): line = self._read_line_by_index(index) try: obj = json.loads(line) text = obj.get(\u0026#34;text\u0026#34;, \u0026#34;\u0026#34;) if isinstance(obj, dict) else str(obj) except Exception: text = line ids = self._tokenize_to_ids(text) # padding 到 max_length pad_len = self.max_length - len(ids) if pad_len \u0026gt; 0: ids = ids + [self.pad_id] * pad_len elif pad_len \u0026lt; 0: ids = ids[:self.max_length] seq = torch.tensor(ids, dtype=torch.long) input_ids = seq[:-1] labels = seq[1:].clone() attention_mask = (seq != self.pad_id).long()[:-1] # labels padding -\u0026gt; -100 labels[attention_mask == 0] = -100 return input_ids, labels, attention_mask def __del__(self): try: if self._fp is not None: self._fp.close() except Exception: pass # ====== 测试例子 ====== if __name__ == \u0026#34;__main__\u0026#34;: from torch.utils.data import DataLoader from transformers import AutoTokenizer # 创建临时 jsonl 文件 test_file = \u0026#34;test.jsonl\u0026#34; texts = [ {\u0026#34;text\u0026#34;: \u0026#34;我爱南巷的花猫\u0026#34;}, {\u0026#34;text\u0026#34;: \u0026#34;今天天气很好\u0026#34;}, {\u0026#34;text\u0026#34;: \u0026#34;机器学习真有趣\u0026#34;} ] with open(test_file, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: for item in texts: f.write(json.dumps(item, ensure_ascii=False) + \u0026#34;\\n\u0026#34;) tokenizer = AutoTokenizer.from_pretrained( \u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/tokenizer_k\u0026#34; ) dataset = PretrainMapDataset(path=test_file, tokenizer=tokenizer, max_length=10) dataloader = DataLoader(dataset, batch_size=2, shuffle=False) for batch in dataloader: X, Y, mask = batch print(\u0026#34;input_ids:\\n\u0026#34;, X) print(\u0026#34;labels:\\n\u0026#34;, Y) print(\u0026#34;attention_mask:\\n\u0026#34;, mask) break os.remove(test_file) 来看看输出结果：\n自此，我们的数据加载器就大功告成了。\n下面就是我们最重要的部分训练模型。怎么训练的?,怎么去做loss？这个才应该是我们更值得去关注的事情，因为学会一套方法后，来再多的模型都可以手到擒来。轻轻松松，我们现在已经有了训练数据，网络模型，现在我们就来手撸一个预训练代码。\n预训练 # 在做预训练之前，我们先将第二章写的代码都整理一下，按照文件夹来放置。\n模型代码都是一样的，这个时候我们主要编写的是train。我们下面开始一步一步拆分。\n代码拆分 # import os import argparse import time import math import torch from torch import optim from torch.utils.data import DataLoader, DistributedSampler import torch.distributed as dist from contextlib import nullcontext from transformers import AutoTokenizer from ModelConfig import ModelConfig from LLaMA2 import Transformer from PretrainMapDataset import PretrainMapDataset import swanlab os / argparse / time / math：基础 Python 工具。\ntorch：PyTorch，用于模型训练。\nDistributedSampler / dist：分布式训练（多GPU）必备。\nnullcontext：方便 CPU 或 GPU 自动选择混合精度上下文。\nAutoTokenizer：加载 tokenizer（分词器）。\nModelConfig / Transformer / PretrainMapDataset：你自己的模型配置、模型类和数据集类。\nswanlab：实验追踪工具，只在主进程记录日志。\n这里会涉及到DP，DDP训练的内容，这个我们叫做数据并行，然后还有一个东西叫做模型并行。这两部分的内容，会在后面的知识补录中慢慢更新上，我们首先是先做完全流程的模型构建，模型预训练，模型监督微调等。\n日志打印 # def Logger(msg: str, rank=0):\u0026quot;\u0026quot;\u0026quot;多卡训练中只在主卡打印日志\u0026quot;\u0026quot;\u0026quot; `` if rank == 0:print(msg)\n解释：\n多卡训练时，每个 GPU 都会运行一份脚本。\n我们只想让 rank=0 的主进程打印日志，避免重复打印。\n学习率调度 # def get_lr(it, total_iters, args): warmup_iters = args.warmup_iters min_lr = args.learning_rate / 10 if it \u0026lt; warmup_iters: return args.learning_rate * it / warmup_iters # 线性预热 elif it \u0026gt; total_iters: return min_lr # 超过总迭代次数，保持最小 lr else: decay_ratio = (it - warmup_iters) / (total_iters - warmup_iters) coeff = 0.5 * (1 + math.cos(math.pi * decay_ratio)) # 余弦退火 return min_lr + coeff * (args.learning_rate - min_lr) 解释：\n线性预热：刚开始训练，学习率从 0 逐渐升到目标值。\n余弦退火：训练后期，学习率慢慢降低。\n总迭代结束：学习率保持在最小值\n模型保存 # def save_model(model, save_dir, step, lm_config, rank=0): \u0026#34;\u0026#34;\u0026#34;DDP 多卡训练时，只让主卡保存模型\u0026#34;\u0026#34;\u0026#34; if rank == 0: os.makedirs(save_dir, exist_ok=True) state_dict = model.module.state_dict() if hasattr(model, \u0026#39;module\u0026#39;) else model.state_dict() path = f\u0026#34;{save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step + 1}.pth\u0026#34; torch.save(state_dict, path) Logger(f\u0026#34;模型保存: {path}\u0026#34;, rank) 解释：\nDDP 下，每个 GPU 都有一份模型，如果每个都保存，会覆盖和浪费空间。\n所以只让 rank=0 保存模型。\nmodel.module：DDP 包装后需要取 module 才是真正的模型。\n模型初始化 # def init_model(args, lm_config, rank, gpu_id): tokenizer = AutoTokenizer.from_pretrained(\u0026#39;/root/StudyLLM/HappyLLM/Tokenizer/tokenizer_k\u0026#39;) model = Transformer(lm_config).to(args.device) model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[gpu_id], output_device=gpu_id) Logger(f\u0026#39;LLM总参数量: {sum(p.numel() for p in model.parameters() if p.requires_grad)/1e6:.3f} M\u0026#39;, rank) return model, tokenizer 解释：\n加载 tokenizer（把文本转成数字）。\n创建模型，并移动到 当前 GPU。\n用 DistributedDataParallel（DDP） 包装模型，让多卡训练自动同步梯度。\n打印模型参数量，只在主进程显示。\n训练单个 Epoch # def train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, rank): start_time = time.time() for step, (X, Y, Attention_mask) in enumerate(train_loader): X, Y, Attention_mask = X.to(args.device), Y.to(args.device), Attention_mask.to(args.device) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args) for param_group in optimizer.param_groups: param_group[\u0026#39;lr\u0026#39;] = lr with ctx: # 混合精度 out = model(X, Y) loss = out.last_loss / args.accumulation_steps loss_mask_flat = Attention_mask.view(-1) loss = torch.sum(loss * loss_mask_flat) / loss_mask_flat.sum() scaler.scale(loss).backward() if (step + 1) % args.accumulation_steps == 0: scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) if step % args.log_interval == 0: elapsed_time = time.time() - start_time Logger( f\u0026#34;Rank[{rank}] Epoch[{epoch+1}/{args.epochs}] Step[{step}/{iter_per_epoch}] \u0026#34; f\u0026#34;Loss:{loss.item()*args.accumulation_steps:.4f} LR:{lr:.7f} Elapsed:{elapsed_time:.1f}s\u0026#34;, rank ) if args.use_swanlab and rank == 0: swanlab.log({\u0026#34;loss\u0026#34;: loss.item()*args.accumulation_steps, \u0026#34;lr\u0026#34;: lr}) if (step + 1) % args.save_interval == 0: save_model(model, args.save_dir, step, lm_config, rank) 解释：\n梯度累积：一次只更新一部分 batch，模拟大 batch。\n混合精度：节省显存，加速训练。\n日志：只在主进程打印，同时同步到 SwanLab。\n模型保存：每隔 save_interval 步保存一次。\n主函数逻辑 # if __name__ == \u0026#34;__main__\u0026#34;: parser = argparse.ArgumentParser() parser.add_argument(\u0026#34;--out_dir\u0026#34;, type=str, default=\u0026#34;../ModelZoom/base-CodeLab-26M\u0026#34;) parser.add_argument(\u0026#34;--epochs\u0026#34;, type=int, default=1) parser.add_argument(\u0026#34;--batch_size\u0026#34;, type=int, default=64) parser.add_argument(\u0026#34;--learning_rate\u0026#34;, type=float, default=2e-4) parser.add_argument(\u0026#34;--device\u0026#34;, type=str, default=\u0026#34;cuda:0\u0026#34; if torch.cuda.is_available() else \u0026#34;cpu\u0026#34;) parser.add_argument(\u0026#34;--dtype\u0026#34;, type=str, default=\u0026#34;bfloat16\u0026#34;) parser.add_argument(\u0026#34;--use_swanlab\u0026#34;, action=\u0026#34;store_true\u0026#34;, default=True) parser.add_argument(\u0026#34;--num_workers\u0026#34;, type=int, default=8) parser.add_argument(\u0026#34;--data_path\u0026#34;, type=str, default=\u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/Dataset/pretrain_data.jsonl\u0026#34;) parser.add_argument(\u0026#34;--accumulation_steps\u0026#34;, type=int, default=8) parser.add_argument(\u0026#34;--grad_clip\u0026#34;, type=float, default=1.0) parser.add_argument(\u0026#34;--warmup_iters\u0026#34;, type=int, default=0) parser.add_argument(\u0026#34;--log_interval\u0026#34;, type=int, default=100) parser.add_argument(\u0026#34;--save_interval\u0026#34;, type=int, default=1000) parser.add_argument(\u0026#34;--local_rank\u0026#34;, type=int, default=0) args = parser.parse_args() 解释：\n用 argparse 管理训练参数，方便命令行调整。\nlocal_rank：多卡训练时 PyTorch 自动传入的每个进程编号。\n初始化 DDP \u0026amp; GPU # dist.init_process_group(backend=\u0026#39;nccl\u0026#39;) local_rank = int(os.environ.get(\u0026#34;LOCAL_RANK\u0026#34;, 0)) gpu_id = local_rank torch.cuda.set_device(gpu_id) args.device = f\u0026#39;cuda:{gpu_id}\u0026#39; ngpus = torch.cuda.device_count() if gpu_id \u0026gt;= ngpus: raise RuntimeError(f\u0026#34;local_rank {gpu_id} 超出可用 GPU 范围 (0-{ngpus-1})\u0026#34;) Logger(f\u0026#34;[Rank {args.local_rank}] 使用 GPU {gpu_id} / 总 GPU 数量 {ngpus}\u0026#34;, args.local_rank) 解释：\ninit_process_group：启动多卡训练通信。\n每个进程绑定自己的 GPU。\n打印 GPU 信息，方便调试。\nSwanLab 只在主进程初始化 # if args.use_swanlab and args.local_rank == 0: swanlab.login(api_key=\u0026#34;你的api key\u0026#34;) swanlab.init(project=\u0026#34;CodeLab-LLM\u0026#34;, experiment_name=\u0026#34;Pretrain-26M\u0026#34;, config=args) 解释：\nSwanLab 用于实验记录。\n多卡训练只允许 rank=0 初始化，避免重复创建实验。\n模型配置 \u0026amp; 数据加载 # lm_config = ModelConfig(dim=512, n_layers=8) max_seq_len = lm_config.max_seq_len args.save_dir = os.path.join(args.out_dir) os.makedirs(args.out_dir, exist_ok=True) torch.manual_seed(42) ctx = nullcontext() if \u0026#34;cpu\u0026#34; in args.device else torch.amp.autocast(device_type=\u0026#34;cuda\u0026#34;, dtype=torch.bfloat16) model, tokenizer = init_model(args, lm_config, args.local_rank, gpu_id) train_ds = PretrainMapDataset(args.data_path, tokenizer, max_length=max_seq_len) train_sampler = DistributedSampler(train_ds) train_loader = DataLoader(train_ds, batch_size=args.batch_size, sampler=train_sampler, num_workers=args.num_workers, pin_memory=True) 解释：\n设置模型参数。\n混合精度上下文。\n初始化模型 \u0026amp; tokenizer。\n数据集 + DDP sampler + DataLoader。\n优化器 \u0026amp; GradScaler # scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in [\u0026#39;float16\u0026#39;, \u0026#39;bfloat16\u0026#39;])) optimizer = optim.Adam(model.parameters(), lr=args.learning_rate) 解释：\nGradScaler：混合精度训练必须。\nAdam 优化器。\n开始训练循环 # iter_per_epoch = len(train_loader) for epoch in range(args.epochs): train_sampler.set_epoch(epoch) train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, args.local_rank) if args.local_rank == 0: Logger(\u0026#34;训练完成！\u0026#34;) dist.destroy_process_group() 解释：\n每轮训练都设置 epoch（保证 DDP shuffle 正确）。\n调用 train_epoch。\n训练结束后，主进程打印“训练完成”。\n销毁 DDP 进程组。\n是不是很简单，和CV任务是不是一模一样，如果不需要那么多额外功能的话，就十几行就可以搞定。真的真的非常so easy。\n下面附上所有的代码\n代码地址：Code/train.py\n# -*- coding: utf-8 -*- import os import argparse import time import math import torch from torch import optim from torch.utils.data import DataLoader, DistributedSampler import torch.distributed as dist from contextlib import nullcontext from transformers import AutoTokenizer from ModelConfig import ModelConfig from LLaMA2 import Transformer from PretrainMapDataset import PretrainMapDataset import swanlab # ------------------- 工具函数 ------------------- def Logger(msg: str, rank=0): *\u0026#34;\u0026#34;\u0026#34;* * 多卡训练中只在主卡打印日志* * rank=0 表示主进程* * \u0026#34;\u0026#34;\u0026#34;* * *if rank == 0: print(msg) def get_lr(it, total_iters, args): *\u0026#34;\u0026#34;\u0026#34;* * 计算学习率：* * - 线性预热阶段：从0线性增长到目标学习率* * - 余弦退火阶段：按余弦衰减到最小学习率* * - 超过总迭代次数：保持最小学习率* * \u0026#34;\u0026#34;\u0026#34;* * *warmup_iters = args.warmup_iters min_lr = args.learning_rate / 10 if it \u0026lt; warmup_iters: return args.learning_rate * it / warmup_iters elif it \u0026gt; total_iters: return min_lr else: decay_ratio = (it - warmup_iters) / (total_iters - warmup_iters) coeff = 0.5 * (1 + math.cos(math.pi * decay_ratio)) return min_lr + coeff * (args.learning_rate - min_lr) def save_model(model, save_dir, step, lm_config, rank=0): *\u0026#34;\u0026#34;\u0026#34;* * 保存模型：* * - DDP 多卡训练时，只让主卡(rank=0)保存模型* * - 处理 DataParallel 或 DDP 包装的模型* * \u0026#34;\u0026#34;\u0026#34;* * *if rank == 0: os.makedirs(save_dir, exist_ok=True) state_dict = model.module.state_dict() if hasattr(model, \u0026#39;module\u0026#39;) else model.state_dict() path = f\u0026#34;{save_dir}/pretrain_{lm_config.dim}_{lm_config.n_layers}_{lm_config.vocab_size}_step{step + 1}.pth\u0026#34; torch.save(state_dict, path) Logger(f\u0026#34;模型保存: {path}\u0026#34;, rank) # ------------------- 模型初始化 ------------------- def init_model(args, lm_config, rank, gpu_id): *\u0026#34;\u0026#34;\u0026#34;* * 初始化模型 + tokenizer + DDP 包装* * \u0026#34;\u0026#34;\u0026#34;* * *def count_parameters(model): *\u0026#34;\u0026#34;\u0026#34;计算可训练参数量\u0026#34;\u0026#34;\u0026#34;* * *return sum(p.numel() for p in model.parameters() if p.requires_grad) # 加载 tokenizer tokenizer = AutoTokenizer.from_pretrained(\u0026#39;/root/StudyLLM/HappyLLM/Tokenizer/tokenizer_k\u0026#39;) # 创建 Transformer 模型并移动到当前 GPU model = Transformer(lm_config).to(args.device) # DDP 包装模型，每个进程只处理自己对应的 GPU model = torch.nn.parallel.DistributedDataParallel( model, device_ids=[gpu_id], output_device=gpu_id, ) Logger(f\u0026#39;LLM总参数量: {count_parameters(model) / 1e6:.3f} M\u0026#39;, rank) return model, tokenizer # 训练单个epoch，其实对于大模型来说，基本都是只会训练一次，因为这个数据量实在是非常非常庞大 def train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, rank): *\u0026#34;\u0026#34;\u0026#34;* * 训练一个 epoch* * - 支持梯度累积* * - 支持混合精度* * - 支持 DDP* * \u0026#34;\u0026#34;\u0026#34;* * *# 记录开始时间 start_time = time.time() for step, (X, Y, Attention_mask) in enumerate(train_loader): # 将数据迁移到显卡 X, Y, Attention_mask = X.to(args.device), Y.to(args.device), Attention_mask.to(args.device) # 一个简易的学习率调度器 lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args) for param_group in optimizer.param_groups: param_group[\u0026#39;lr\u0026#39;] = lr # 使用混合精度前向传播 with ctx: out = model(X, Y) # 模型前向 # 除以梯度累积步数 loss = out.last_loss / args.accumulation_steps # 展平 mask loss_mask_flat = Attention_mask.view(-1) # 忽略 padding loss = torch.sum(loss * loss_mask_flat) / loss_mask_flat.sum() # 反向传播获取梯度 scaler.scale(loss).backward() # 进行梯度更新 if (step + 1) % args.accumulation_steps == 0: scaler.unscale_(optimizer) # 梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) # 优化器更新 scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) # ------------------- 日志 ------------------- if step % args.log_interval == 0: elapsed_time = time.time() - start_time Logger( f\u0026#34;Rank[{rank}] Epoch[{epoch + 1}/{args.epochs}] \u0026#34; f\u0026#34;Step[{step}/{iter_per_epoch}] \u0026#34; f\u0026#34;Loss:{loss.item() * args.accumulation_steps:.4f} \u0026#34; f\u0026#34;LR:{lr:.7f} Elapsed:{elapsed_time:.1f}s\u0026#34;, rank ) if args.use_swanlab and rank == 0: swanlab.log({\u0026#34;loss\u0026#34;: loss.item() * args.accumulation_steps, \u0026#34;lr\u0026#34;: lr}) # ------------------- 模型保存 ------------------- if (step + 1) % args.save_interval == 0: save_model(model, args.save_dir, step, lm_config, rank) # ------------------- 主函数 ------------------- if __name__ == \u0026#34;__main__\u0026#34;: parser = argparse.ArgumentParser() # ------------------- 基础训练参数 ------------------- parser.add_argument(\u0026#34;--out_dir\u0026#34;, type=str, default=\u0026#34;../ModelZoom/base-CodeLab-26M\u0026#34;, help=\u0026#34;模型输出目录\u0026#34;) parser.add_argument(\u0026#34;--epochs\u0026#34;, type=int, default=1, help=\u0026#34;训练轮数\u0026#34;) parser.add_argument(\u0026#34;--batch_size\u0026#34;, type=int, default=64, help=\u0026#34;模型训练批次大小\u0026#34;) parser.add_argument(\u0026#34;--learning_rate\u0026#34;, type=float, default=2e-4, help=\u0026#34;训练的学习率\u0026#34;) parser.add_argument(\u0026#34;--device\u0026#34;, type=str, default=\u0026#34;cuda:0\u0026#34; if torch.cuda.is_available() else \u0026#34;cpu\u0026#34;, help=\u0026#34;训练设备\u0026#34;) parser.add_argument(\u0026#34;--dtype\u0026#34;, type=str, default=\u0026#34;bfloat16\u0026#34;) parser.add_argument(\u0026#34;--use_swanlab\u0026#34;, action=\u0026#34;store_true\u0026#34;, default=True, help=\u0026#34;是否使用SwanLab进行实验跟踪\u0026#34;) parser.add_argument(\u0026#34;--num_workers\u0026#34;, type=int, default=8, help=\u0026#34;数据加载的工作进程数\u0026#34;) parser.add_argument(\u0026#34;--data_path\u0026#34;, type=str, default=\u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/Dataset/pretrain_data.jsonl\u0026#34;, help=\u0026#34;训练数据路径\u0026#34;) parser.add_argument(\u0026#34;--accumulation_steps\u0026#34;, type=int, default=8, help=\u0026#34;梯度累积步数\u0026#34;) parser.add_argument(\u0026#34;--grad_clip\u0026#34;, type=float, default=1.0, help=\u0026#34;梯度裁剪阈值\u0026#34;) parser.add_argument(\u0026#34;--warmup_iters\u0026#34;, type=int, default=0, help=\u0026#34;学习率预热迭代次数\u0026#34;) parser.add_argument(\u0026#34;--log_interval\u0026#34;, type=int, default=100, help=\u0026#34;日志记录间隔\u0026#34;) parser.add_argument(\u0026#34;--save_interval\u0026#34;, type=int, default=1000, help=\u0026#34;模型保存间隔\u0026#34;) # DDP 本地 rank parser.add_argument(\u0026#34;--local_rank\u0026#34;, type=int, default=0, help=\u0026#34;DDP local rank\u0026#34;) args = parser.parse_args() # ------------------- 初始化 DDP \u0026amp; 自动适配 GPU ------------------- dist.init_process_group(backend=\u0026#39;nccl\u0026#39;) local_rank = int(os.environ.get(\u0026#34;LOCAL_RANK\u0026#34;, 0)) gpu_id = local_rank torch.cuda.set_device(gpu_id) args.device = f\u0026#39;cuda:{gpu_id}\u0026#39; # 获取总 GPU 数量 ngpus = torch.cuda.device_count() # 检查 local_rank 是否超出可用 GPU 范围 if gpu_id \u0026gt;= ngpus: raise RuntimeError(f\u0026#34;local_rank {gpu_id} 超出可用 GPU 范围 (0-{ngpus - 1})\u0026#34;) Logger(f\u0026#34;[Rank {args.local_rank}] 使用 GPU {gpu_id} / 总 GPU 数量 {ngpus}\u0026#34;, args.local_rank) # ------------------- SwanLab ------------------- if args.use_swanlab and args.local_rank == 0: swanlab.login(api_key=\u0026#34;你的api key\u0026#34;) swanlab.init(project=\u0026#34;CodeLab-LLM\u0026#34;, experiment_name=\u0026#34;Pretrain-26M\u0026#34;, config=args) # ------------------- 模型配置 ------------------- lm_config = ModelConfig(dim=512, n_layers=8) max_seq_len = lm_config.max_seq_len args.save_dir = os.path.join(args.out_dir) os.makedirs(args.out_dir, exist_ok=True) torch.manual_seed(42) # ------------------- 混合精度上下文 ------------------- ctx = nullcontext() if \u0026#34;cpu\u0026#34; in args.device else torch.amp.autocast(device_type=\u0026#34;cuda\u0026#34;, dtype=torch.bfloat16) # ------------------- 模型 \u0026amp; 数据 ------------------- model, tokenizer = init_model(args, lm_config, args.local_rank, gpu_id) train_ds = PretrainMapDataset(args.data_path, tokenizer, max_length=max_seq_len) # DDP 必须使用 DistributedSampler train_sampler = DistributedSampler(train_ds) train_loader = DataLoader( train_ds, batch_size=args.batch_size, sampler=train_sampler, num_workers=args.num_workers, pin_memory=True ) # ------------------- 优化器 \u0026amp; 混合精度 scaler ------------------- scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in [\u0026#39;float16\u0026#39;, \u0026#39;bfloat16\u0026#39;])) optimizer = optim.Adam(model.parameters(), lr=args.learning_rate) # ------------------- 开始训练 ------------------- iter_per_epoch = len(train_loader) for epoch in range(args.epochs): train_sampler.set_epoch(epoch) # DDP 每轮必须重置 epoch 保证 shuffle train_epoch(epoch, model, train_loader, optimizer, scaler, args, lm_config, ctx, iter_per_epoch, args.local_rank) if args.local_rank == 0: Logger(\u0026#34;训练完成！\u0026#34;) dist.destroy_process_group() 运行的话，在code里面有一个run.sh。在命令行运行这个就行。\nsh run.sh 如果代码都理解了的话 # 我们补充一下环境配置吧。\n首先是显卡 # 显卡我是在https://www.autodl.com/平台上租的，租了4张3090 24G跑预训练，因为我们切分的很小，只要2G的训练数据。所以30分钟不到就跑完了。\n然后我们模型仅仅只有26M的训练参数，这是一个非常非常小的模型。又是属于是ok单人套餐啦。\n显卡有了。\n下面就是我们的训练监控，这里知道非常非常推荐大家使用SwanLab。\nSwanLab # https://docs.swanlab.cn/\n里面的官方文档非常非常详细，这个大家自己去探索。\n下面是在我的项目里面使用到的\n测试我们的模型 # 我这里提供一个代码\n# -*- coding: utf-8 -*- *\u0026#34;\u0026#34;\u0026#34;* *简化版文本生成器示例* *功能：* *- 加载预训练模型* *- 自动选择 CPU/GPU* *- 支持混合精度推理* *- 提供简单的生成接口* *\u0026#34;\u0026#34;\u0026#34;* from contextlib import nullcontext import torch from transformers import AutoTokenizer from ModelConfig import ModelConfig from LLaMA2 import Transformer class TextGenerator: def __init__(self, checkpoint_path, # 模型检查点路径 tokenizer_path, # 分词器路径 device=None, # 推理设备，默认使用 GPU dtype=\u0026#34;bfloat16\u0026#34;, # 浮点精度 seed=42): # 随机种子，保证生成可复现 *\u0026#34;\u0026#34;\u0026#34;* * 初始化文本生成器* * \u0026#34;\u0026#34;\u0026#34;* * *# ---------------- 设备与随机种子 ---------------- self.device = device or (\u0026#39;cuda:0\u0026#39; if torch.cuda.is_available() else \u0026#39;cpu\u0026#39;) self.device_type = \u0026#39;cuda\u0026#39; if \u0026#39;cuda\u0026#39; in self.device else \u0026#39;cpu\u0026#39; self.dtype = dtype torch.manual_seed(seed) if \u0026#39;cuda\u0026#39; in self.device: torch.cuda.manual_seed(seed) torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True # ---------------- 自动混合精度上下文 ---------------- dtype_map = {\u0026#39;float32\u0026#39;: torch.float32, \u0026#39;bfloat16\u0026#39;: torch.bfloat16, \u0026#39;float16\u0026#39;: torch.float16} self.ctx = nullcontext() if self.device_type == \u0026#39;cpu\u0026#39; else torch.amp.autocast( device_type=self.device_type, dtype=dtype_map[self.dtype]) # ---------------- 模型加载 ---------------- checkpoint = torch.load(checkpoint_path, map_location=self.device) self.model = Transformer(ModelConfig(dim=512, n_layers=8)) # 处理 checkpoint 中可能多余的前缀 prefix = \u0026#39;_orig_mod.\u0026#39; for k in list(checkpoint.keys()): if k.startswith(prefix): checkpoint[k[len(prefix):]] = checkpoint.pop(k) self.model.load_state_dict(checkpoint, strict=False) self.model.eval() # 推理模式 self.model.to(self.device) # 移动到 GPU/CPU # 模型参数量 total_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad) print(f\u0026#34;模型加载完成，共 {total_params/1e6:.2f} M 参数。\u0026#34;) # ---------------- 分词器加载 ---------------- self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) def generate_text(self, prompt, max_new_tokens=128, temperature=0.7, top_k=50, num_samples=1): *\u0026#34;\u0026#34;\u0026#34;* * 根据输入 prompt 生成文本* * 参数：* * - prompt: str, 生成的起始文本* * - max_new_tokens: 每条生成文本的最大 token 数* * - temperature: 生成随机性，越大越随机* * - top_k: 采样时保留概率最高的 top_k token* * - num_samples: 生成样本数量* * 返回：* * - List[str]，生成的文本列表* * \u0026#34;\u0026#34;\u0026#34;* * *# 编码文本为 token ID input_ids = torch.tensor(self.tokenizer(prompt).data[\u0026#39;input_ids\u0026#39;], device=self.device).unsqueeze(0) outputs = [] with torch.no_grad(): with self.ctx: for _ in range(num_samples): generated_ids = self.model.generate(input_ids, max_new_tokens=max_new_tokens, temperature=temperature, top_k=top_k) text = self.tokenizer.decode(generated_ids[0].tolist()) outputs.append(text) return outputs # --------------------- 使用示例 --------------------- if __name__ == \u0026#34;__main__\u0026#34;: # 模型路径 checkpoint_path = \u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/ModelZoom/base-CodeLab-26M/pretrain_512_8_6144_step9000.pth\u0026#39; tokenizer_path = \u0026#39;/root/StudyLLM/NX_LLM/第三章_预训练流程/tokenizer_k\u0026#39; # 初始化生成器 generator = TextGenerator(checkpoint_path, tokenizer_path) # 测试生成 pretrain_prompt_datas = [ \u0026#39;\u0026lt;|im_start|\u0026gt;教学\u0026#39;, \u0026#39;\u0026lt;|im_start|\u0026gt;建设\u0026#39;, ] for i, prompt in enumerate(pretrain_prompt_datas): samples = generator.generate_text(prompt, max_new_tokens=120, temperature=0.75, num_samples=1) print(f\u0026#34;\\nSample {i + 1}:\\n{samples[0]}\\n{\u0026#39;-\u0026#39;*30}\u0026#34;) 上面由于是出于教学目的，模型参数量和数据量都是非常非常小的，这导致我们的模型效果非常非常糟糕，能力几乎是没有的，我们看loss也是可以看出来。下面我会基于50M参数的模型，以及全部的20G数据进行训练，模型可以在仓库里面下载。\n后续我们的STF也是基于这个版本。\n很明显，现在我换了50M参数量的模型后loss确是有下降\n说明拟合的更好了\n在后续的微调环节，我们也是基于此进行微调。\n好贵啊啊啊啊啊，现在训练这个已经花费我大几百大洋了。\n大家如果是想先走完全套流程的。在ModelConfig里面设置n_layers: int = 8，12就好。半个小时就训练好了\n我们看看实际测试效果\n是不是可以发现，前后文的连接程度变动更好了，更加连贯了。\n如此，我们可以得知，加大参数量，加大数据量是可以提升效果的。\n参数量多，可以学习到的特征就多，可以更好的拟合我们的训练数据。\n同时，训练数据多，这样就可以加大同频的词出现的概率，因为我们就训练一轮，在这一轮中，重复的内容是有助于更好的拟合这个数据集的。\n","date":"2026/06/14","externalUrl":null,"permalink":"/blog/llama2-pretraining-process/","section":"日常记录、技术札记与转载收藏。","summary":"从数据集构建、样本切分、词表处理、模型输入到训练循环，梳理 LLaMA 2 预训练的核心流程。","title":"LLaMA 2 预训练流程","type":"blog"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/pytorch/","section":"Tags","summary":"","title":"PyTorch","type":"tags"},{"content":"","date":"2026/06/14","externalUrl":null,"permalink":"/tags/%E9%A2%84%E8%AE%AD%E7%BB%83/","section":"Tags","summary":"","title":"预训练","type":"tags"},{"content":" 认识数据集 # 1、预训练数据集 和 SFT 数据集 # 在进行 LLaMA 2 模型的数据集准备时，需要明确区分预训练数据集（pretraining dataset）和SFT 数据集（supervised fine-tuning dataset），因为两者的来源、形式和用途都有明显差异。\n预训练数据集\n特点：通常用于模型的基础语言理解能力训练，数据量大、覆盖面广，但不要求格式严格。\n来源：主要是从网络上爬取的海量文本，包括新闻、论坛帖子、维基百科、技术文档、小说、学术论文等。\n数据形式：文本形式为主，不一定包含明确的对话或问答结构，例如一篇文章、一段新闻报道或一本电子书的章节。\n举例：\n从维基百科抓取的“自然语言处理”条目文本\nGitHub 上的开源代码及注释\nReddit 或知乎论坛的讨论帖子（去掉敏感信息）\nSFT 数据集（对话式微调数据集）\n特点：用于指导模型学习具体任务或对话能力，需要有明确的输入输出（instruction-response）关系。\n来源：通常通过人工标注、半自动生成或整理已有对话数据集获得。\n数据形式：以对话或问答形式呈现，每条数据通常包含“用户输入 → 模型期望输出”，有时还会包含上下文历史。\n举例：\n人工编写的问答对：用户问“LLaMA 2 是什么？” → 输出“LLaMA 2 是一个由 Meta 发布的开源大语言模型。”\n已有对话数据集如 Alpaca、ShareGPT 数据\n客服对话记录（脱敏处理后）\n问题来了，为什么要这么做呢？\n两者的目标不一样，预训练的目标是让模型掌握语言的基本规律、知识储备和常见模式。SFT 阶段让模型学会按照指令完成具体任务，或者进行有用的对话。\n总的可以概述为：\n预训练 = 打语言和知识基础，像“打地基”，覆盖面大但不精细。\nSFT = 教模型做事情，像“装修和布置房子”，精细但依赖基础。\n2、我们使用的 # 我们就不再从0开始，去爬数据，洗数据了，有机会我们可以单独开一章怎么做SFT数据，因为这个比较有意思，不需要从头开始训练，省点钱，只要去制造一些数据，这个时候是不是会发现盯着微信，qq里面好几个G的聊天数据发呆，没错。这个就是天然的，高质量的数据，是非常非常符合我们每个人具有特色的说话方式的数据。幻想一下，我们和一个完全拥有自己说话口吻的方式相互对话，那可太棒了，相当于是有一个自己的聊天分身，同时和一万个人聊天都是毫无压力的。\n好了，回归正题：\n数据来源于这个项目：https://github.com/mobvoi/seq-monkey-data?tab=readme-ov-file\n下载完成后：\n整体数据大小有20G多一点，还是有点大的，2块3090 24G要跑3天。所以我打算抽取2G左右的数据，也就是1/10的数据量级。所有的我们自己训练的数据都会上传到GitHub仓库。我们仅仅是为了跑通模型，不追求效果，所以只要部分数据即可。\n数据抽取 # 对应这种几个G的数据，直接肯定是打不开的，我们写几行简单的代码，查看前面几行就行。看看数据到底是长什么样的，同时，在作者声明中也有说明：{\u0026ldquo;text\u0026rdquo;: \u0026ldquo;\u0026lt;文档\u0026gt;\u0026rdquo;}\n代码在同级目录的Code/preview_jsonl.py\nimport json # 我们定义一个文件路径 file_path = \u0026#34;/root/autodl-tmp/Dataset/mobvoi_seq_monkey_general_open_corpus.jsonl\u0026#34; k = 1000 with open(file_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: for i, line in enumerate(f): if i \u0026gt;= k: break try: data = json.loads(line.strip()) print(data) except json.JSONDecodeError: print(\u0026#34;解析失败\u0026#34;) 可以看见，具体里面是这样的内容：\n他们都是符合这样{\u0026ldquo;text\u0026rdquo;: \u0026ldquo;\u0026lt;文档\u0026gt;\u0026quot;}的格式。\n{\u0026#39;text\u0026#39;: \u0026#39;在查处虚开增值税专用发票案件中，常常涉及进项留抵税额和税款损失的认定和处理。在计算税款损失时，要不要将进项留抵税额包括在内？\\n对此，实务中存在意见分歧。\\n有人主张归并，即计算税款损失时包括进项留抵税额；\\n有人主张剥离，即计算税款损失时剔除进项留抵税额。分析这个问题，需要确定进项留抵税额与税款损失之间是什么关系。\\n理清这二者之间的关系，首先需要了解增值税的概念和其抵扣机制。增值税是以商品（货物、服务等）在流转过程中产生的增值额作为计税依据而征收的一种流转税。为避免重复征税，在增值税中存在抵扣链条机制。\\n一般而言，交易上游企业缴纳的税额，交易下游企业可以对相应的税额进行抵扣。\\n对增值税一般纳税人来说，其购进货物、服务等取得增值税专用发票，发票上的税额是进项税额。\\n其出售货物、服务等，向购买方开具增值税专用发票，发票的税额是销项税额。\\n一般情况下，销项税额减去进项税额的金额是应纳税额，企业根据应纳税额按期申报纳税。\\n其次需要了解进项留抵税额的概念及产生原因。\\n在计算销项税额和进项税额的差额时，有时会出现负数，即当期进项税额大于当期销项税额。这个差额在当期未实现抵扣，为进项留抵税额，在以后纳税人有销项税额时再进行抵扣。\\n企业产生进项留抵税额的主要原因是其进项税额和销项税额时间上的不一致。\\n例如，企业前期集中采购货物和服务，投资大，销项税率低于进项税率等。\\n从税款抵扣的角度看，进项留抵税额只是购进的这部分进项税额参与到增值税应纳税额的计算过程中，但是其对应的进项税额抵扣还未真正实现，一般要等到其未来有相应的销项税额时，才能真正实现进项税额抵扣。\\n可见，进项留抵税额处于不确定状态，能否抵扣受到很多因素影响，例如企业经营中断，没有销项税额，这时进项留抵税额就无法实现抵扣。但如果企业按照税收政策规定申请进项留抵退税，进项税额抵扣就随之实现。\\n最后需要了解税款损失的概念。\\n税款损失，通常是指因虚开增值税专用发票，导致国家税款被骗或者流失的金额。关于税款损失，实务中有多种表述。\\n例如，北京大学法学院教授陈兴良曾谈到虚开行为本身不会造成国家税款损失，只有利用发票抵扣时才会造成国家税款损失。刘兵等编著的《虚开增值税专用发票案例司法观点和案例解析》一书中提到：“给国家税款造成损失的数额，实际上就是被骗取的国家税款在侦查终结以前无法追回的部分。”\\n赵清海与王家欣合著的《增值税专用发票虚开的判定与预防》一书中提到：“司法实践中，受票方用虚开的增值税专用发票予以抵扣的税款，从而导致受票方应纳税额的减少是法院所认定的国家税款流失的金额。”\\n从这些表述可见，税款损失应该是实际造成的损失，不应包括不确定的部分——进项留抵税额，进项留抵税额与税款损失之间不能直接画等号。\\n综上分析，进项留抵税额，只是使国家税款处于可能被抵扣的状态，还没有真正造成国家税款流失，一般情况下应将其从税款损失中剥离，特殊条件下将其归并入税款损失。\\n例如，当纳税人造假按照税收政策规定申请进项留抵税额退税后，有关税款损失将会从危险状态转化成危害结果，这时候要将有关进项留抵税额并入税款损失。\\n所以，在虚开增值税专用发票案件中，一般情况下，如果以纳税人的进项税额作为税款损失的计算基数，在对其进行行政处罚或刑事处罚时，应把进项留抵税额从税款损失中剔除，但纳税人申请进项留抵退税的除外。这样处理，把处罚与危害结果相对应，体现行政处罚法的过罚相当原则和刑法的罚当其罪原则。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;读者在使用本《年鉴》时发现与以前本局出版、公布、或内部提供的资料有出入的，概以本《年鉴》为准。\\n《年鉴》正文内容分为三大部分。第一部分为文字部分，收录了《2012年政府工作报告》以及《2011年河源市国民经济和社会发展统计公报》。第二部分为统计图，形象地反映建市以来河源市国民经济发展变化情况。第三部分为统计资料，具体分为行政区划和自然资源，综合、核算、人口，农村经济，工业，能源，交通、邮电，贸易业、物价指数，对外经济、旅游，财政、金融和保险，固定资产投资与建筑业，劳动工资，人民生活，文教、卫生和其他，河源市乡镇主要经济指标，广东省县域主要经济指标,广东省各市主要经济指标等16部分。此外，为便于读者正确理解和使用统计资料，特附主要统计指标解释、统计术语简介及统计法律法规等资料。\\n《年鉴》中，本市的数据是根据我局及有关部门的统计年报整理汇编而成，由于某些专业统计制度和统计口径的变化，有些数据空缺。使用本《年鉴》时，请注意指标名称的含义、统计口径、统计范围、计算单位、可比价与现行价(当年价)等。\\n《年鉴》第一部分中的有些数据为初步统计数，凡与本《年鉴》中第三部分的数据有出入的，则以第三部分的统计数据为准。\\n本《年鉴》部分统计数据使用了四舍五入的进位方法，因此，可能令统计表内个别项目相加与总数略有出入。\\n本《年鉴》统计表中符号使用说明：“＃”表示其中主要项；“空格”表示该项统计指标数据不详或无该项数据。\\n本《年鉴》的编辑出版，得到县、区及市直有关部门和单位的大力支持，在此表示感谢！本书疏漏之处敬请批评指正。\\n下载说明： �本站下载的文件一律为压缩文件，请使用 WinRAR 解压。\\n�PDF格式的资料请使用 Adobe Reader 浏览。\\n�本站提供的一些资料是供学习研究之用，如用于商业用途，请购买正版。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;初中阶段是学生身心发育的一个突变期。尤其是初一学生，从小学到中学，随着环境改变，课程增多，难度加大，他们内心发生了急剧变化，产生了许多烦恼、困惑，造成较大的心理偏差，这就需要教师和家长及时给予心理指导和帮助。\\n一、心理偏差的种种表现\\n1、骄傲自负心理。这种心理偏差主要表现在思维敏捷、小学成绩拔尖的学生身上，特别是一些长期担任班干部、竞赛获奖、父母有权力的学生表现尤为明显。\\n2．单纯求趣心理。求趣激趣，这是教学的原则之一，但是，有些初一学生过分地追求接受知识要符合自己的兴趣，还想回到幼儿园、小学时“游戏教育”和“愉快教育”中去，不能努力适应初中阶段的学习生活。\\n3．自卑孤僻心理。多数来自普通工薪家庭及农村贫困地区或遭遇父母婚变的学生，往往在干部、富家子弟、有特长的同学面前感到自卑，心理压抑，行为孤僻，甚至变态的自尊，影响学习。\\n4．胆怯畏惧心理。部分性格内向、胆小的学生，主要是女生，羞于用语言表达思想，沉溺于内心活动和笔头表达。内心活动不能外显，妨碍了思维素质的深入发展。\\n5．浮躁马虎心理。部分活泼好动的学生，智力水平不低，但就是不能静下心学习，总是浅尝辄止，马虎应付，不愿作深入的思考，常常“半罐水响叮当”。\\n6．贪图享受心理。一些家境较好的学生，行为懒散，好逸恶劳，学习上畏难怕苦，生活上讲吃讲穿。\\n二、上述心理偏差的形成原因\\n1．生理上的原因。初中学生处于发育高峰期，身高体重剧增，性发育开始。生理上的急剧变化使儿童意识到自己不再是孩子，“成人感”增强。但是，青年身体成熟速度存在着很大的个体差异：不同性别之间相差两年左右，同性别之间相差四年左右。因此，同是初中学生，一部分学生生理已跨入青年期，而另一部分学生可能还停留在童年期。\\n2．心理上的原因。随着生理的变化，“成人感”的出现，初中学生心理产生“独立”，力求摆脱对成人的依赖，老师、家长在他们心目中的权威降低，同学之间相互影响增强。思维上发展了批判性，但由于经验的缺乏含有片面性和主观性；行为上出现“独特性”和“受暗示性”乃至“抗拒性”，即逆反心理；情绪上带有冲动性，不善于克制自己；兴趣和愿望上带有随意性、多变性、狂热性，常为了所谓讲“义气”而庇护同伴，或为同伴打抱不平；感情上具有“闭锁性”，而对于艰苦的学习活动特别重要的意志品质，则还处在比较软弱的状态。\\n3．环境的原因。心理学认为，个体的生物遗传因素规定了发展的潜在可能范围，而个体环境教育则确定他在此可能范围内的现实水平。环境条件有利与否对个体发展的现实水平起了决定性作用。\\n①家庭。社会的信仰、观念等社会化目标都是首先通过父母的过渡，以高度个性化了的、有选择的形式传递给儿童的。父母本身的个性特征、社会地位、教育水平、宗教信仰、价值标准等等都强烈地影响他们的后代。父母的教养方式、家庭结构、物质条件、人际环境、文化和情绪氛围，都在很大程度上影响着学生。\\n②学校。学校不仅是对学生传授文化科学知识，进行政治思想教育的社会基本教育单元，还是促进学生良好品格形成和发展的重要场所。学生在学校里形成良好的品格，才能顺利走向社会，适应社会生活。反之，则会发生各种问题。而现在的应试教育制度，像紧箍咒一样，时时冲击着素质教育，教师以升学率论质量给待遇，使一些教师对成绩好的学生倍加宠爱，对成绩差的学生则百般呵斥。更有少数教师将腐朽庸俗的人际关系引入师生和家长的关系，身教言传，污染了学生心灵；让孩子过早成人化、世故化。\\n③社会。社会上各种腐朽思想沉渣泛起，对学生负面影响很大。影视传媒、流失少年、勒索等等，浸染着学生稚嫩的心灵；电子游戏机、卡拉OK厅等，又使我们的孩子面临着极强的诱惑，意志薄弱者稍不留意，便坠入其间。\\n三、纠正初中学生的心理偏差的对策\\n为了纠正初中学生的心理偏差，我们必须对教育环境影响予以高度重视。在现有环境中，我们应做到：\\n1．坚持以德、智、体、美、劳全面的教育方针为指导思想进行教育管理，坚持“要成才先成人”的教育思想。\\n2．“学高为师，身正为范。”作为教师，必须加强道德修养，提高职业素质，全面关心和爱护每一位学生的身心健康发展。\\n3．以激励为心育的主要手段。我们要将思想教育和学生喜闻乐见的实践活动结合起来，不断提高学生对美的感受和鉴赏力，使其求真向善，茁壮成长。\\n4．形成教育合力。在抓好班集体建设的同时，我们必须密切联系家长，与家长一起研究分析学生，共同教育学生。\\n5．帮助学生正确认识、分析、评价自己的心理过程。让他们将社会化标准－－《中学生日常行为规范》逐渐内化，用以规范自己的言行，自觉抵制不良诱惑，不断提高自我意识水平和自我教育能力。\\n6．对各类心理偏差学生施以不同的教育。对有骄傲自负心理的学生施以“挫折教育”；对有自卑、胆怯畏惧心理的学生施以“磨难教育”；对有虚荣忌妒、趋同报复、庸俗心理的学生施以分辨真美善、假丑恶的“是非教育”等。\\n与此同时，还应努力提高、优化当代中学生的心理特点。\\n首先，作为家长必须转变观念。对自己的孩子，在作业和职业方面的“期望值”不能脱离子女的实际而好高骛远，每个孩子因智力因素、情趣爱好，性格意志和心理承力各不相同，如果孩子确实尽了自己的努力，而未达到你所期望的目标，不应过多责怪，更不能冷嘲热讽，惩罚打骂。诚然，家长望子成龙“天经地义”，无可厚非。但“龙”的内涵并不专指读大学、考研究生。“三百六十行，行行出状元”，如果每一位家长都能建立这样的“职业观”，让孩子在宽松的环境里读书，\\n其次，作为教育者----教师来说，则更要不断学习，及时吸收新鲜气息，不断提高自己的思想、政治教育水平，提高自己的专业知识和业务水平，做到不仅能教书育人，更能进行教育评价，尊重学生人格，依法执教，用先进的具有创造性的教育思想、理论、方法促进教育水平的提高，注重培养学生的全面发展，加强能力培养和思维训练，提高学生的综合素质。具体方法如下：\\n第一，让学生充分了解自己的心理特点，通过与周围的同学以及其他同龄人相比，通过同电影、小说电视里特定情景中的人物相比，如宣传奥斯特洛夫斯基、托尔斯泰、张海迪、贝多芬等等，通过对比，找出自己在哪些方面存在弱点，或者也可以通过父母、老师、同学对自己经常的评价了解自己在哪些方面存在不良心理特点，从而扬长避短。\\n第二，选择恰当的方法进行锻炼。例如：\\n1、教他们多读好书，如《周恩来》、《钢铁是怎样炼成的》等优化心理品质。人类的几千年文明，其智慧、经验、真知灼见，都浓缩于书中，如果多读好书，能经常与这样一些“高尚朋友”对话，听听他们的“指点”以此开阔视野，启迪智慧，这对优化学生的心理品质是大有裨益的，作为中学生，不仅要读好的故事书，还应该读一些伟人的传记，读一些思想、修养方面的书籍，并且养成做读书笔记的习惯。\\n2、鼓励学生参加社会活动，锻炼心理品质，如送“温暖小组”、“助残小分队”等活动的开展，都是锻炼心理品质行之有效的方法。\\n3、也要注重培养学生琴、棋、书、画、音、体、美等美育活动，有助于疏导、排解不良情绪，给人以美的熏陶和享受，从而对心理产生良性刺激。让美来充实孩子的精神生活，让美来帮助塑造孩子健康的心理。\\n4、在条件可能的情况下，可组织学生春游、郊游、野炊等活动，学生也可以利用寒、曙假、节假日到一些名胜古迹去游览、旅游、参观、陶冶自己的情操，走进大自然，亲近大自然，细心体会大自然，不仅能使人心胸开阔、情绪放松，精神振奋，还常能使人领悟到人生的真谛。\\n只有这样，优化了学生的心理特点，才能促使学生健康成长，从而成为新世纪的合格人才。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;我们生产的食品消泡剂，具有可以快速消除泡沫的特点。\\n丹东食品消泡剂相关内容：一般而言，纯水和纯表面活性剂不起泡，这是因为它们的表面和内部是均匀的，很难形成弹性薄膜，即使形成亦不稳定，会瞬间消失。\\n丹东食品消泡剂选择：\\n1. 相容性：相容性是指两种或者两种以上物质混合时，不产生相斥分离现象的能力，相容性好，消泡剂就能够长期、稳定、均匀地存在于体系中，进而发挥消抑泡的作用；反之，就会出现分层等现象，使消泡剂的消泡工作无法正常进行。\\n2. 消泡能力：消泡能力是消泡剂的最主要性能，鉴别此项性能的标准是在同等条件下，分别加入等量不同的消泡剂，观察消泡剂的消泡速度。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;程总在座谈中首先向学校的客人介绍了三一集团和北京三一重机的情况，以及在公司快速发展过程中对人才的渴求，指出通过校企联合，学校可以依靠企业的参与制定人才培养方案，使培养的人才更贴近市场，贴近企业，又可以借助企业的资源充实学校的办学实力。同时校企联合有利于企业的可持续发展。校企联合是企业实现人才战略的途径。企业在与高等职业教育合作过程中可以贯彻自己的培养意向，满足对生产第一线实用型人才的需求。\\n武汉交通职业学院盛建龙院长和河北工业职业技术学院李军锁副院长分别介绍了各自学校人才培养情况，并对三一集团的高速发展表示钦佩和赞赏，表示将和公司开展深入、全面的合作，优势互补，使学校和企业实现充分的资源共享，建立全方位长效合作机制。\\n本次联合办学签约仪式，是北京桩机高起点校企合作的开始。按照北京桩机人力资源提升计划，明年北京桩机将和所高职高专院校进行联合办学成立“三一班”，均为统招大专高技学历层次，涉及焊接、装配、机加工、售后服务等紧缺工种，“三一班”学员将达到近300人，为北京桩机的下一个五年跨越式发展打下良好的人才基础。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;此类溶剂很可能会随着生产过程挥发出来而导致污染，其排放主要发生在投料、反应、溶剂回收、过滤、离心、烘干、出料等操作单元。\\n制药废气危害\\n在医药化行业中大量使用有机溶剂(如DMF、苯系物、有机胺、乙酸乙酯、二氯甲烷、丙酮、甲醇、乙醇、丁酮、乙醚、二氯乙烷、醋酸、氯仿等)，挥发形成了具有刺激性气味和恶臭的气体，并具有一定毒害性，长期排放必然恶化区域大气环境质量，并对附近居民的身体产生危害。因此，有效治理制药行业VOCs污染已经成为亟待解决的重要问题。\\n制药废气成分\\nDMF、苯系物、有机胺、乙酸乙酯、二氯甲烷、丙酮、甲醇、乙醇、丁酮、乙醚、二氯乙烷、醋酸、氯仿等。\\n制药废气特点\\n(1)排放点多, 排放量大, 无组织排放严重。医药化工产品得率低, 溶剂消耗大, 溶剂废气排放点多, 且溶剂废气大多低空无组织排放, 溶剂废气浓度较高。\\n(2)间歇性排放多。反应过程基本上为间歇反应, 溶剂废气也呈间歇性排放。\\n(3)排放不稳定。溶剂废气成分复杂, 污染物种类和浓度变化大, 同一套装置在不同时期可能排放不同性质的污染物。\\n(4)溶剂废气影响范围广。溶剂废气中的VOCs大多具有恶臭性质, 嗅域值低, 易扩散, 影响范围广。\\n(5)在生产过程中易燃、易爆物质多, 反应过程激烈, 生产事故风险大。\\n制药废气处理方案/ Treatment plan\\n活性炭吸附方案\\n当制药废气进入吸附箱后进入活性炭吸附层，由于活性炭吸附表面上存在着未平衡和未饱和的分子引力或化学键力，因此当活性炭吸附剂的表面与气体接触时，就能吸引气体分子，使其浓聚并保持在固体表面，此现象称为吸附。利用活性炭吸附剂表面的吸附能力，使废气与大表面的多孔活性炭吸附剂相接触，废气中的污染物被吸附在活性炭表面上，使其与气体混合物分离，净化后的气体高空排放。\\nUV光解净化方案\\nUV光解废气处理技术是指利用高能UV紫外线光束分解空气中的氧分子产生游离氧（即活性氧），因游离氧所携带正负电子不平衡所以需与氧分子结合，进而产生臭氧，臭氧具有很强的氧化性，通过臭氧对有机废气、恶臭气体进行协同光解氧化作用，使有机废气、恶臭气体物质降解转化成低分子化合物、水和二氧化碳。\\n技术特点\\n（1）高效除恶臭：能高效去除挥发性有机物(VOC)、无机物、硫化氢、氨气、硫醇类等主要污染物，以及各种恶臭味气体，脱臭效率可达到95%以上，脱臭效果超过国家1993年颁布的恶臭污染物排放标准(GB14554-93)和1996年颁布的《大气污染物综合排放标准》(GB16297-1996)。\\n无需预处理：有机气体无需进行特殊的预处理，如加温、加湿等,设备工作环境温度在-30℃－95℃之间，湿度在30%－98%、PH值在2-11范围均可正常工作。\\n（2）无需添加任何物质：只需要设置相应的排风管道和排风动力，使恶臭气体以及工业废气通过UV光解废气净化设备进行脱臭分解净化，无需添加任何物质参与化学反应。\\n（3）适应性强：可适应中低浓度，不同工业废气物质的脱臭、净化处理，可每天24小时连续工作，运行稳定可靠。\\n（4）运行成本低：无任何机械动作，无噪音，无需专人管理和日常维护，只需作定期检查，设备能耗低，设备风阻低＜50pa,可节约大量排风动力能耗。\\n（5） 安全可靠：因采用光解原理，模块采取隔爆处理，消除了安全隐患，防火、防爆、防腐蚀性能高，设备性能安全稳定，特别适用于采油(气)田、石油化工、制药等防爆要求高的行业。\\n应用范围\\n印刷厂、印染厂、电子厂、塑料厂、涂料厂、家具厂、炼油厂、橡胶厂、化工厂、造纸厂、皮革厂、农药厂、制药厂、油漆厂、化肥厂、食品加工厂、饲料厂、香精香料厂、屠宰厂、污水处理厂、垃圾中转站、喷涂喷漆等恶臭气体、工业废气的净化处理。\\n催化燃烧方案\\n蓄热式热力氧化技术是把有机废气加热到760℃以上，使废气中的VOC在氧化分解成二氧化碳和水。氧化产生的高温气体流经特制的陶瓷蓄热体，使陶瓷体升温而“蓄热”，此“蓄热”用于预热后续进入的有机废气。从而节省废气升温的燃料消耗。\\n技术特点\\n（1）高浓度废气处理实现自供热燃烧，运行费用低，性价比合理。\\n（2）净化效率高，三室型RTO可达99.5%。\\n（3）采用陶瓷蓄热体作为热能回收，预热、蓄热交替运行，热效率≥95%。\\n（4）炉体钢结构牢靠，保温层厚实，运行安全可靠，稳定性高。\\n（5）PLC可编程自动化控制，自动化程度高。\\n（6）适用性广，可净化任何有机废气\\n（7）余热利用，经济效益高；多余的热能回用至烘房、烤箱等，烘房的加热不用额外消耗燃料或电能。\\n适用范围\\n石油、化工、塑料、橡胶、制药、印刷、家具、纺织印染、涂布、涂料、半导体制造、合成材料等行业产生中、高浓度大风量有机废气处理，可处理有机物质种 类包括苯类、酚类、醛类、酮类、醒类、酯 类、醇类、炷类等。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;白癜风病人调节心理要偶尔也要屈服。能屈能伸，能进能退，轻松自如；凡事认真，一味固执，肯定烦恼重重。其实，只要大前提不受影响，一些细枝末节上的让步、妥协，是明智的，也是一种大智若愚的姿态。\\n转移治愈力：很多人的烦恼是由于对于生活的期望值过高而造成的，一旦这些期望没能实现，烦恼也就随之而来。因此，正确的面对现实，踏实做自己。尽量克制自己的情绪，并将注意力转移到学习、工作、娱乐或者其他感兴趣的方面，可以通过听歌、跳舞等娱乐活动来忘记心中的苦闷，还可以通过体育运动来消除不良的情绪，这样就不至于越想越难过了。贵州白癜风\\n白癜风患者怎样更好的减轻心理压力生活中要消除各种精神刺激，白癜风患者心理努力改善精神状态和不良的生活、工作环境，保持良好的心理，增强自身免疫功能，及早发现，及时治疗，并持之以恒。而且，在治疗的过程中，还要根据不同的病因，采用不同的治疗方法，同时要保持精神乐观，心情舒畅，切勿悲观急躁，尽力解除一切不必要的思想顾虑。\\n专家建议一旦发现在即皮肤出现白斑最好还是到正规的医院去进行确诊，然后在医生的指导下进行治疗。不要自己判断完了就自行用药，以免给自己带来不必要的伤害。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;对全校教学保障、教学建设、教学管理、教学运行和教学改革等进行质量监控和评价。\\n贯彻执行党的教育方针和上级教育部门文件精神，制定并组织实施学校教学质量监控与评估的相关文件及规章制度。\\n依据《博鱼平台入口(中国)有限公司各主要教学环节质量标准和评价方案》等规章制度，定期开展课堂教学、课程考核、毕业设计（论文、创作）、实验（实践）教学等主要教学环节的检查与评价工作，规范教学行为。\\n组织学校本科教学基本状态数据库的采集工作，进行数据整理、分析。充分利用信息技术，全面如实反映学校的办学状况以及在教学质量提升方面采取的措施和存在的问题，建立本科教学工作及其质量常态监控机制。\\n积极开展各类教学状况信息的收集和整理，并做好分析、反馈和整改工作。组织学校年度本科教学质量报告的撰写和发布工作，在客观反映学校教学质量的同时，分析学校教学质量发展的动态趋势，并揭示学校在人才培养和教学质量中存在的问题，提出具体的改进措施。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;有趣的是，库里在第三节上篮时被防守球员犯规，但裁判并未理会，怒不可遏的库里对着裁判一顿输出，随后怒吃一T。这彻底激怒了他，在随后的75秒内，库里连中3记三分带走比赛，而在命中本场比赛第7记三分后，库里还学裁判比出给T的手势热烈庆祝！在赛后采访时，库里谈到比T的庆祝手势：“很明显，我认为我被犯规了，所以我想发泄情绪，然后你就会放开了，并且专心的打篮球。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;担任地点省市的区域运营中心的办理作业。承受总部相关KPI查核。\\n1、了解新闻职业或媒体相关运营运营岗位，其间，应聘区域运营中心主任有3年以上当地干流媒体作业经验者优先，应聘事务主管有2年以上当地干流媒体作业经验者优先。\\n2、交流才能强，抗压才能强，长于处理复杂情况，了解GR作业优先，能独立完结策划计划优先。具有独立开发客户才能。\\n北京、天津、河北、山西、黑龙江、吉林、辽宁、上海、江苏、浙江、安徽、江西、福建、山东、河南、湖北、湖南、广东、海南、重庆、四川、贵州、云南、陕西等。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;在党建展览馆，全体党员跟随解说员通过开天辟地、改天换地、翻天覆地、惊天动地、红心向党五部分的学习，重温了中国共产党团结带领中国人民铸就百年辉煌的不懈奋斗、不怕牺牲、理论探索为民造福、自身建设的壮阔历程，深刻认识到红色政权来之不易、新中国来之不易、中国特色社会主义来之不易。随后，新发展的学生党员在庄严的国旗和党旗下郑重宣誓，接受了心灵的洗礼。\\n参观结束后全体党员进行了党性体检，通过查找不足，大家进一步加强了党性锻炼，筑牢了党员初心。此次活动，外语系党总支利用校内红色资源，使全体党员进一步深刻认识到了中国共产党为什么能、马克思主义为什么行、中国特色社会主义为什么好。在以后的工作生活中，大家将进一步继承我党光荣传统和优良作风，为祖国的教育事业贡献自己的一份力量。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;1962年始，他開始了新的寫作嘗試，即不用標點符號。這一年他的小說《天堂之門》問世，16年後該小說被譯成英語，在倫敦出版。小說描寫十字軍東征背後的種種動機，整部小說為一個句子，前40000個詞沒有用一個標點符號。\\n2．紀洛姆·阿波裡耐（1880-1918）\\n他是一位意大利軍官的非婚生子，出生後被父母遺棄，他是在法國利維拉省的教會學校中長大的。後來他到了巴黎，當過教師、教士、捉刀代筆人，還寫過色情小說。最後他成了文學批評家、小說家和詩人。他是未來派和立體派的創始人，並創造了\u0026#34;超現實主義\u0026#34;這個術語。他發現了原始派畫家亨利·盧索，畢加肅、布拉克、馬提西、弗拉明克和杜菲等畫家也都是他的好友。1913年他的詩集《醇酒集》使他一舉成名，這部作品激怒了許多批評家，因為裡面沒有標點符號。對此，阿波裡耐自己是這樣解釋的：「我擯棄標點符號，因為我覺得它們毫無用處，事實也確實如此，詩的節奏和詩行就是真正的標點，除此之外，什麼也不需要了。」\\n3．提摩西·德克斯特（1747-1806）\\n德克斯特在從事寫作之前做過商人，並因此發了大財。他在麻薩諸塞州的紐卡斯爾買了一幢很大的邸宅，門前放著40個真人大小的木雕塑像，其中有尼爾遜、亞當、夏娃、華盛頓、路易十四，還有他自己。在邸宅中他供養了一位妻子，一個星相學家，一個大塊頭宮廷小丑，一位非洲公主的女管家，還有一位充當桂冠詩人的魚販子。1802年德克斯特開始寫作，出版了一本24頁的自傳體哲學小冊子，題為《智者的困惑》。在這本書中，德克斯特竭力為自己的生活方式辯護，他描寫了自己發財的過程，並暗示他可以成為美國的好皇帝。這本書的一個顯著特點是它只包含一個句子，或者說不包含任何句子。書中沒有任何標點符號，從語法上說，這個句子沒有開頭也沒有結束。它就像時間和空間一樣，來自無限，並歸於無限。人們對這一\u0026#34;文學瑰寶\u0026#34;的反應使德克斯特意識到一部沒有標點符號的書實在算不上一本書。在這本書的第二版中，他增加了一頁，這一頁中有整整13行全是標點符號—-一行又一行的分號、逗號、問號、句號。德克斯特說：讀者們可以\u0026#34;隨心所欲\u0026#34;地自行在他書中的字裡行間\u0026#34;撒點胡椒和鹽\u0026#34;。\\n4．約翰·多布遜牧師（1794-1847）\\n多布遜牧師以數學見長，也從事文學寫作，但他是以憎恨標點符號而出名的。1815年，他的兩卷本《幾何原理》出版了，全書除了每段末尾用了一個句號外，沒有任何標點符號。不過這本書的完整性被出版商偷偷地破壞了，他在第一頁中加上了一個分號、一個冒號、一個逗號和一個句號。\\n5．哥楚特·斯泰因（1874-1946）\\n這位賓夕法尼亞出生的先鋒派女作家後來成了巴黎最著名的美籍作家之一。畢加索和海明威都是她的好友，她的沙龍和藝術收藏品是舉世聞名的。她的地位是由《三個女人的一生》和《艾麗斯·B·托克拉斯的自傳》奠定的。她的風格——簡單化與重複——獨具匠心，她自認這種風格在詹姆士·喬伊斯之上。但最突出的是她藐視標點符號。斯泰因認為「句號有著自己的生命\u0026#34;，所以她只用句號。有時她會不留神寫下一個逗號，不過她十分厭惡逗號，認為它\u0026#34;具有奴性\u0026#34;，使得作品讀起來過份容易。她認為問號和感歎號\u0026#34;具有鮮明的背叛性\u0026#34;。斯泰因最出名的字行是：「玫瑰就是玫瑰就是玫瑰就是玫瑰。\u0026#34;她終生未婚，與一位叫艾麗斯·B·托克拉斯的女子過著同性戀生活。\\n6．E·卡明斯\\n卡明斯是波士頓人，他既是詩人，又是畫家，他在文壇贏得一個令人不安的外號：「可怕的孩童\u0026#34;。他隨心所欲地使用標點符號、語法和排版，最初令評論家\u0026#34;大為迷惑\u0026#34;，他們指責他\u0026#34;想入非非\u0026#34;，不過，後來他成了一個出名的詩人。卡明斯憎惡句號和逗號，但他喜歡括弧和連接號。他有些詩中沒有一個標點，這種奇特的風格吸引了一些模仿者，不過他們缺乏卡明斯詩中獨特的韻味。\\n後一頁\\n前一頁\\n回目錄\\n\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;妈妈感慨“美好的一天”，也能作息规律，专注工作。\\n愿所有的美好如约而至，世界上没有父母不爱自己的孩子，世界上也没有不想学好的孩子，亲子之间，从相对无言到知无不言，从横眉冷对到设身处地，从针锋相对到理解包容，可能只需要一个转变的契机。\\n如果你日日夜夜的坐在电脑前，沉浸在一个干扰你的习惯、新陈代谢和内部时钟的虚拟世界中，那它怎么可能对你的智力发展有好处呢？对网络的迷恋和对学习没有兴趣，学业成绩形成了恶性循环。国内外研究还表明，青少年长期沉迷于网络中，除了影响头部发育外，还会导致植物神经紊乱、激素水平失衡，使免疫功能降低，引起紧张性头痛、焦虑，甚至导致死亡。\\n网上的一些内容很能抓住青少年的心理，网上游戏就是其中的一种，有些网络游戏与赌博的性质差不多，这种游戏采取积分上层次的方式。玩者第一次在网上玩，打到一定的分数，如果再打一定的分数就可上一个层次，上了层次其功能和能力就会更高更强。有了这种不断上层次、能力不断增强的吸引，孩子很容易玩上瘾。 网吧的宽容使得家长和学校管理孩子的难度越来越大。因为人们可以从网络上学习到各种信息，所以网上的很多孩子都很自豪，在网上看到新奇的内容，他们可以在同学、同伴之间互相炫耀，这种虚荣心使没有进入网吧的孩子大量走进网吧的大门。在孩子们的眼中，网吧是非常时尚的，而目前的舆论对于“网虫”还是开放的一面，并没有批评的成分。所以，不少青少年虽然把钱“送”给了网吧，他们却认为自己的做法是前卫的表现。\\n开展心理健康教育的必要性是不言而喻的，同时，这也是一项很讲究科学与方法的工作。我们要坚持科学的态度和遵循心理发展规律以及在正确的教育思想指导下，结合心理学基本原理和运用科学的方法，深入全面地了解学生的实际问题，这样才能为较好地实现心理健康教育目标提供现实的可能性。\\n网络成瘾的治疗已成为时代的紧迫任务。这意味着，如果任何人能够真正拥有一套有效的医学互联网成瘾技术，他们将拥有巨大的市场，甚至带来巨大的利润。对休克疗法本身的重视证明了治疗网络成瘾的迫切需要。\\n在我看来，网络成瘾的治疗，是不能放弃的。一些网络游戏成为魔鬼吞噬灵魂的最重要原因是网络游戏行业严重缺乏规范管理。近年来，网络游戏产业取得了长足的发展，网络游戏越来越让人上瘾。但是，网络游戏缺乏有效的法治管理。在这种形势下，网络游戏产业日益背离公共责任，产生了一些“电子海洛酮”，使得缺乏自我保护意识的未成年人成瘾成为一场灾难。\u0026#39;} {\u0026#39;text\u0026#39;: \u0026#39;二、企业信息化的内涵\\n1、目标：企业进行信息化建设的目的是“增强企业的核心竞争力”。\\n2、手段：计算机网络技术。\\n3、涉及的部门：企业的各个部门，包括：企业的生产、经营、设计、 我们可以看见，本质上来说，就是·从各个地方收集整理的数据，每一行之间也没有前后相互的关联。但是同一行的，是完整的一段话，但是这个也不要紧，后面我们还会对这个数据进行进一步的切分。\n下面我们来看看如何抽取小的一个数据集。\n很显然，我们可以获取这个数据集的总行数，然后截取1/10即可。\n代码地址：Code/split_small_dataset.py\nfrom tqdm import tqdm file_path = \u0026#34;/root/autodl-tmp/Dataset/mobvoi_seq_monkey_general_open_corpus.jsonl\u0026#34; count = 0 with open(file_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;, errors=\u0026#39;ignore\u0026#39;) as f: for _ in f: count += 1 subset_lines = count // 10 output_file = \u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/Dataset/mobvoi_seq_monkey_general_open_corpus_min.jsonl\u0026#34; with open(file_path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;, errors=\u0026#34;ignore\u0026#34;) as fin, \\ open(output_file, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as fout, \\ tqdm(total=subset_lines, desc=\u0026#34;抽取进度\u0026#34;, unit=\u0026#34;行\u0026#34;) as pbar: for i, line in enumerate(fin): if i \u0026gt;= subset_lines: break fout.write(line) pbar.update(1) print(f\u0026#34;已完成抽取，结果保存到：{output_file}\u0026#34;) 数据处理（文本切块） # 文本有了，那么我们为什么要切块呢？\n大家肯定经常听说，大模型上下文窗口，是多大，多大。这个就是每个模型输入的最大限制，也叫做序列长度，每个语言模型都有一个固定的最大上下文长度（context length）。\n模型 最大长度（token数） GPT-2 / BERT 512 LLaMA 2 4096 LLaMA 3 8192 GPT-4-turbo 可达 128k 这里我们为了节约算力，我们的窗口上下文就设置为256吧。\n这里为了保证切块的时候，具有一定的上下文，我们采用滑动窗口的形式，并且并且保证有32个字是重复的，这个我们应该怎么做呢？\ntext = ( \u0026#34;在自然语言处理中，语言模型是一种用于计算句子概率的模型。\u0026#34; \u0026#34;它可以被用来生成文本、翻译语言、回答问题。\u0026#34; \u0026#34;为了让模型能够理解更复杂的上下文，我们通常需要将长文本切分为较短的片段。\u0026#34; ) chunk_len = 50 overlap = 10 chunks = [] strat = 0 step = chunk_len - overlap while strat \u0026lt; len(text): end = min(strat + chunk_len, len(text)) chunk = text[strat: end] chunks.append(chunk) if end == len(text): break strat += step for i, c in enumerate(chunks): print(f\u0026#34;--- chunk {i} ({len(c)}字) ---\u0026#34;) print(c) print() /root/miniconda3/bin/python3 /root/StudyLLM/NX_LLM/第二章 动手实现/Code/deal_dataset_pre.py --- chunk 0 (50字) --- 在自然语言处理中，语言模型是一种用于计算句子概率的模型。它可以被用来生成文本、翻译语言、回答问题。为 --- chunk 1 (45字) --- 译语言、回答问题。为了让模型能够理解更复杂的上下文，我们通常需要将长文本切分为较短的片段。 下面是将我们完整的小数据集全部进行切分为256长度的块。\ndef split_text(text, chunk_size=256, overlap=32): *\u0026#34;\u0026#34;\u0026#34;* * 将文本按指定长度切分成块，支持滑动窗口，保证相邻块有 overlap 个字符重叠。* * 参数：* * text (str): 待切分文本* * chunk_size (int): 每块最大字符数* * overlap (int): 相邻块重叠字符数* * 返回：* * List[str]: 切分后的文本块列表* * \u0026#34;\u0026#34;\u0026#34;* * *if overlap \u0026gt;= chunk_size: raise ValueError(\u0026#34;overlap 必须小于 chunk_size\u0026#34;) chunks = [] step = chunk_size - overlap start = 0 n = len(text) while start \u0026lt; n: end = min(start + chunk_size, n) chunks.append(text[start:end]) if end == n: break start += step # 滑动窗口 return chunks pretrain_data = \u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/Dataset/mobvoi_seq_monkey_general_open_corpus_min.jsonl\u0026#34; output_pretrain_data = \u0026#34;/root/StudyLLM/NX_LLM/第二章 动手实现/Dataset/pretrain_data.jsonl\u0026#34; with open(output_pretrain_data, \u0026#39;a\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;, errors=\u0026#39;ignore\u0026#39;) as pretrain: # 注意这里加了 errors=\u0026#39;ignore\u0026#39;，避免非 utf-8 字节报错 with open(pretrain_data, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;, errors=\u0026#39;ignore\u0026#39;) as f: for line in tqdm(f, desc=f\u0026#34;Processing lines in {pretrain_data}\u0026#34;, leave=False): try: line = json.loads(line) # 尝试解析 JSON text = line.get(\u0026#39;text\u0026#39;, \u0026#39;\u0026#39;) # 保险：如果没有 text 字段返回空字符串 chunks = split_text(text) for chunk in chunks: pretrain.write(json.dumps({\u0026#39;text\u0026#39;: chunk}, ensure_ascii=False) + \u0026#39;\\n\u0026#39;) except Exception as e: # 如果某行解析失败（坏行 / 编码问题），跳过 print(f\u0026#34;跳过坏行: {e}\u0026#34;) 我们用之前的预览代码查看一下：\n可以看见，我们是成功处理了。下面我们就要把数据先放一边了，我们开始将模块一个一个编写完成。下面要讲的是\nTokenizers 组件 # 将输入的 token（数字化）映射为向量表示。我们一般都是使用Tokenizer这个模型。我们用句子 “我喜欢南巷的花猫” 来具体说明 Tokenizer 的工作原理。这里我举两个常见示例：字符级和 子词（BPE / SentencePiece）级，并展示 Token ID 转换过程。\n好的，我们用句子 “我喜欢南巷的花猫” 来具体说明 Tokenizer 的工作原理。这里我举两个常见示例：字符级和 子词（BPE / SentencePiece）级，并展示 Token ID 转换过程。\n原理 # 1、原始文本\n我喜欢南巷的花猫 长度：7 个中文字符。\n2、字符级 Tokenizer\n每个字符就是一个 token\n适合中文简单示例\ntext = \u0026#34;我喜欢南巷的花猫\u0026#34; tokens = list(text) print(tokens) 输出：\n[\u0026#39;我\u0026#39;, \u0026#39;喜\u0026#39;, \u0026#39;欢\u0026#39;, \u0026#39;南\u0026#39;, \u0026#39;巷\u0026#39;, \u0026#39;的\u0026#39;, \u0026#39;花\u0026#39;, \u0026#39;猫\u0026#39;] 如果我们给每个字符分配整数 ID（假设简单映射）：\n我 -\u0026gt; 101 喜 -\u0026gt; 102 欢 -\u0026gt; 103 南 -\u0026gt; 104 巷 -\u0026gt; 105 的 -\u0026gt; 106 花 -\u0026gt; 107 猫 -\u0026gt; 108 那么token IDs就是：\n[101, 102, 103, 104, 105, 106, 107, 108] 3、 BPE / SentencePiece 子词 Tokenizer\n模型训练时常用\n对中文长句通常按子词拆分（不是单字，也不是整词）\n假设 LLaMA2 的 tokenizer 处理这个句子可能结果如下（示例）：\nfrom transformers import LlamaTokenizer tokenizer = LlamaTokenizer.from_pretrained(\u0026#34;meta-llama/Llama-2-7b-hf\u0026#34;) text = \u0026#34;我喜欢南巷的花猫\u0026#34; encoding = tokenizer(text) print(\u0026#34;token IDs:\u0026#34;, encoding[\u0026#34;input_ids\u0026#34;]) print(\u0026#34;tokens:\u0026#34;, tokenizer.convert_ids_to_tokens(encoding[\u0026#34;input_ids\u0026#34;])) 可能输出（示意）：\ntoken IDs: [1, 230, 543, 876, 456, 13, 2] tokens: [\u0026#39;\u0026lt;s\u0026gt;\u0026#39;, \u0026#39;我\u0026#39;, \u0026#39;喜欢\u0026#39;, \u0026#39;南巷\u0026#39;, \u0026#39;的\u0026#39;, \u0026#39;花猫\u0026#39;, \u0026#39;\u0026lt;/s\u0026gt;\u0026#39;] \u0026lt;s\u0026gt; 和 \u0026lt;/s\u0026gt; 是起始/结束 token\n中文被拆成若干子词：我、喜欢、南巷、的、花猫\n这些 token ID 是模型 embedding 表中对应的索引，模型只理解这些数字。\n4、逆过程：Token ID → 文本\ndecoded_text = tokenizer.decode(encoding[\u0026#34;input_ids\u0026#34;]) print(decoded_text) 输出：\n我喜欢南巷的花猫 Tokenizer 保证文本和 token ID 之间可以一一映射，方便模型训练与生成。\n5、总结\n层次 Token 示例 Token ID 示例 字符级 [\u0026lsquo;我\u0026rsquo;,\u0026lsquo;喜\u0026rsquo;,\u0026lsquo;欢\u0026rsquo;,\u0026lsquo;南\u0026rsquo;,\u0026lsquo;巷\u0026rsquo;,\u0026lsquo;的\u0026rsquo;,\u0026lsquo;花\u0026rsquo;,\u0026lsquo;猫\u0026rsquo;] [101,102,103,104,105,106,107,108] 子词级 (BPE) [\u0026rsquo;\u0026rsquo;,\u0026lsquo;我\u0026rsquo;,\u0026lsquo;喜欢\u0026rsquo;,\u0026lsquo;南巷\u0026rsquo;,\u0026lsquo;的\u0026rsquo;,\u0026lsquo;花猫\u0026rsquo;,\u0026rsquo;\u0026rsquo;] [1,230,543,876,456,13,2] 字符级：粒度小，序列长\n子词级：粒度适中，更节省 token 数，适合 LLaMA、GPT 预训练\nToken ID 是模型真正能理解的数字，所有计算都在 token ID 上进行\n好了，那让我们看看怎么做吧，看了这么多原理，上代码前先把这些都安装上：\npip install tokenizers datasets transformers 我们的目标是：目标是 从零训练一个自定义的中文 BPE Tokenizer，并配置特殊 token 和聊天模板，使它可以用于中文对话模型的预训练或微调。\n主要流程包括：\n读取 JSONL 格式的中文文本语料\n用 BPE 模型训练 tokenizer\n保存 tokenizer 配置和文件\n测试 tokenizer 是否正常工作（编码、解码、特殊 token、聊天模板）\n关于BPE Tokenizer的原理和介绍看第一章的LLama2解析。\n这里也再次回顾一下，因为这个确实会比较陌生一点。\n假设我们有一个非常简单的文本：我喜欢南巷的花猫\nStep 1：初始化\n把文本拆成最小单位（通常是字符或字节）：\n[\u0026quot;我\u0026quot;, \u0026quot;喜\u0026quot;, \u0026quot;欢\u0026quot;, \u0026quot;南\u0026quot;, \u0026quot;巷\u0026quot;, \u0026quot;的\u0026quot;, \u0026quot;花\u0026quot;, \u0026quot;猫\u0026quot;]\n每个字符初始都是一个 token\nStep 2：统计频率\n统计 连续 token 对（pair） 出现的频率\n例如如果文本中出现了 \u0026quot;喜欢 南\u0026quot; 很多次，就记录 pair (\u0026quot;喜欢\u0026quot;,\u0026quot;南\u0026quot;) 的频率\nStep 3：合并最高频 pair\n找到频率最高的 pair，合并成一个新的 token\n例如：(\u0026quot;南\u0026quot;,\u0026quot;巷\u0026quot;) → \u0026quot;南巷\u0026quot;\n文本变为：\n[\u0026quot;我\u0026quot;, \u0026quot;喜\u0026quot;, \u0026quot;欢\u0026quot;, \u0026quot;南巷\u0026quot;, \u0026quot;的\u0026quot;, \u0026quot;花\u0026quot;, \u0026quot;猫\u0026quot;]\nStep 4：重复迭代\n再次统计连续 token 对频率，合并最高频的\n(\u0026quot;喜欢\u0026quot;,\u0026quot;南巷\u0026quot;) → \u0026quot;喜欢南巷\u0026quot;\n文本变为：\n[\u0026quot;我\u0026quot;, \u0026quot;喜欢南巷\u0026quot;, \u0026quot;的\u0026quot;, \u0026quot;花\u0026quot;, \u0026quot;猫\u0026quot;]\n迭代直到达到 vocab_size 或没有可合并的 pair\n训练词表 # 这里直接上代码：\nimport random import json import os from transformers import AutoTokenizer from tokenizers import ( decoders, models, pre_tokenizers, trainers, Tokenizer, ) from tokenizers.normalizers import NFKC from typing import Generator random.seed(42) def read_texts_from_jsonl(file_path: str) -\u0026gt; Generator[str, None, None]: *\u0026#34;\u0026#34;\u0026#34;读取JSONL文件并安全提取文本数据\u0026#34;\u0026#34;\u0026#34;* * *with open(file_path, \u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: for line_num, line in enumerate(f, 1): try: data = json.loads(line) if \u0026#39;text\u0026#39; not in data: raise KeyError(f\u0026#34;Missing \u0026#39;text\u0026#39; field in line {line_num}\u0026#34;) yield data[\u0026#39;text\u0026#39;] except json.JSONDecodeError: print(f\u0026#34;Error decoding JSON in line {line_num}\u0026#34;) continue except KeyError as e: print(e) continue def create_tokenizer_config(save_dir: str) -\u0026gt; None: *\u0026#34;\u0026#34;\u0026#34;创建完整的tokenizer配置文件\u0026#34;\u0026#34;\u0026#34;* * *config = { \u0026#34;add_bos_token\u0026#34;: False, \u0026#34;add_eos_token\u0026#34;: False, \u0026#34;add_prefix_space\u0026#34;: False, \u0026#34;bos_token\u0026#34;: \u0026#34;\u0026lt;|im_start|\u0026gt;\u0026#34;, \u0026#34;eos_token\u0026#34;: \u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34;, \u0026#34;pad_token\u0026#34;: \u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34;, \u0026#34;unk_token\u0026#34;: \u0026#34;\u0026lt;unk\u0026gt;\u0026#34;, \u0026#34;model_max_length\u0026#34;: 256, \u0026#34;clean_up_tokenization_spaces\u0026#34;: False, \u0026#34;tokenizer_class\u0026#34;: \u0026#34;PreTrainedTokenizerFast\u0026#34;, \u0026#34;chat_template\u0026#34;: ( \u0026#34;{% for message in messages %}\u0026#34; \u0026#34;{% if message[\u0026#39;role\u0026#39;] == \u0026#39;system\u0026#39; %}\u0026#34; \u0026#34;\u0026lt;|im_start|\u0026gt;system\\n{{ message[\u0026#39;content\u0026#39;] }}\u0026lt;|im_end|\u0026gt;\\n\u0026#34; \u0026#34;{% elif message[\u0026#39;role\u0026#39;] == \u0026#39;user\u0026#39; %}\u0026#34; \u0026#34;\u0026lt;|im_start|\u0026gt;user\\n{{ message[\u0026#39;content\u0026#39;] }}\u0026lt;|im_end|\u0026gt;\\n\u0026#34; \u0026#34;{% elif message[\u0026#39;role\u0026#39;] == \u0026#39;assistant\u0026#39; %}\u0026#34; \u0026#34;\u0026lt;|im_start|\u0026gt;assistant\\n{{ message[\u0026#39;content\u0026#39;] }}\u0026lt;|im_end|\u0026gt;\\n\u0026#34; \u0026#34;{% endif %}\u0026#34; \u0026#34;{% endfor %}\u0026#34; \u0026#34;{% if add_generation_prompt %}\u0026#34; \u0026#34;{{ \u0026#39;\u0026lt;|im_start|\u0026gt;assistant\\n\u0026#39; }}\u0026#34; \u0026#34;{% endif %}\u0026#34; ) } # 保存主配置文件 with open(os.path.join(save_dir, \u0026#34;tokenizer_config.json\u0026#34;), \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: json.dump(config, f, ensure_ascii=False, indent=4) # 创建special_tokens_map.json special_tokens_map = { \u0026#34;bos_token\u0026#34;: \u0026#34;\u0026lt;|im_start|\u0026gt;\u0026#34;, \u0026#34;eos_token\u0026#34;: \u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34;, \u0026#34;unk_token\u0026#34;: \u0026#34;\u0026lt;unk\u0026gt;\u0026#34;, \u0026#34;pad_token\u0026#34;: \u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34;, \u0026#34;additional_special_tokens\u0026#34;: [\u0026#34;\u0026lt;s\u0026gt;\u0026#34;, \u0026#34;\u0026lt;/s\u0026gt;\u0026#34;] } with open(os.path.join(save_dir, \u0026#34;special_tokens_map.json\u0026#34;), \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: json.dump(special_tokens_map, f, ensure_ascii=False, indent=4) def train_tokenizer(data_path: str, save_dir: str, vocab_size: int = 8192) -\u0026gt; None: *\u0026#34;\u0026#34;\u0026#34;训练并保存自定义tokenizer\u0026#34;\u0026#34;\u0026#34;* * *os.makedirs(save_dir, exist_ok=True) # 初始化tokenizer tokenizer = Tokenizer(models.BPE(unk_token=\u0026#34;\u0026lt;unk\u0026gt;\u0026#34;)) tokenizer.normalizer = NFKC() # 添加文本规范化 tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) tokenizer.decoder = decoders.ByteLevel() # 配置特殊token special_tokens = [ \u0026#34;\u0026lt;unk\u0026gt;\u0026#34;, \u0026#34;\u0026lt;s\u0026gt;\u0026#34;, \u0026#34;\u0026lt;/s\u0026gt;\u0026#34;, \u0026#34;\u0026lt;|im_start|\u0026gt;\u0026#34;, \u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34; ] # 配置训练器 trainer = trainers.BpeTrainer( vocab_size=vocab_size, special_tokens=special_tokens, min_frequency=2, # 提高低频词过滤 show_progress=True, initial_alphabet=pre_tokenizers.ByteLevel.alphabet() ) # 训练tokenizer print(f\u0026#34;Training tokenizer with data from {data_path}\u0026#34;) texts = read_texts_from_jsonl(data_path) tokenizer.train_from_iterator(texts, trainer=trainer, length=os.path.getsize(data_path)) # 验证特殊token映射 try: assert tokenizer.token_to_id(\u0026#34;\u0026lt;unk\u0026gt;\u0026#34;) == 0 assert tokenizer.token_to_id(\u0026#34;\u0026lt;s\u0026gt;\u0026#34;) == 1 assert tokenizer.token_to_id(\u0026#34;\u0026lt;/s\u0026gt;\u0026#34;) == 2 assert tokenizer.token_to_id(\u0026#34;\u0026lt;|im_start|\u0026gt;\u0026#34;) == 3 assert tokenizer.token_to_id(\u0026#34;\u0026lt;|im_end|\u0026gt;\u0026#34;) == 4 except AssertionError as e: print(\u0026#34;Special tokens mapping error:\u0026#34;, e) raise # 保存tokenizer文件 tokenizer.save(os.path.join(save_dir, \u0026#34;tokenizer.json\u0026#34;)) # 创建配置文件 create_tokenizer_config(save_dir) print(f\u0026#34;Tokenizer saved to {save_dir}\u0026#34;) def eval_tokenizer(tokenizer_path: str) -\u0026gt; None: *\u0026#34;\u0026#34;\u0026#34;评估tokenizer功能\u0026#34;\u0026#34;\u0026#34;* * *try: tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) except Exception as e: print(f\u0026#34;Error loading tokenizer: {e}\u0026#34;) return # 测试基本属性 print(\u0026#34;\\n=== Tokenizer基本信息 ===\u0026#34;) print(f\u0026#34;Vocab size: {len(tokenizer)}\u0026#34;) print(f\u0026#34;Special tokens: {tokenizer.all_special_tokens}\u0026#34;) print(f\u0026#34;Special token IDs: {tokenizer.all_special_ids}\u0026#34;) # 测试聊天模板 messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一名住在南巷的居民，熟悉小巷生活和街坊习惯。\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;我今天在南巷遇到一只花猫，好可爱！\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;太棒了！南巷的花猫很友好，你给它取名字了吗？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;还没有，你有什么建议吗？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;可以叫它‘巷巷’，听起来很有南巷的感觉。\u0026#34;}, ] print(\u0026#34;\\n=== 聊天模板测试 ===\u0026#34;) prompt = tokenizer.apply_chat_template( messages, tokenize=False, # add_generation_prompt=True ) print(\u0026#34;Generated prompt:\\n\u0026#34;, prompt, sep=\u0026#34;\u0026#34;) # 测试编码解码 print(\u0026#34;\\n=== 编码解码测试 ===\u0026#34;) encoded = tokenizer(prompt, truncation=True, max_length=256) decoded = tokenizer.decode(encoded[\u0026#34;input_ids\u0026#34;], skip_special_tokens=False) print(\u0026#34;Decoded text matches original:\u0026#34;, decoded == prompt) # 测试特殊token处理 print(\u0026#34;\\n=== 特殊token处理 ===\u0026#34;) test_text = \u0026#34;\u0026lt;|im_start|\u0026gt;user\\nHello\u0026lt;|im_end|\u0026gt;\u0026#34; encoded = tokenizer(test_text).input_ids decoded = tokenizer.decode(encoded) print(f\u0026#34;Original: {test_text}\u0026#34;) print(f\u0026#34;Decoded: {decoded}\u0026#34;) print(\u0026#34;Special tokens preserved:\u0026#34;, decoded == test_text) def main(): # 配置路径 data_path = \u0026#34;your data path\u0026#34; save_dir = \u0026#34;tokenizer_k\u0026#34; # 训练tokenizer train_tokenizer( data_path=data_path, save_dir=save_dir, vocab_size=6144 ) # 评估tokenizer eval_tokenizer(save_dir) if __name__ == \u0026#39;__main__\u0026#39;: main() 正在拼命的统计分词\n在训练的过程中，很可能会有较大的内存消耗，我租用的3090服务器90G内存都爆了，好几次，没办法后面换4090的机子，有120G内存，这里就先给大家训练好了词表。也同时放到项目中了\n地址：第二章 动手实现/tokenizer_k\n下面单独给出一段测试代码：\nfrom transformers import AutoTokenizer def eval_tokenizer(tokenizer_path: str) -\u0026gt; None: *\u0026#34;\u0026#34;\u0026#34;评估tokenizer功能\u0026#34;\u0026#34;\u0026#34;* * *try: tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) except Exception as e: print(f\u0026#34;Error loading tokenizer: {e}\u0026#34;) return # 测试基本属性 print(\u0026#34;\\n=== Tokenizer基本信息 ===\u0026#34;) print(f\u0026#34;Vocab size: {len(tokenizer)}\u0026#34;) print(f\u0026#34;Special tokens: {tokenizer.all_special_tokens}\u0026#34;) print(f\u0026#34;Special token IDs: {tokenizer.all_special_ids}\u0026#34;) # 测试聊天模板 messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一名住在南巷的居民，熟悉小巷生活和街坊习惯。\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;我今天在南巷遇到一只花猫，好可爱！\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;太棒了！南巷的花猫很友好，你给它取名字了吗？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;还没有，你有什么建议吗？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;可以叫它‘巷巷’，听起来很有南巷的感觉。\u0026#34;}, ] print(\u0026#34;\\n=== 聊天模板测试 ===\u0026#34;) prompt = tokenizer.apply_chat_template( messages, tokenize=False, # add_generation_prompt=True ) print(\u0026#34;Generated prompt:\\n\u0026#34;, prompt, sep=\u0026#34;\u0026#34;) # 测试编码解码 print(\u0026#34;\\n=== 编码解码测试 ===\u0026#34;) encoded = tokenizer(prompt, truncation=True, max_length=256) print(\u0026#34;Encoded input_ids:\u0026#34;, encoded[\u0026#34;input_ids\u0026#34;]) decoded = tokenizer.decode(encoded[\u0026#34;input_ids\u0026#34;], skip_special_tokens=False) print(\u0026#34;Decoded text matches original:\u0026#34;, decoded == prompt) # 测试特殊token处理 print(\u0026#34;\\n=== 特殊token处理 ===\u0026#34;) test_text = \u0026#34;\u0026lt;|im_start|\u0026gt;user\\nHello\u0026lt;|im_end|\u0026gt;\u0026#34; encoded = tokenizer(test_text).input_ids decoded = tokenizer.decode(encoded) print(f\u0026#34;Original: {test_text}\u0026#34;) print(f\u0026#34;Decoded: {decoded}\u0026#34;) print(\u0026#34;Special tokens preserved:\u0026#34;, decoded == test_text) eval_tokenizer(\u0026#39;/root/StudyLLM/NX_LLM/第二章 动手实现/tokenizer_k\u0026#39;) 结果为：\n我们可以看见，已经成功将文字转换为id，自此，我觉得很有必要回到我绘制的那一张图了：\n是不是感觉突然就懂了，好了，可以回头继续再看一遍是怎么把这个词表做出来的。\nOK，上面我们已经完成了数据处理，词表的训练，全部的基础工作已经做完了，下面到我们核心的各个组件是怎么编写的了。其实也非常的简单，只是将公式，有代码的方式来表达就行。\nRMSNorm（Root Mean Square Normalization） # 聚焦公式 # $y = \\frac{x }{\\sqrt{mean（x^{2}） + \\epsilon } } \\times \\gamma$\n这个过于简单，就不拆解了 # 这个实现起来是最简单的，就算是没有学过深度学习的图像，也能直接秒了：\n代码地址：第二章 动手实现/Code/RMSNorm.py\nimport torch from torch import nn \u0026#34;\u0026#34;\u0026#34; 种归一化有助于通过确保权重的规模不会变得过大或过小来稳定学习过程，这在具有许多层的深度学习模型中特别有用。 \u0026#34;\u0026#34;\u0026#34; class RMSNorm(nn.Module): *\u0026#34;\u0026#34;\u0026#34;* * RMSNorm (Root Mean Square Layer Normalization)* * 归一化方法：* * RMSNorm(x) = x / sqrt(mean(x^2) + eps) * weight* * 与 LayerNorm 的区别：* * - 不减去均值* * - 更轻量，计算效率高* * - 常用于大型 Transformer 模型，如 LLaMA* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, dim: int, eps: float = 1e-8): *\u0026#34;\u0026#34;\u0026#34;* * Args:* * dim (int): 输入特征维度* * eps (float): 避免除以零的微小常数* * \u0026#34;\u0026#34;\u0026#34;* * *super().__init__() self.eps = eps # 可学习的缩放参数 γ，初始化为 1 self.weight = nn.Parameter(torch.ones(dim)) def _rms_norm(self, x: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 核心归一化操作：按最后一个维度计算 RMS* * Args:* * x (torch.Tensor): 输入张量，形状 (..., dim)* * Returns:* * torch.Tensor: 归一化后的张量* * \u0026#34;\u0026#34;\u0026#34;* * *# 计算均方根 rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) # 将输入除以 RMS，实现归一化 return x / rms def forward(self, x: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 前向传播* * Args:* * x (torch.Tensor): 输入张量，形状 (..., dim)* * Returns:* * torch.Tensor: 归一化并缩放后的输出* * \u0026#34;\u0026#34;\u0026#34;* * *# 转为 float 避免低精度问题 x_norm = self._rms_norm(x.float()) # 转回原始数据类型，并乘以可学习权重 γ return x_norm.type_as(x) * self.weight if __name__ == \u0026#34;__main__\u0026#34;: norm = RMSNorm(768, 0.001) x = torch.randn(1, 60, 768) output = norm(x) print(output.shape) 测试结果：\ntorch.Size([1, 60, 768]) Grouped Multi Query Attention # 在这个地方，因为旋转嵌入是直接作用在QK矩阵中，所以，这里我们直接先带着将旋转嵌入的代码写完。再接着将，GQA的代码写。\n下面将整体的框架图绘制好了，我们一步一步实现就行。\n1、旋转嵌入 # 上公式：\n预计算旋转频率\n$\\text{freqs}[t, i] = \\frac{1}{\\theta^{i / (\\text{dim}/2)}} \\cdot t$\n$\\text{cos}[t, i] = \\cos(\\text{freqs}[t, i]), \\quad$ $\\text{sin}[t, i] = \\sin(\\text{freqs}[t, i])$\n$i \\in [0, seqlen - 1]$\n$i \\in [0, \\text{dim}/2 - 1]$\n这是对每个位置和每个偶数维度计算的旋转角度 θ，然后求 cos/sin。\n旋转嵌入应用\n假设 query 的某个 head 的向量拆成实部 ($x_r$) 和虚部 ($x_i$)（每两个维度一组）：\n$ x'_r = x_r \\cdot \\cos\\theta - x_i \\cdot \\sin\\theta$\n$x'_i = x_r \\cdot \\sin\\theta + x_i \\cdot \\cos\\theta$\n这里的 (\\theta = \\text{freqs}[t, i])，对应每个位置 t 和每对维度 i。\n广播对齐\n为了把 cos/sin 张量广播到 [batch, seq_len, n_head, head_dim//2] 的形状：\n$ \\text{freqscosbroad}[b, t, h, i] = \\cos(\\theta_{t,i})$\nb：batch\nt：序列位置\nh：head\ni：每对维度的索引\n所以广播就是在 batch 和 head 维度上复制。\n最终合并\n代码：\nxq_out = torch.stack([xq_out_r, xq_out_i], dim=-1).flatten(3) 公式：\n把每对维度的旋转结果再合并回原始向量：\n$ x'_{q} = flatten([x^{'}_{r}], [x^{'}_{i}])$\n每两个维度一对旋转\nflatten 后恢复原始 head 维度\n代码地址：\n完整的代码为：Code/ROPE.py\nimport torch from typing import Tuple # ----------------------------- # 1计算旋转频率 # ----------------------------- def precompute_rotary_freqs(dim: int, seq_len: int, theta: float = 10000.0) -\u0026gt; Tuple[torch.Tensor, torch.Tensor]: *\u0026#34;\u0026#34;\u0026#34;* * dim: 每个head的维度* * seq_len: 序列长度* * theta: 基数，通常10000* * \u0026#34;\u0026#34;\u0026#34;* * *# dim维度的一半，用于正余弦计算 half_dim = dim // 2 # 频率：1 / theta^(i/dim) freqs = 1.0 / (theta ** (torch.arange(half_dim).float() / half_dim)) # 生成 seq_len x half_dim 的矩阵 t = torch.arange(seq_len).unsqueeze(1) freqs = t * freqs.unsqueeze(0) # 计算正余弦 return torch.cos(freqs), torch.sin(freqs) # ----------------------------- # 2调整频率形状以广播 # ----------------------------- def reshape_for_broadcast(freqs: torch.Tensor, x: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 将 freqs 调整成可以广播到 x 的形状* * \u0026#34;\u0026#34;\u0026#34;* * *shape = [1] * x.ndim shape[1] = freqs.shape[0] # 序列长度 shape[-1] = freqs.shape[1] # head维度 return freqs.view(shape) # ----------------------------- # 3应用旋转嵌入 # ----------------------------- def apply_rotary_emb( xq: torch.Tensor, xk: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor ) -\u0026gt; Tuple[torch.Tensor, torch.Tensor]: *\u0026#34;\u0026#34;\u0026#34;* * xq, xk: [batch, seq_len, n_head, head_dim]* * cos, sin: [seq_len, head_dim//2]* * \u0026#34;\u0026#34;\u0026#34;* * *# 先拆成实部和虚部 xq_r, xq_i = xq.reshape(*xq.shape[:-1], -1, 2).unbind(-1) xk_r, xk_i = xk.reshape(*xk.shape[:-1], -1, 2).unbind(-1) # 调整频率形状以广播 cos = reshape_for_broadcast(cos, xq_r) sin = reshape_for_broadcast(sin, xq_r) # 应用旋转公式 xq_rot = torch.stack([xq_r * cos - xq_i * sin, xq_r * sin + xq_i * cos], dim=-1).flatten(-2) xk_rot = torch.stack([xk_r * cos - xk_i * sin, xk_r * sin + xk_i * cos], dim=-1).flatten(-2) return xq_rot.type_as(xq), xk_rot.type_as(xk) # ----------------------------- # 4示例 # ----------------------------- if __name__ == \u0026#34;__main__\u0026#34;: batch, seq_len, n_head, head_dim = 1, 50, 6, 48 xq = torch.randn(batch, seq_len, n_head, head_dim) xk = torch.randn(batch, seq_len, n_head, head_dim) cos, sin = precompute_rotary_freqs(head_dim, seq_len) xq_out, xk_out = apply_rotary_emb(xq, xk, cos, sin) print(\u0026#34;xq_out shape:\u0026#34;, xq_out.shape) print(\u0026#34;xk_out shape:\u0026#34;, xk_out.shape) 2、复制KV # 代码地址：第二章 动手实现/Code/Attention.py\n下面是进行KV复制的代码操作。\ndef repeat_kv(x: torch.Tensor, n_rep: int) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 重复 key/value 的头，保证与 query 的头数对齐* * x: [batch, seq_len, n_kv_heads, head_dim]* * n_rep: 重复次数* * \u0026#34;\u0026#34;\u0026#34;* * *if n_rep == 1: return x # 不需要重复，直接返回 bs, slen, n_kv_heads, head_dim = x.shape # 在头维度后面加一个维度 x = x[:, :, :, None, :] # [bs, seq_len, n_kv_heads, 1, head_dim] # 扩展这个维度到 n_rep x = x.expand(bs, slen, n_kv_heads, n_rep, head_dim) # [bs, seq_len, n_kv_heads, n_rep, head_dim] # 合并 head 维度和重复维度 x = x.reshape(bs, slen, n_kv_heads * n_rep, head_dim) # [bs, seq_len, n_kv_heads*n_rep, head_dim] return x if __name__ == \u0026#34;__main__\u0026#34;: # 假设 batch=1, seq_len=2, n_kv_heads=2, head_dim=3 x = torch.tensor([ [ [[1,2,3], [4,5,6]], # 序列位置 0 [[7,8,9], [10,11,12]] # 序列位置 1 ] ], dtype=torch.float) print(\u0026#34;原始 x:\u0026#34;) print(x) print(\u0026#34;shape:\u0026#34;, x.shape) # [1, 2, 2, 3] # 重复每个 head 2 次 n_rep = 2 x_repeated = repeat_kv(x, n_rep) print(\u0026#34;\\n重复后的 x:\u0026#34;) print(x_repeated) print(\u0026#34;shape:\u0026#34;, x_repeated.shape) # [1, 2, 4, 3] 可以很明显看到结果：\n3、实现Attention # 这个我们就正常的按照我们的框架图一步一步写就好了，但是这里需要补充的一点就是关于Flash Attention和Mask矩阵的说明。\n对于Flash Attention来说需要PyTorch \u0026gt;= 2.0，一些计算的优化 显著降低自注意力（Self-Attention）的内存占用，同时加速计算。\nFlash Attention 通过 “分块计算 + 逐块 softmax” 来节省内存：\n分块（tiling / chunking）\n不一次生成完整的 $QK^T$矩阵\n将 Q、K、V 按小块（tile）处理，逐步计算每个块的 attention\n逐块 softmax（online softmax）\n逐块计算 softmax，避免把整个 [seq_len, seq_len] 矩阵放到显存里\n使用数值稳定的累积方式计算 softmax\n对于老版本，不支持的，我们手动的去计算Mask矩阵，加入进去就行。\n我们看看公式是怎么操作的：\n$Attention(Q,K,V) = Softmax(\\frac{QK^{T} }{\\sqrt{d_{k} } } + Mask)V$\n就是这么暴力！为什么呢？让我们来看看Mask矩阵：\n大概就是长这个矩阵，来我们结合例子看看为什么这样设计：\n假设原始注意力分数矩阵\n应用 Causal Mask\n似不似so easy！！！\n来来来上代码了。每一行都写的非常细，对着框架图来，就很好写。所以，还是学会绘制框架图比较OK！！！\n代码地址：第二章 动手实现/Code/Attention.py\nclass Attention(nn.Module): def __init__(self, args: ModelConfig): super().__init__() # 我们更具有多少个头的数量来确定需要多少个key 和 value的数量 # 如果没有指定 key/value 的头数，就默认使用 query 的头数 self.n_heads = args.n_heads self.n_kv_heads = args.n_kv_heads if args.n_kv_heads is not None else self.n_heads # 确保总头数可以被键值头数整除。 assert args.n_heads % self.n_kv_heads == 0 # 模型并行处理大小，默认为1。model_parallel_size=1 表示 没有分割，单卡计算如果你用多卡训练，模型会被切分成多个部分并行计算 model_parallel_size = 1 # 本地计算头数，等于总头数除以模型并行处理大小。 self.n_local_heads = args.n_heads // model_parallel_size # 本地键值头数，等于键值头数除以模型并行处理大小。 self.n_local_kv_heads = self.n_kv_heads // model_parallel_size # 重复次数，用于扩展键和值的尺寸。 self.n_rep = self.n_local_heads // self.n_local_kv_heads # 每个头的维度，等于模型维度除以头的总数。 self.head_dim = args.dim // args.n_heads # 开始定义qkv的权重矩阵W这个部分和传统的并未差别 self.wq = nn.Linear(args.dim, args.n_heads * self.head_dim, bias=False) self.wk = nn.Linear(args.dim, args.n_kv_heads * self.head_dim, bias=False) self.wv = nn.Linear(args.dim, args.n_kv_heads * self.head_dim, bias=False) # 最后合并输出权重矩阵，这个时候其实就是说明了wq，wo他们的维度是一样的。 self.wo = nn.Linear(args.n_heads * self.head_dim, args.dim, bias=False) # 定义dropout，防止过拟合，这里分为两个，一个是注意力计算过程中的，一个是输出矩阵中的 self.attn_dropout = nn.Dropout(args.dropout) self.output_dropout = nn.Dropout(args.dropout) self.dropout = args.dropout # 上面我们都定义好了，开始到计算环节了，这里我们环境是满足Flash Attention self.flash = hasattr(torch.nn.functional, \u0026#39;scaled_dot_product_attention\u0026#39;) if not self.flash: # 若不支持Flash Attention，则使用手动实现的注意力机制，并设置mask，这个手动实现也是非常简单的 print(\u0026#34;WARNING: using slow attention. Flash Attention requires PyTorch \u0026gt;= 2.0\u0026#34;) # 创建一个上三角矩阵，用于遮蔽未来信息。这个-inf就已经代表负无穷。 mask = torch.full((1, 1, args.max_seq_len, args.max_seq_len), float(\u0026#34;-inf\u0026#34;)) mask = torch.triu(mask, diagonal=1) # 注册为模型的缓冲区 self.register_buffer(\u0026#34;mask\u0026#34;, mask) # 开始真正的撸流程了，按照框架图一步一步的编写就行。 def forward(self, x: torch.Tensor, freqs_cos: torch.Tensor, freqs_sin: torch.Tensor): *\u0026#34;\u0026#34;\u0026#34;* * 前向传播计算注意力。* * x: [batch_size, seq_len, dim] 输入特征* * freqs_cos, freqs_sin: 旋转位置嵌入（RoPE）预计算的 cos 和 sin* * \u0026#34;\u0026#34;\u0026#34;* * *# 获取输入形状 batch_size, seq_len, dim = x.shape # xq/xk/xv 要求 shape: [batch, seq_len, n_heads, head_dim] # ----------------------------- # 计算 Q/K/V 矩阵 # ----------------------------- # 通过线性变换生成查询（Q）、键（K）、值（V） xq, xk, xv = self.wq(x), self.wk(x), self.wv(x) # 调整形状以适应多头注意力 # Q 头数 = n_local_heads xq = xq.reshape(batch_size, seq_len, self.n_local_heads, self.head_dim) # K/V 头数 = n_local_kv_heads xk = xk.reshape(batch_size, seq_len, self.n_local_kv_heads, self.head_dim) xv = xv.reshape(batch_size, seq_len, self.n_local_kv_heads, self.head_dim) # 应用旋转位置嵌入（RoPE） # 将 Q/K 的向量旋转编码位置信息，使模型可以感知序列位置 xq, xk = apply_rotary_emb(xq, xk, freqs_cos, freqs_sin) # 扩展 K/V 头以匹配 Q 头 # 当 KV 头数 \u0026lt; Q 头数时，需要重复 KV 头 xk = repeat_kv(xk, self.n_rep) xv = repeat_kv(xv, self.n_rep) # 转置为 [batch, heads, seq_len, head_dim] # 方便矩阵乘法和注意力计算 xq = xq.transpose(1, 2) xk = xk.transpose(1, 2) xv = xv.transpose(1, 2) # 计算注意力 if self.flash: # 如果支持 Flash Attention（PyTorch \u0026gt;= 2.0），使用高效实现 # is_causal=True 保证未来信息不可见（自回归） output = torch.nn.functional.scaled_dot_product_attention( xq, xk, xv, attn_mask=None, dropout_p=self.dropout if self.training else 0.0, is_causal=True ) else: # 手动实现注意力计算（慢版本） # 计算注意力分数: QK^T / sqrt(head_dim) scores = torch.matmul(xq, xk.transpose(2, 3)) / math.sqrt(self.head_dim) # 添加 causal mask 遮蔽未来信息 assert hasattr(self, \u0026#39;mask\u0026#39;) scores = scores + self.mask[:, :, :seq_len, :seq_len] # softmax 得到注意力权重 scores = F.softmax(scores.float(), dim=-1).type_as(xq) # dropout 防止过拟合 scores = self.attn_dropout(scores) # 加权求和得到输出 output = torch.matmul(scores, xv) # 恢复时间维度并合并头 # output shape: [batch, heads, seq_len, head_dim] -\u0026gt; [batch, seq_len, heads*head_dim] output = output.transpose(1, 2).contiguous().reshape(batch_size, seq_len, -1) # 输出投影回模型维度并 dropout output = self.wo(output) output = self.output_dropout(output) return output 我们从三张图（单个 head heatmap、平均 head heatmap、某个 query token 的条形图）是三种不同角度观察注意力分布（attention distribution）的方式。它们分别揭示模型在「注意力机制」中的不同层面的信息。\n单个 head 的注意力矩阵 (plot_attention_matrix) 图像特征：\nx 轴（横轴）：Key 位置（即模型“被注意”的 token）\ny 轴（纵轴）：Query 位置（即模型“在看”的 token）\n颜色深浅：注意力权重大小（颜色越深，表示越关注）\n含义解释：\n这张图反映了：\n某个注意力头（head）在不同 token 间建立了怎样的依赖关系。\n每个 head 在 transformer 里通常学习到不同的“模式”：\n有的 head 只看「上一个 token」（语言模型常见）；\n有的 head 看「句首」或特殊符号（比如 \u0026lt;BOS\u0026gt;、段落标记）；\n有的 head 看「动词」或「名词」等关键位置；\n有的 head 看「句尾」或全局平均。\n所以这张图能帮我们理解：\n这个具体的 head 在学习过程中，捕捉到了怎样的结构或依赖。\n举例：\n假设我们输入一句话（被分成 token）：\n“The cat sat on the mat”\n如果第 1 个 head 的图在“on”那一行，对应“the mat”列特别亮，\n说明这个 head 在预测 “on” 时重点关注了 “the mat” ——\n它可能在捕捉语法结构（介词短语）。\n平均所有 head 的注意力 (plot_avg_attention) 图像特征：\n同样是 heatmap（x 轴 = key，y 轴 = query）\n但这里的每个像素 = 所有 head 的平均注意力权重。\n含义解释：\n这是整个多头注意力的「总体关注模式」。\n它会淡化个别 head 的差异，展示出整体趋势：\n若图像接近对角线 → 模型主要关注“临近 token”（局部依赖）\n若图像有远离对角线的亮条 → 模型存在“长程注意力”（远距依赖）\n若上三角暗、下三角亮 → 说明是“因果注意力”（只能看过去）\n举例：\n语言模型通常是“因果掩码（causal mask）”形式，\n所以这张平均图的下三角会亮，对角线附近最亮，\n表示每个 token 主要注意自己和之前的 token。\n单个 query token 的注意力分布 (plot_attention_for_query_token) 图像特征：\nx 轴：Key 的位置（序列中所有 token）\ny 轴：注意力权重大小（条形图高度）\n含义解释：\n这张图展示「某一个特定 query token」在看哪些 key 时最关注。\n换句话说：\n当模型在处理第 query_pos 个 token 时， 它把注意力主要放在了哪些过去的 token 上。\n举例：\n假设我们分析句子：\n“The cat sat on the mat”\n如果我们画第 5 个 token（“mat”）的注意力分布：\n可能看到 “the” 和 “on” 位置权重较高 → 说明模型在理解 “mat” 时依赖了 “the” 和 “on”。\n如果某些远处 token（比如句首 “The”）也亮 → 表示模型捕捉到了长距离依赖。\nif __name__ == \u0026#34;__main__\u0026#34;: import os from pathlib import Path import numpy as np import matplotlib.pyplot as plt import torch import math import torch.nn.functional as F # ---------- 绘图工具 ---------- def _to_numpy(t: torch.Tensor) -\u0026gt; np.ndarray: *\u0026#34;\u0026#34;\u0026#34;把 tensor 转成 numpy（先 detach、cpu）。\u0026#34;\u0026#34;\u0026#34;* * *if isinstance(t, torch.Tensor): return t.detach().cpu().numpy() return np.array(t) def plot_attention_matrix(weights, batch_idx=0, head_idx=0, title=None, show=True, fname=None): *\u0026#34;\u0026#34;\u0026#34;* * 画单个 head 的注意力矩阵（heatmap）。* * weights: [B, H, L, L] 的 numpy 或 torch tensor* * \u0026#34;\u0026#34;\u0026#34;* * *w = _to_numpy(weights) mat = w[batch_idx, head_idx] # [L, L] plt.figure(figsize=(6, 5)) plt.imshow(mat, aspect=\u0026#34;auto\u0026#34;) plt.xlabel(\u0026#34;Key position\u0026#34;) plt.ylabel(\u0026#34;Query position\u0026#34;) plt.title(title or f\u0026#34;Batch {batch_idx} Head {head_idx}\u0026#34;) plt.colorbar() if fname: plt.savefig(fname, bbox_inches=\u0026#34;tight\u0026#34;) if show: plt.show() plt.close() def plot_avg_attention(weights, batch_idx=0, title=None, show=True, fname=None): *\u0026#34;\u0026#34;\u0026#34;* * 所有 head 平均后的 attention heatmap。* * \u0026#34;\u0026#34;\u0026#34;* * *w = _to_numpy(weights) mat = w[batch_idx].mean(axis=0) # [L, L] plt.figure(figsize=(6, 5)) plt.imshow(mat, aspect=\u0026#34;auto\u0026#34;) plt.xlabel(\u0026#34;Key position\u0026#34;) plt.ylabel(\u0026#34;Query position\u0026#34;) plt.title(title or f\u0026#34;Batch {batch_idx} AvgHeads\u0026#34;) plt.colorbar() if fname: plt.savefig(fname, bbox_inches=\u0026#34;tight\u0026#34;) if show: plt.show() plt.close() def plot_attention_for_query_token(weights, query_pos, batch_idx=0, head_idx=0, show=True, fname=None): *\u0026#34;\u0026#34;\u0026#34;* * 绘制某个 query_pos 的 attention distribution（对所有 key positions）。* * weights: [B, H, L, L]* * \u0026#34;\u0026#34;\u0026#34;* * *w = _to_numpy(weights) vec = w[batch_idx, head_idx, query_pos] # 长度 L plt.figure(figsize=(8, 2)) plt.bar(np.arange(len(vec)), vec) plt.xlabel(\u0026#34;Key position\u0026#34;) plt.ylabel(\u0026#34;Attention weight\u0026#34;) plt.title(f\u0026#34;Batch {batch_idx} Head {head_idx} QueryPos {query_pos}\u0026#34;) if fname: plt.savefig(fname, bbox_inches=\u0026#34;tight\u0026#34;) if show: plt.show() plt.close() # ---------- 计算 attention weights（slow path，用于可视化） ---------- @torch.no_grad() def compute_attn_weights_for_vis(xq: torch.Tensor, xk: torch.Tensor, mask: torch.Tensor | None = None, head_dim: int | None = None) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 输入：* * xq, xk: [B, H, L, D]* * mask: optional causal mask with shape broadcastable to [B, H, L, L] (can be None)* * head_dim: D，若为 None 则从 xq.shape[-1] 读取* * 返回：* * weights: [B, H, L, L]（CPU tensor，便于绘图）* * \u0026#34;\u0026#34;\u0026#34;* * *if head_dim is None: head_dim = xq.shape[-1] scores = torch.matmul(xq, xk.transpose(-2, -1)) / math.sqrt(head_dim) # [B,H,L,L] if mask is not None: # 确保 mask 与 scores 在同一设备 / dtype，然后相加 if mask.device != scores.device: mask = mask.to(scores.device) scores = scores + mask weights = F.softmax(scores.float(), dim=-1) return weights.detach().cpu() # ---------- 准备模型与输入 ---------- args = ModelConfig(dim=64, n_heads=4, n_kv_heads=2, dropout=0.0, max_seq_len=64) attention_model = Attention(args) B = 1 L = 20 x = torch.rand(B, L, args.dim) # 你之前有 precompute_rotary_freqs 或 precompute_freqs_cis，请确保导入了名称一致的函数 freqs_cos, freqs_sin = precompute_rotary_freqs(args.dim // args.n_heads, L) # 正常前向（可能使用 flash），仅获得输出（不用于可视化） out = attention_model(x, freqs_cos, freqs_sin) # ---------- 单独计算 Q/K 并算权重（slow path，仅用于可视化） ---------- with torch.no_grad(): # 生成线性层输出并 reshape 为 [B, L, H, D]（与 forward 中相同的顺序） xq = attention_model.wq(x).reshape(B, L, attention_model.n_local_heads, attention_model.head_dim) xk = attention_model.wk(x).reshape(B, L, attention_model.n_local_kv_heads, attention_model.head_dim) # 应用 RoPE 与 KV 重复（与 forward 保持一致） xq, xk = apply_rotary_emb(xq, xk, freqs_cos, freqs_sin) xk = repeat_kv(xk, attention_model.n_rep) # 转置为 [B, H, L, D] xq = xq.transpose(1, 2) xk = xk.transpose(1, 2) # 可选 mask（若模型使用 slow path 时注册了 mask） mask = None if hasattr(attention_model, \u0026#34;mask\u0026#34;): # mask 形状为 [1,1,max_seq,max_seq]，裁剪到实际长度并广播 mask = attention_model.mask[:, :, :L, :L] # 计算并得到 CPU 上的权重张量 [B, H, L, L] weights = compute_attn_weights_for_vis(xq, xk, mask=mask, head_dim=attention_model.head_dim) # ---------- 保存 / 绘图 ---------- out_dir = Path(\u0026#34;attn_figs\u0026#34;) out_dir.mkdir(parents=True, exist_ok=True) plot_attention_matrix(weights, batch_idx=0, head_idx=0, fname=str(out_dir / \u0026#34;head0.png\u0026#34;)) plot_avg_attention(weights, batch_idx=0, fname=str(out_dir / \u0026#34;avg_heads.png\u0026#34;)) plot_attention_for_query_token(weights, query_pos=3, batch_idx=0, head_idx=0, fname=str(out_dir / \u0026#34;query3_head0.png\u0026#34;)) print(f\u0026#34;Saved attention figures to {out_dir.resolve()}\u0026#34;) 前馈网络（Feed Forward Network, FFN） # 说明：LLaMA / LLaMA-2 的 MLP 本质上是一个 gated feed-forward：先做一个线性投影到 2×hidden，分成两部分，一部分过激活函数（SiLU/GELU），再与另一部分相乘（gating），最后投影回 output_dim。常见叫法有 SwiGLU / GEGLU / GLU。下实现支持这些变体。\n下面，我们仍然从数学公式开始入手：\n1、数学公式 # 输入 $x \\in \\mathbb{R}^{B\\times L \\times D}$：\n先线性投影： $ [u, v] = xW_1 + b_1 \\quad \\text{with } [u,v]\\in\\mathbb{R}^{B\\times L \\times 2H}$\n分成两块$u,v \\in \\mathbb{R}^{B\\times L \\times H}$：\n计算 gated activation（以 SwiGLU 为例）：\n$ h = \\mathrm{SiLU}(u)\\odot v$\n输出投影： $ \\text{out} = h W_2 + b_2$\n太简单了，直接上代码 # 是不是感觉好像什么地方见过，没错，就是一个非常非常简单的多层感知机架构。\n我们直接上代码：\n代码地址：第二章 动手实现/Code/MLP.py\nimport torch import torch.nn.functional as F from torch import nn from ModelConfig import ModelConfig class LlamaMLP(nn.Module): *\u0026#34;\u0026#34;\u0026#34;* * 输入维度：dim* * 中间维度：hidden_dim（若未指定，则自动计算为 4/3 * dim，并取 multiple_of 的倍数）* * 结构：* * x -\u0026gt; W1 -\u0026gt; SiLU() 激活* * x -\u0026gt; W3 -\u0026gt; gating 通道* * 二者逐元素相乘 (SwiGLU)* * -\u0026gt; W2 -\u0026gt; Dropout -\u0026gt; 输出* * 等价于：output = Dropout(W2(SiLU(W1(x)) * W3(x)))* * \u0026#34;\u0026#34;\u0026#34;* * *def __init__(self, dim: int, hidden_dim: int = None, multiple_of: int = 256, dropout: float = 0.0): super().__init__() # 若未指定 hidden_dim，则： # 1. 取输入维度 dim 的 4 倍； # 2. 再缩小为 2/3； # 3. 调整为 multiple_of 的整数倍（例如 256 的倍数）。 if hidden_dim is None: hidden_dim = 4 * dim hidden_dim = int(2 * hidden_dim / 3) hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) # -------------------------------------------- # 定义线性层 # -------------------------------------------- # W1：主通道（经过 SiLU 激活） self.fc_gate = nn.Linear(dim, hidden_dim, bias=False) # W3：门控通道（直接相乘） self.fc_up = nn.Linear(dim, hidden_dim, bias=False) # W2：输出投影层（回到原始维度） self.fc_down = nn.Linear(hidden_dim, dim, bias=False) # Dropout 防止过拟合 self.dropout = nn.Dropout(dropout) def forward(self, x: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 前向传播：* * Args:* * x: [batch_size, seq_len, dim]* * Returns:* * output: [batch_size, seq_len, dim]* * \u0026#34;\u0026#34;\u0026#34;* * *# 主通道经过 SiLU 激活 gated = F.silu(self.fc_gate(x)) # [B, L, hidden_dim] # 门控通道（线性变换） up = self.fc_up(x) # [B, L, hidden_dim] # SwiGLU 核心：逐元素相乘 hidden = gated * up # [B, L, hidden_dim] # 输出投影 + Dropout output = self.fc_down(hidden) output = self.dropout(output) return output # ------------------------------------------------- # 测试示例 # ------------------------------------------------- if __name__ == \u0026#34;__main__\u0026#34;: args = ModelConfig() # 创建 MLP 模块实例 mlp = LlamaMLP( dim=args.dim, hidden_dim=args.hidden_dim, multiple_of=args.multiple_of, dropout=args.dropout ) # 随机输入 [batch=1, seq_len=50, dim] x = torch.randn(1, 50, args.dim) # 前向计算 output = mlp(x) print(f\u0026#34;输入形状: {x.shape}\u0026#34;) print(f\u0026#34;输出形状: {output.shape}\u0026#34;) Decoder Layer # 回顾整体框架图 # 我们回顾一下整个网络架构的图，到目前为止，基本所有的组件，我们都一步一步的实现好了。现在就到我们开始组装整个模型了，但是我们还有一步需要完成，就是可以重复的嵌套的Decoder层。这个就比较简单了，因为Decoder层的输入输出的维度都是一样的，所以，只需一点点技巧就可以完整的实现。\n下面我们就开始着手开始组装。\n组装Decoder层 # 代码地址：Code/DecoderLayer.py\nimport torch.nn.functional as F import math import torch from torch import nn from ModelConfig import ModelConfig from Attention import Attention from MLP import LlamaMLP from RMSNorm import RMSNorm from ROPE import * class DecoderLayer(nn.Module): def __init__(self, layer_id: int, args: ModelConfig): super().__init__() # 下面我们按照框架图一步一步来就行 # 定义输入特征维度 self.dim = args.dim # 定义有几个多头注意力头 self.n_heads = args.n_heads # 定义每个头的维度，等于输入维度除以头数,这个要千万注意 self.head_dim = args.dim // args.n_heads # 定义注意力机制 self.attention = Attention(args) # 定义FFN self.FFN = LlamaMLP( dim=args.dim, hidden_dim=args.hidden_dim, multiple_of=args.multiple_of, dropout=args.dropout, ) # 设置层id self.layer_id = layer_id # 定义注意力计算的归一化层 self.attention_norm = RMSNorm(args.dim, eps=args.norm_eps) # 定义前馈神经网络计算的归一化层 self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps) def forward(self, x: torch.Tensor, freqs_cos: torch.Tensor, freqs_sin: torch.Tensor) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * 单层解码器前向（pre-norm 风格）* * Args:* * x: [batch_size, seq_len, dim] 输入张量（残差输入）* * freqs_cos, freqs_sin: RoPE 预计算的 cos / sin，shape 与 Attention 要求一致* * Returns:* * out: [batch_size, seq_len, dim] 经过本层处理后的输出（已加残差）* * \u0026#34;\u0026#34;\u0026#34;* * *# 先对输入做 RMSNorm（归一化），再传入 Attention 计算 # 注意：attention 接受的是原始 x 的归一化版本（x_norm） # Shapes: # x -\u0026gt; [B, L, D] # x_norm -\u0026gt; [B, L, D] x_norm = self.attention_norm(x) # attention 返回 [B, L, D] attn_out = self.attention(x_norm, freqs_cos, freqs_sin) # 残差连接：将 attention 的输出加回到原始输入 x = x + attn_out # 对残差后的 x 做 RMSNorm，再输入到 FFN（LlamaMLP） # Shapes: # x -\u0026gt; [B, L, D] # x_ffn_norm -\u0026gt; [B, L, D] x_ffn_norm = self.ffn_norm(x) # FFN 返回 [B, L, D] ffn_out = self.FFN(x_ffn_norm) # 残差连接：将 FFN 的输出加回 x = x + ffn_out # 返回本层最终输出 return x if __name__ == \u0026#34;__main__\u0026#34;: args = ModelConfig() decoderlayer = DecoderLayer(0, args) # 模拟输入数据 dim = args.dim seq_len = 50 x = torch.randn(1, seq_len, dim) # [bs, seq_len, dim] freqs_cos, freqs_sin = precompute_rotary_freqs(dim // args.n_heads, seq_len) out = decoderlayer(x, freqs_cos, freqs_sin) print(out.shape) # 形状和输入的x一样 [batch_size, seq_len, dim] 到此基本的都搭建完成！！！\n等等，是不是还有一个问题，ModelConfig这个是什么玩意？\nModelConfig # ModelConfig作为全局统一配置的参数。这里详细的介绍一下。\n代码地址：第二章 动手实现/Code/ModelConfig.py\nfrom transformers import PretrainedConfig class ModelConfig(PretrainedConfig): model_type = \u0026#34;Tiny-K\u0026#34; def __init__(self, dim: int = 384, # 模型维度 n_layers: int = 6, # Transformer层数 n_heads: int = 8, # 多头 n_kv_heads: int = 4, # kv数量 vocab_size: int = 6144, # 词汇表大小 hidden_dim: int = None, # 隐藏层大小 multiple_of: int = 64, # 隐藏层对齐数 norm_eps: float = 1e-5, # 归一化层的平滑项 max_seq_len: int = 256, # 最大序列长度 dropout: float = 0.0, flash_attn: bool = True, # 是否使用Flash Attention **kwargs, ): self.dim = dim self.n_layers = n_layers self.n_heads = n_heads self.n_kv_heads = n_kv_heads self.vocab_size = vocab_size self.hidden_dim = hidden_dim self.multiple_of = multiple_of self.norm_eps = norm_eps self.max_seq_len = max_seq_len self.dropout = dropout self.flash_attn = flash_attn super().__init__(**kwargs) dim: int = 384\n模型的隐藏维度（embedding dimension / model width），通常记为 D。\n影响：Embedding 参数量（vocab_size × dim）、所有线性层的输入/输出维度、每层计算量和显存。\nn_layers: int = 6\nTransformer 层数（stacked decoder/encoder 层数）。\n影响：模型深度、参数量（层数越多参数越多）、推理/训练时间。\nn_heads: int = 8\nQuery 的头数（总 head 数）。\n影响：每个 head 的维度 head_dim = dim // n_heads（因此要能整除），query/projection 的矩阵形状、并行度。\nn_kv_heads: int = 4\nkey/value 的头数（可以与 n_heads 不同）。有些实现为了节省计算/参数，KV 头数少于 Q 头数（然后通过 repeat_kv 把 KV 复制到 Q 的头数）。\n影响：如果 n_kv_heads \u0026lt; n_heads，需要额外逻辑（像你前面的 repeat_kv），并且要保证 n_heads % n_kv_heads == 0（常见约束），否则头重复次数不是整数，会出问题。\nvocab_size: int = 6144\n词表大小，影响 embedding 矩阵大小（vocab_size × dim），直接影响显存与参数量。 hidden_dim: int = None\nMLP 中间层维度（通常 4 * dim 或 4/3 * dim，取决实现）。\nNone 表示让模型在运行时或构造时计算默认值（你的代码里只是保存了 None；建议在模型内部或 config 初始化时计算并保存最终值）。\nmultiple_of: int = 64\n用于把 hidden_dim 向上对齐到 multiple_of 的倍数（这在 LLaMA/一些实现里用于内核/张量对齐以提高性能）。\n举例：如果 hidden_dim 计算结果为 2458，multiple_of=64，则会向上取到 64 * ceil(2458/64)。\nnorm_eps: float = 1e-5\nLayerNorm / RMSNorm 的 eps 值，用于数值稳定性（避免除以 0）。\n小数值常见：1e-5、1e-6 等。\nmax_seq_len: int = 256\n模型能处理的最大序列长度，常用于预构造 causal mask 或 RoPE 的频率表长度等。\n注意：如果输入序列超过这个长度且代码没做动态处理，会报错或截断。\ndropout: float = 0.0\ndropout 概率（训练时），用于 MLP 或 attention 的 dropout。\n在大型语言模型训练/推理阶段常为 0（或小值）。\nflash_attn: bool = True\n指示是否优先使用 Flash Attention（PyTorch \u0026gt;= 2.0 的 scaled_dot_product_attention 或其他高效 kernel）。\n如果 True，在支持的环境会选择 faster kernel；如果 False，走慢实现以便获取权重或兼容旧版本。\n**kwargs\n任何额外传入的参数都会被 PretrainedConfig 捕获并纳入序列化/反序列化（这也是 HuggingFace 生态常用的做法，便于扩展字段而不破坏兼容性）。 上面的参数配置，已经压缩都非常非常小了。属于是ok单人套餐了。\n下面就开始实现完整的LLaMA2模型。\n手撸完整LLaMA2模型 # 我们依然对着完整的框架图：\n框架图稍微变动了一下，最后输出采用CausalLMOutputWithPast。本质上和softmax没有什么区别，但是它有很多可以扩展的东西可以选择：\n预测的 logits\n模型输出的原始未归一化概率分数（即 softmax 前的向量）。\n形状通常为 [batch_size, seq_len, vocab_size]。\n用于生成下一个 token 的概率或计算损失。\npast_key_values（可选）\n对于多层 Transformer，保存每一层的注意力缓存（Key/Value）。\n形状大致为 [layer][2][batch, num_heads, seq_len, head_dim]。\n作用：在生成长序列时无需重新计算历史上下文，提高推理效率（尤其在 autoregressive decoding 中）。\nhidden_states（可选）\n每一层 Transformer 的隐藏状态。\n用于分析或微调模型中间层特征。\nattentions（可选）\n每一层的注意力权重（可用于可视化）。\n形状 [batch, num_heads, seq_len, seq_len]。\n代码地址：第二章 动手实现/Code/LLaMA2.py\nfrom typing import Optional import math import torch from torch import nn from transformers import PreTrainedModel from transformers.modeling_outputs import CausalLMOutputWithPast import torch.nn.functional as F from ModelConfig import ModelConfig from DecoderLayer import DecoderLayer from RMSNorm import RMSNorm from ROPE import * class Transformer(PreTrainedModel): *\u0026#34;\u0026#34;\u0026#34;* * 自回归 Transformer 模型（类似 GPT/LLaMA 架构）* * - 支持多层 Decoder* * - 使用 Rotary Positional Embedding* * - 可共享词嵌入与输出投影* * - 输出 CausalLMOutputWithPast 类型* * \u0026#34;\u0026#34;\u0026#34;* * *# 注入配置类 config_class = ModelConfig # 记录最后一次计算的损失 last_loss: Optional[torch.Tensor] def __init__(self, args: ModelConfig): super().__init__(args) # 初始化模型参数 self.args = args # 词汇表大小,这个大小必须是和之前我们训练的Tokenizer里面一样，不然id会错乱 self.vocab_size = args.vocab_size # 层数 self.n_layers = args.n_layers # 词嵌入层，这一步是将我们id变为向量的形式，这里可以联想一下独热编码 self.tok_embeddings = nn.Embedding(args.vocab_size, args.dim) # Dropout层 self.dropout = nn.Dropout(args.dropout) # Decoder层，这里通过短短的三行代码就实现对Decoder层的遍历嵌套 self.layers = torch.nn.ModuleList() for layer_id in range(args.n_layers): self.layers.append(DecoderLayer(layer_id, args)) # 归一化层 self.rmsnorm = RMSNorm(args.dim, eps=args.norm_eps) # 输出层 self.output = nn.Linear(args.dim, args.vocab_size, bias=False) # 将词嵌入层的权重与输出层的权重共享 self.tok_embeddings.weight = self.output.weight # 相对位置嵌入的频率 freqs_cos, freqs_sin = precompute_rotary_freqs(self.args.dim // self.args.n_heads, self.args.max_seq_len) self.register_buffer(\u0026#34;freqs_cos\u0026#34;, freqs_cos, persistent=False) self.register_buffer(\u0026#34;freqs_sin\u0026#34;, freqs_sin, persistent=False) # 初始化所有权重 self.apply(self._init_weights) # 对残差投影进行特殊的缩放初始化 for pn, p in self.named_parameters(): if pn.endswith(\u0026#39;w3.weight\u0026#39;) or pn.endswith(\u0026#39;wo.weight\u0026#39;): torch.nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * args.n_layers)) # 初始化最后一次前向传播的损失属性 self.last_loss = None self.OUT = CausalLMOutputWithPast() # 输出容器 self._no_split_modules = [name for name, _ in self.named_modules()] # 不分割的模块列表 def _init_weights(self, module): # 初始化权重的函数 if isinstance(module, nn.Linear): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) if module.bias is not None: torch.nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) def forward(self, tokens: torch.Tensor, targets: Optional[torch.Tensor] = None, **kwargs) -\u0026gt; torch.Tensor: *\u0026#34;\u0026#34;\u0026#34;* * - tokens: Optional[torch.Tensor], 输入 token 张量。* * - targets: Optional[torch.Tensor], 目标 token 张量。* * - kv_cache: bool, 是否使用键值缓存。* * - kwargs: 其他关键字参数。* * - self.OUT: CausalLMOutputWithPast, 包含 logits 和损失。* * \u0026#34;\u0026#34;\u0026#34;* * *# 处理输入别名 if \u0026#39;input_ids\u0026#39; in kwargs: tokens = kwargs[\u0026#39;input_ids\u0026#39;] if \u0026#39;attention_mask\u0026#39; in kwargs: targets = kwargs[\u0026#39;attention_mask\u0026#39;] # 前向传播函数 _bsz, seqlen = tokens.shape # 通过词嵌入层和Dropout层 h = self.tok_embeddings(tokens) h = self.dropout(h) # 获取相对位置嵌入的频率 freqs_cos = self.freqs_cos[:seqlen] freqs_sin = self.freqs_sin[:seqlen] # 通过Decoder层 for layer in self.layers: h = layer(h, freqs_cos, freqs_sin) # 通过归一化层 h = self.rmsnorm(h) if targets is not None: # 如果给定了目标，计算损失 logits = self.output(h) self.last_loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=0, reduction=\u0026#39;none\u0026#39;) else: # 推理时的小优化：只对最后一个位置的输出进行前向传播 logits = self.output(h[:, [-1], :]) self.last_loss = None # 设置输出 self.OUT.__setitem__(\u0026#39;logits\u0026#39;, logits) self.OUT.__setitem__(\u0026#39;last_loss\u0026#39;, self.last_loss) return self.OUT @torch.inference_mode() def generate(self, idx, stop_id=None, max_new_tokens=256, temperature=1.0, top_k=None): *\u0026#34;\u0026#34;\u0026#34;* * 逐步生成序列（采样版本）* * - idx: [batch_size, seq_len] 输入序列* * - stop_id: 停止 token id* * - max_new_tokens: 最大生成长度* * - temperature: 采样温度* * - top_k: top-k 采样* * \u0026#34;\u0026#34;\u0026#34;* * *index = idx.shape[1] for _ in range(max_new_tokens): # 如果序列上下文过长，截断它到最大长度 idx_cond = idx if idx.size(1) \u0026lt;= self.args.max_seq_len else idx[:, -self.args.max_seq_len:] # 前向传播获取序列中最后一个位置的 logits logits = self(idx_cond).logits logits = logits[:, -1, :] # 只保留最后一个时间步的输出 if temperature == 0.0: # 选择最有可能的索引 _, idx_next = torch.topk(logits, k=1, dim=-1) else: # 缩放 logits 并应用 softmax logits = logits / temperature if top_k is not None: v, _ = torch.topk(logits, min(top_k, logits.size(-1))) logits[logits \u0026lt; v[:, [-1]]] = -float(\u0026#39;Inf\u0026#39;) probs = F.softmax(logits, dim=-1) idx_next = torch.multinomial(probs, num_samples=1) if idx_next == stop_id: break # 将采样的索引添加到序列中并继续 idx = torch.cat((idx, idx_next), dim=1) # 只返回生成的token return idx[:, index:] if __name__ == \u0026#34;__main__\u0026#34;: def count_parameters(model): *\u0026#34;\u0026#34;\u0026#34;* * 统计模型中可训练参数的数量* * Args:* * model: PyTorch模型* * Returns:* * int: 可训练参数总数* * \u0026#34;\u0026#34;\u0026#34;* * *return sum(p.numel() for p in model.parameters() if p.requires_grad) args = ModelConfig() # LLaMA2Model.forward 接受两个参数，tokens和targets，其中tokens是输入的张量, 应为int类型 x = torch.randint(0, 6144, (64, 256)) # [bs, seq_len] # 实例化LLaMA2Model model = Transformer(args=args) # 计算model的全部参数 print(f\u0026#39;LLM总参数量：{count_parameters(model) / 1e6:.3f} 百万\u0026#39;) out = model(x) print(out.logits.shape) # [batch_size, 1, vocab_size] 完结，撒花！！！\n到此，完整的网络框架搭建完成。\n后续就是更为重要的预训练过程与SFT训练。\n","date":"2026/06/14","externalUrl":null,"permalink":"/blog/llama2-hands-on-implementation/","section":"日常记录、技术札记与转载收藏。","summary":"围绕 LLaMA 2 的数据集、Tokenizer、模型结构与训练代码，记录从原理拆解到工程实现的完整实践过程。","title":"LLaMA 2 动手实现","type":"blog"},{"content":" 我们通过学习LLama2模型，来逐步的将这个模型搭建起来，包含各个组件，以及做预训练，SFT训练，模型推理对话，等全套流程。\n模型架构 # 总体概述 # LLaMA 2 是典型的 Decoder-only Transformer（自回归语言模型），核心思想与 GPT 系列类似，但在细节上有所优化，特别针对大规模训练和高效推理。\n输入：tokenized 文本序列\n输出：预测下一个 token 的概率分布\n总体架构可以概括为：\nInput tokens \u0026ndash;\u0026gt; Token Embedding + Positional Encoding \u0026ndash;\u0026gt; N x Transformer Block \u0026ndash;\u0026gt; LM Head \u0026ndash;\u0026gt; Output logits\n下面，我将从输入到输出，数据经过各个组件，一一讲解，多讲解为什么，为什么要这样设计\nEmbeddings组件 # 将输入的 token（数字化）映射为向量表示,这里需要了解一下Tokenizer这个模型。\nTokenizer 的任务是：\n将文本转成 token（数字索引）\n将 token 转回文本\n注意：这个映射必须覆盖模型可能遇到的所有单词、子词甚至符号，否则模型训练和推理都会出现未知 token。\n在Llama2中使用的是BPE（Byte Pair Encoding），在我们的实际项目中使用transformers库里面的AutoTokenizer，以及tokenizers库。\n这里我们举例子看看BPE是怎么工作的。\n使用：中文例子 “我爱南巷的花猫” 和 “我爱南巷” 来演示 BPE 编码过程。\n原始文本 # 我爱南巷的花猫 我爱南巷 初始化词表 # 每个汉字作为初始 token\n加上结束符 \u0026lt;\\w\u0026gt;\n初始词表：\n我, 爱, 南, 巷, 的, 花, 猫, \u0026lt;\\w\u0026gt; 文本拆分为 token 序列：\n我 爱 南 巷 的 花 猫 \u0026lt;\\w\u0026gt; 我 爱 南 巷 \u0026lt;\\w\u0026gt; 统计频率并合并（BPE 训练（统计）） # 步骤 1：统计最频繁相邻 token\n合并最频繁 pair → 我 爱\n更新文本：\n我爱 南 巷 的 花 猫 \u0026lt;\\w\u0026gt; 我爱 南 巷 \u0026lt;\\w\u0026gt; 步骤 2：继续合并\n下一轮最频繁 pair → 南 巷 → 南巷 我爱 南巷 的 花 猫 \u0026lt;\\w\u0026gt; 我爱 南巷 \u0026lt;\\w\u0026gt; 再合并可能的 pair（选择高频或符合词典）：\n的 花 → 的花（低频，可保留拆分）\n花 猫 → 花猫\n最终文本：\n我爱 南巷 的花 猫 \u0026lt;\\w\u0026gt; 我爱 南巷 \u0026lt;\\w\u0026gt; 词表示例： [\u0026#34;我爱\u0026#34;, \u0026#34;南巷\u0026#34;, \u0026#34;的\u0026#34;, \u0026#34;花猫\u0026#34;, \u0026#34;\u0026lt;\\w\u0026gt;\u0026#34;] 编码示例 # ps：南巷，南巷的花猫都是我\n\u0026quot;我爱南巷的花猫\u0026quot; → 拆分为 token： 我爱 + 南巷 + 的 + 花猫 + \u0026lt;\\w\u0026gt; token IDs: [0, 1, 2, 3, 4] \u0026quot;我爱南巷\u0026quot; → 拆分为 token： 我爱 + 南巷 + \u0026lt;\\w\u0026gt; token IDs: [0, 1, 4] 可以看到：\n高频组合 \u0026quot;我爱\u0026quot;、\u0026quot;南巷\u0026quot; 被合并为一个 token\n稀有组合 \u0026quot;的花猫\u0026quot; 拆分或部分合并\n序列长度比按单字符编码短，训练效率更高\n这个需要注意，因为在训练这个固定长度的词表的过程，是与我们的训练集是息息相关的，所以我们在使用数据分布差异比较大的数据的时候，需要重新训练一个新的编码器。不然效果会有的折扣。\nRMSNorm（Root Mean Square Normalization） # 在 Transformer 里，**残差连接（Residual Connection）**会导致激活值在层与层之间不断累积。\n如果不做归一化，可能出现：\n梯度爆炸/消失\n训练不稳定\n不同层数的值分布不一致\n传统解决办法是 LayerNorm。\n好问题，什么叫做激活值在层与层之间不断累积？\n我们现在从数值的分布角度上结合公式来说明这个问题\n1、残差连接 # 在 Transformer 的每一层都有 Residual Connection，公式是这样的：\n$x_{l+1} = x_{l} + F(x_{l}) $\n$x_{l+1}$代表当前层的输出，下一层的输入\n$x_{l}$代表当前层的输入\n$F(x_{l})$代表当前层经过设计的网络的变换\n2、为什么会累积 # 因为每一层都在 叠加：\n第一层：\n$x_{1}=x_{0} + f(x_{0})$\n第二层：\n$x_{2}=x_{0} + f(x_{0}) + f(x_{1})$\n第三层：\n$x_{2}=x_{0} + f(x_{0}) + f(x_{1}) + f(x_{2})$\n\u0026hellip;\u0026hellip;\n可以看到：输入经过多层残差后，前面的值不断累加进去。这样就会导致数值分布变的分散，不集中，反向传播时，梯度也会被放大或变得不稳定，每一层的输入分布不同，导致训练难以收敛。\n3、None (无归一化)、LayerNorm、RMSNorm # 通过一个简单的例子可以说明这一点\u0026ndash;在一个小的残差网络里，取把采样层改成 每隔 8 层取一次激活值（比如一个 32 层的小网络，就取第 8、16、24、32 层）的激活值，然后画直方图对比不同归一化方法 (None / LayerNorm / RMSNorm) 的分布。\n最直观的，从图中可以非常明显的看到，随着模型的层数增长，激活值开始慢慢变的分散。最明显的在没有任何归一化方法的None实验组，它的分散程度是最大的。加入LayerNorm 和RMSNorm的会相对集中一点。\n代码如下：\nimport torch, torch.nn as nn, matplotlib.pyplot as plt import numpy as np # --- RMSNorm 实现 --- class RMSNorm(nn.Module): def __init__(self, dim, eps=1e-8): super().__init__() self.eps = eps self.scale = nn.Parameter(torch.ones(dim)) def forward(self, x): rms = torch.sqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps) return self.scale * (x / rms) # --- 残差模块 --- class ResidualBlock(nn.Module): def __init__(self, dim, norm_type=None): super().__init__() self.fc1 = nn.Linear(dim, dim) self.fc2 = nn.Linear(dim, dim) self.act = nn.GELU() if norm_type == \u0026#34;layernorm\u0026#34;: self.norm = nn.LayerNorm(dim) elif norm_type == \u0026#34;rmsnorm\u0026#34;: self.norm = RMSNorm(dim) else: self.norm = None def forward(self, x): y = self.norm(x) if self.norm is not None else x y = self.fc1(y) y = self.act(y) y = self.fc2(y) return x + y # --- 网络 --- class ResidualNetwork(nn.Module): def __init__(self, dim=64, n_layers=32, norm_type=None): super().__init__() self.blocks = nn.ModuleList([ResidualBlock(dim, norm_type=norm_type) for _ in range(n_layers)]) def forward(self, x, record_layers=None): activations = {} for i, block in enumerate(self.blocks, start=1): x = block(x) if record_layers and i in record_layers: activations[i] = x.detach().cpu().numpy().flatten() return activations # --- 绘制直方图 --- def plot_histograms(norm_types=[\u0026#34;none\u0026#34;, \u0026#34;layernorm\u0026#34;, \u0026#34;rmsnorm\u0026#34;], n_layers=32, step=8): dim, batch_size = 64, 64 x = torch.randn(batch_size, dim) record_layers = list(range(step, n_layers+1, step)) # 每隔 step 采样 fig, axes = plt.subplots(len(record_layers), len(norm_types), figsize=(12, 3*len(record_layers)), sharey=True) for j, norm in enumerate(norm_types): norm_type = None if norm==\u0026#34;none\u0026#34; else norm net = ResidualNetwork(dim=dim, n_layers=n_layers, norm_type=norm_type) acts = net(x, record_layers=record_layers) for i, layer in enumerate(record_layers): ax = axes[i,j] if len(record_layers)\u0026gt;1 else axes[j] ax.hist(acts[layer], bins=30, density=True, alpha=0.7, color=\u0026#34;steelblue\u0026#34;) ax.set_title(f\u0026#34;{norm} - Layer {layer}\u0026#34;) plt.tight_layout() plt.show() # 运行绘图 plot_histograms(n_layers=32, step=8) 为什么传统的使用LayerNorm 而不是使用BatchNorm ？\n输入数据的特点不同 BatchNorm\n假设一个 batch 内的样本分布相似\n用整个 batch 的均值/方差来归一化\n在 图像任务里效果好，因为图片统计分布稳定\nLayerNorm\n对 单个样本 内的特征维度做归一化\n和 batch 大小无关\n在 NLP / Transformer 中，输入是序列化的 token 表示，不同 batch 的分布差异很大，BatchNorm 得到的统计值可能不稳定；LayerNorm 不依赖 batch，稳定得多。\n序列长度变化 NLP 的输入长度变化大（句子长短不一）\nBatchNorm 在变长序列上不好用，需要额外对 padding 做 mask，否则统计量会被污染\nLayerNorm 不受序列长度影响 → 更方便\n小 batch 训练问题 大模型（尤其在分布式训练中）常常 batch 很小（甚至 1~4 个样本）\nBatchNorm 在小 batch 下统计量非常不稳定，训练会发散\nLayerNorm 与 batch size 无关，即使 batch=1 也能正常工作\n并行和分布式训练 BatchNorm 需要在 GPU/TPU 上 跨设备同步均值/方差（很耗通信）\nLayerNorm 不需要跨 batch 计算，全在单样本内完成，更易于大规模分布式训练\n4、RMSNorm # 我们来看LayerNorm 和 RMSNorm两者的公式对比：\n对于LayerNorm来说：\n$y = \\frac{x-\\mu }{\\sqrt{\\sigma ^{2} + \\epsilon } } \\times \\gamma + \\beta $\n其中$\\mu$为均值，$\\sigma ^{2}$为方差\n对于RMSNorm来说，计算方式非常的相似，并且更加简单：\n$y = \\frac{x }{\\sqrt{mean（x^{2}） + \\epsilon } } \\times \\gamma$\n只用 均方根 (RMS) 来归一化\n不再减去均值$\\mu$\n只保留一个缩放参数 γ，没有偏置 β\n这样做的好处为：\n(1) 减少计算量 \u0026amp; 提升速度\nLayerNorm 需要计算均值、方差、标准差，代价高\nRMSNorm 只计算均方值，少一次减法和一次方差运算\n在大模型里，每一步都能省下显著计算时间和显存带宽\n(2) 对均值中心化不敏感\n在 Transformer 里，每一层有残差连接（residual connection），输入往往已经被归一化或分布平衡\n去掉均值归一化影响不大，模型仍能稳定训练\n实际上，均值项经常被 residual shortcut 抵消\n(3) 更稳定的训练\nRMSNorm 只保证向量的“尺度（norm）”一致，不强制分布均值归零\n实践表明，它能让梯度传播更稳定，尤其在 超大模型 里表现更好\n(4) 节省内存\nLayerNorm 需要额外存储均值和方差的梯度\nRMSNorm 计算简单，节省内存，训练时更高效\nGrouped Multi Query Attention # 关于注意力机制，我推荐几个视频帮助大家更好的理解。\nself-Attention\nhttps://www.bilibili.com/video/BV1sw4m1k7Gt/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d6bdf037117ac9f8dc86da23d37ee748\n1、Self-Attention # 用上面表格最简单的例子来表述，要找到词与词直接相似度高的，相似度高，那么最后加权也高，相似度低的，加权就低，灰色的部分为mask值，一般取负无穷。这样做的原因是，模型输出我这个字，不直接输出到后面的字，后面的字应该被遮挡。就是说，模型只可以看见前面的内容去预测下一个被遮挡的字。\n依旧，我们回归到最原始的，从公式入手，这个是最直接，最直观，最美丽。代码也不过是公式的表达方式而已。\n$Q = XW_{Q}$ $K = XW_{K}$ $V = XW_{V}$\n$Attention(Q,K,V) = Softmax(\\frac{QK^{T} }{\\sqrt{d_{k} } })V$\n其中我们可以这样理解$QK^{T}$得到每个 token 对其他 token 的相似度，$\\sqrt{d_{k}}$是为了进行数值缩放，不要让梯度拉的太大或者太小。\nSoftmax → 转成概率\n与 V 相乘 → 得到加权信息聚合\n核心思想：每个 token “看”其他 token 的信息，并按相关性加权汇总。\n用一个非常简单的代码来表示：\n在代码中进行了torch.manual_seed(0)固定，所以生成的X每次运行都是一样的，所以我们的矩阵图每次也都是一样的。\nimport torch import torch.nn as nn import matplotlib.pyplot as plt import seaborn as sns import numpy as np # ---- 生成随机输入 ---- L, d = 6, 8 # 序列长度6，特征维8 torch.manual_seed(0) X = torch.randn(L, d) # 每一行是一个 token 的向量 # ---- 定义 Self-Attention ---- class SimpleSelfAttention(nn.Module): def __init__(self, d_model): super().__init__() self.d_model = d_model self.W_Q = nn.Linear(d_model, d_model) self.W_K = nn.Linear(d_model, d_model) self.W_V = nn.Linear(d_model, d_model) def forward(self, X): Q = self.W_Q(X) # (L,d) K = self.W_K(X) # (L,d) V = self.W_V(X) # (L,d) scores = Q @ K.T / np.sqrt(self.d_model) # (L,L) weights = torch.softmax(scores, dim=-1) # 注意力权重 out = weights @ V # 加权求和 return out, weights # ---- 前向计算 ---- attn = SimpleSelfAttention(d_model=d) out, weights = attn(X) # ---- 可视化注意力权重 ---- plt.figure(figsize=(6,5)) sns.heatmap(weights.detach().numpy(), annot=True, cmap=\u0026#39;Blues\u0026#39;, cbar=True) plt.title(\u0026#34;Self-Attention Weights Heatmap\u0026#34;) plt.xlabel(\u0026#34;Key Index\u0026#34;) plt.ylabel(\u0026#34;Query Index\u0026#34;) plt.show() 图中 横轴 Key Index，纵轴 Query Index\n每个元素表示 query token 对 key token 的注意力权重\n颜色越深 → 权重越大 → 当前 token “越关注”这个 token\n2、Multi-Head Attention # 这个解释其实多多少少也有问题，更好的方法就是看看下面我们给出的代码。这个在论文中也是作者所做实验得到的一些证明，当然肯定是有其他的划分方式，就比如我是不是可以给每个头加一个可学习的权重参数呢？！！！通过不断的做实验，可视化展示热力图，我们总会得到一个较好的实验结果（絮絮叨叨，哈哈哈哈哈）。\nhttps://www.bilibili.com/video/BV1sN4y1g768/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d6bdf037117ac9f8dc86da23d37ee748\n在上一个过程中，我们一直讲的都是单头，但是单头有一些显而易见的缺陷：\n注意力模式单一（还是从公式出发）\n一个 token 只能对整个序列产生一套注意力分布\n这套分布可能同时需要兼顾：\n最近邻依赖（语法）\n远距离语义相关（核心概念或上下文信息）\n单头往往难以同时兼顾近距离与远距离依赖\n信息容量有限\n单个 head 的子空间维度 $d_{k} = d_{model }$\n难以在同一向量空间里表示多种语义关系\n长文本累积干扰\n随着序列长度增加，softmax 注意力容易分散（权重稀释）\n单头可能把注意力分散在很多 token 上，无法突出关键信息\n怎么解决呢？\n将$d_{model}$拆分为多个子空间，每个子空间作为一个 head\n每个 head 有独立的 Q/K/V 参数 → 学习不同注意力模式\n多头 attention：多个人分工合作，每个人专注不同方面 → 综合后得到完整理解\n依旧回归到数学公式上表达：\n$head_{i} = Softmax(\\frac{Q_{i}K_{i}^{T} }{\\sqrt{d_{k} } })V_{i}$ $i = 1...h$\n每个 head 的权重矩阵独立\n拼接后：\n$[head_{1},...,head_{h}]W_{o}$\n一个 token 最终表示融合了 h 种注意力模式 → 可以同时捕获近距离/远距离/语义/上下文等多种依赖。\n（可以简单的说就是把一个大矩阵划分为多个大小相等的小矩阵做同样的事情，最后再讲结果按照最后一个维度平均起来）\n每个头只处理 64 维子空间\n不是随机选择 64 维，而是通过 独立的线性变换 W_Q, W_K, W_V 投影出来\n我们也可以用具体的数值来表示：\n假设：\nTransformer 的隐藏维度 $d_{model}=768 $\n多头数量 h=12\n那么每个 head 的维度：\n$d_{k} = \\frac{d_{model}}{h} = \\frac{768}{12} = 64$\n这里有一个点需要注意，我们不是讲一个768维度的信息切分为每个头64。更好的说法是投影变换而来（可以认为是逻辑切分，但是在物理空间上还是连续的）。这个可以看代码也更好的理解，下面用一个矩阵来表述一下：\n我们用一个序列长度为6，特征维度为16的特征矩阵，用4个头，代码表述，每个头的注意力得分：\n这里在展示一下四个头合并后取平均的注意力得分，注意这个地方仅仅是展示探讨一下，在实际中，4个矩阵合并在一起heads_concat = torch.cat(heads, dim=-1)，再经过一个线性投射层就行out = self.W_O(heads_concat) 。\nimport torch import torch.nn as nn import matplotlib.pyplot as plt import seaborn as sns import numpy as np # ---- 输入数据 ---- L, d_model = 6, 16 # 序列长度6，特征维16 torch.manual_seed(0) X = torch.randn(L, d_model) X = X.unsqueeze(0) # 增加 batch 维度，shape=(1, L, d_model) batch_size = X.shape[0] # ---- Multi-Head Attention ---- class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.d_model = d_model self.n_heads = n_heads assert d_model % n_heads == 0, \u0026#34;d_model must be divisible by n_heads\u0026#34; self.d_k = d_model // n_heads # 每个 head 的 Q/K/V 映射 self.W_Q = nn.Linear(d_model, d_model) self.W_K = nn.Linear(d_model, d_model) self.W_V = nn.Linear(d_model, d_model) self.W_O = nn.Linear(d_model, d_model) def forward(self, X): # X: (batch, L, d_model) Q = self.W_Q(X) # (batch, L, d_model) K = self.W_K(X) V = self.W_V(X) # 拆分多头 Q = Q.reshape(batch_size, L, self.n_heads, self.d_k).permute(0, 2, 1, 3) # (batch, n_heads, L, d_k) K = K.reshape(batch_size, L, self.n_heads, self.d_k).permute(0, 2, 1, 3) V = V.reshape(batch_size, L, self.n_heads, self.d_k).permute(0, 2, 1, 3) all_weights = [] heads = [] for h in range(self.n_heads): q = Q[:, h, :, :] # (batch, L, d_k) k = K[:, h, :, :] # (batch, L, d_k) v = V[:, h, :, :] # (batch, L, d_k) # 注意力分数 scores = q @ k.transpose(-2, -1) / np.sqrt(self.d_k) # (batch, L, L) weights = torch.softmax(scores, dim=-1) # (batch, L, L) out = weights @ v # (batch, L, d_k) heads.append(out) all_weights.append(weights.squeeze(0).detach().numpy()) # 去掉 batch 维度方便可视化 # 拼接多头输出 heads_concat = torch.cat(heads, dim=-1) # (batch, L, d_model) out = self.W_O(heads_concat) # (batch, L, d_model) return out, all_weights # ---- 前向计算 ---- n_heads = 4 mha = MultiHeadAttention(d_model=d_model, n_heads=n_heads) out, weights = mha(X) # ---- 可视化每个头的注意力权重 ---- fig, axes = plt.subplots(1, n_heads, figsize=(4*n_heads,4)) for i, w in enumerate(weights): sns.heatmap(w, annot=True, cmap=\u0026#39;Blues\u0026#39;, ax=axes[i]) axes[i].set_title(f\u0026#34;Head {i+1}\u0026#34;) axes[i].set_xlabel(\u0026#34;Key Index\u0026#34;) axes[i].set_ylabel(\u0026#34;Query Index\u0026#34;) plt.tight_layout() plt.show() Multi-Head Attention = “多个人看同一句话，每个人关注不同信息 → 汇总后得到丰富 token 表示”\n核心优势：分工协作 + 自由组合原始特征 → 捕捉复杂语义关系\n实现关键：线性映射 + reshape/permute + 多头注意力计算 + 拼接映射\n3、Grouped Multi-Query Attention (GQA) # 讲到这个，这里要和大家区分一下误区：很多教程会直接告诉你（GQA可以将 head 分组，每组 head 共享同一组 K/V，但仍然保留各自的 Q，这样的方式来节约显存）但是这个说法不是很准确。这里需要严格的区分训练和推理。\n在训练时，每个 step 都要重新计算完整的 Q/K/V，不会缓存 Key/Value。\nMHA (Multi-Head Attention)\nQ、K、V 都是独立的线性层，维度 （B,L,$d_{model}$）\nQ: (B,L,h,$d_{k }$)\nK/V: (B,L,h,$d_{k }$)\n计算量：和 head 数量 h 成正比\n显存：主要花在存储 Q/K/V（训练时没缓存概念）\nMQA (Multi-Query Attention)\nQ：仍然每个 head 独立\nK/V：只有 1 份（共享所有 head）\n计算量：Q 计算不变，K/V 映射减少了 h 倍\n显存：K/V 显存占用减少，但整体相比 MHA 的训练显存节省不算非常大\nGQA (Grouped MQA)\nQ：每个 head 独立\nK/V：分组共享，g 份，而不是 h 份\n计算量：K/V 计算量介于 MHA 和 MQA 之间\n显存：K/V 显存也减少，但仍需存 Q（大头还是在梯度和激活）\n在推理时（尤其是自回归生成），情况完全不同：\n每生成一个新 token，Q 是新的，但 K/V 来自历史 token，需要缓存 (KV Cache)\nKV Cache 占用显存是推理的主要瓶颈\n对比：\nMHA\n每个 head 都存一份 K/V\nKV cache 大小：O(L x h x $d_{k}$)\n随着上下文长度 L 增加，显存急剧膨胀\nMQA\n所有 head 共用一份 K/V\nKV cache 大小：O(L x $d_{k}$)\n比 MHA 小 h 倍，推理速度更快，显存占用极低\nGQA\n分组共享 K/V，假设 g 组\nKV cache 大小：O(L x g x $d_{k}$)\n介于 MHA 和 MQA 之间，既节省显存又保留一定多样性\n这里相信细心的你就会有疑问了？\n“既然不同的 head 表示不同类型的注意力模式（语法、语义、长程依赖等），为什么还能让它们共享 Key/Value，而不让表达能力塌缩？”\n我们依然回到公式出发：\n$head_{i} = Softmax(\\frac{Q_{i}K_{i}^{T} }{\\sqrt{d_{k} } })V_{i}$ $i = 1...h$\n每个 head 的输出之所以不同，是因为它们的 Q/K/V 投影矩阵不同：\n$Q = XW^{i}_{Q}$ $K = XW^{i}_{K}$ $V = XW^{i}_{V}$\n代码中直观的体现就是为\nself.W_Q = nn.Linear(d_model, d_model) self.W_K = nn.Linear(d_model, d_model) self.W_V = nn.Linear(d_model, d_model) self.W_O = nn.Linear(d_model, d_model) Query 决定了“看哪里”（要关注的模式）\nKey/Value 决定了“被看见的是什么”\n而实验表明：\n对于同一个输入 X，不同 head 的 K/V 表征往往高度相似，差异主要来自 Q 的查询方向。\n也就是说，不同 head 虽然计算不同的注意力模式，但它们访问的“信息库”（K/V）非常接近。\n换句话说，多个 head 在 K/V 空间中的“感知视角”高度重叠。\n也就是说在高维度的比如一个token 768维度的空间信息上，里面有大量的维度信息是重合的（冗余信息非常多），这里并不是说这些特征信息不重要，而是说从降低算力开销的角度上来思考，在平衡效果的前提下可以这样做。正如我们之前提到到LayerNorm与RMSNorm一样本质上两个公式非常相似，但从实验分析上来说可以进一步去除增益效果不大的部分。这就是RMSNorm的来源。\n怎么进行分组呢？\n这里推荐观看原文：GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints\nhttps://arxiv.org/abs/2305.13245\nGQA方案是基于前面我们提到的MHA，MQA的中间折中方案。但是为了可以方便矩阵乘法可以直接运算，需要将K V矩阵的数量复制到和Q矩阵相等的数量，如下图:\n我们从代码上来看实现是非常简单的，后面也会一步一步带着写代码‘’\ndef repeat_kv(x: torch.Tensor, n_rep: int) -\u0026gt; torch.Tensor: # 获取输入张量的形状：批量大小、序列长度、键/值对头的数量、每个头的维度大小 bs, slen, n_kv_heads, head_dim = x.shape # 如果重复次数为1，则不需要重复，直接返回原始的张量 if n_rep == 1: return x # 对张量进行扩展和重塑操作用以重复键值对 return ( x[:, :, :, None, :] # 在第四个维度（头的维度前）添加一个新的维度 .expand(bs, slen, n_kv_heads, n_rep, head_dim) # 将新添加的维度扩展到n_rep大小，实现重复的效果 .reshape(bs, slen, n_kv_heads * n_rep, head_dim) # 重新塑形，合并键/值对头的数量和重复次数的维度 ) 旋转位置编码 # 1、为什么要用位置编码 # Transformer 天然不懂“顺序”，无法想图像那样，天然具体空间特征知道前后左右像数值是那个（本质上可以当做是一个二维矩阵，只要知道分辨率多少，天然就可以得到所有像素的索引。）下面我们举一个例子：\nTransformer 的输入是一串向量（token embedding），例如：\n这些向量被送进注意力机制（Self-Attention），但注意力只看到 一组向量的集合，它不会知道 e₁ 是第一个，e₃ 是第三个。这个从数学公式上可以很明显的看出，每个向量只知道和所有包括自己相互点乘，这个阶段不会知道任何有关于空间位置的信息存在。\n对模型来说，「我喜欢南巷的花猫」和「花猫喜欢南巷的我」如果没有位置信息，是一样的！\n举几个变体：\n这些句子里的词一模一样，但顺序不同。 人类理解句子主要依靠语序（句法结构）， 而 Transformer 不像 CNN/RNN 沿时间逐步扫描，它是同时看到所有词， 如果不告诉它“第一个词是谁”第二个词是谁”， 它无法知道谁修饰谁、谁是主语谁是宾语。\n位置编码就是在告诉模型“谁在哪儿”\n位置编码（Positional Encoding）做的事非常简单： 在每个词的 embedding 上加入一个位置向量， 让模型区分：\n“我”在第 1 个位置\n“喜欢”在第 2 个\n“花猫”在第 5 个\n这就像每个词带了一个“坐标标签”。\n举个简化示意：\n这样解释是不是很清晰易懂了，在原来特征向量中，加入位置向量的信息。这个做法是当前主流大模型非常通用的做法，但是仍然有一些差异，有一些模型只是在输入的时候加上位置信息，有的模型在每一个注意力层都加入了位置编码，LLama2模型正是这样做的\n这里如果大家仔细的话，就会发现，仅仅只有Q，K矩阵加入了位置编码的信息，那么V矩阵不需要吗，不是是QKV三个矩阵，里面的特征信息都是一样的X吗？\n这里就是需要我们再次强调一下，QKV三个矩阵到底在做什么了，我们再次回到公式：\n$Attention(Q,K,V) = Softmax(\\frac{QK^{T} }{\\sqrt{d_{k} } })V$\n我们在做什么呢，我们通过QK点积，获取相似度得分，通过softmax转化为概率分布，然后再去乘V矩阵。相当于是softmax转化QK为注意力得分，任何再去V里面让数值大的更加突出，数值小的更加小，相当于是加强和抑制的做用。\n更加通俗的来说：\nQ（Query）：当前词想“去找”谁的信息\u0026ndash;》需要知道自己的位置\nK（Key）：每个词的“被找”标签\u0026ndash;》需要知道自己的位置\nV（Value）：被取回的内容（语义特征）\u0026ndash;》不需要知道位置，只要内容\nQ、K 负责匹配，它们之间的点积决定“谁和谁相关”；\nV 只是携带信息，根据 QK 的匹配结果被加权平均后输出。\n碎碎念\u0026mdash;-致自己\n这里应该就懂了，说的很详细了。这个公式是不是看吐了，感觉很简单。那当然不是了，这个也是经过很多实验，验证出来的，如果没有验证出来，那必然会有很多小黑粉喷，虽然现在也有很多人喷，但这又何尝不是前进的动力呢？！！！人生就是这样，在否定之否定中旋转上升前行。曾经我们深信不疑的，比如爱情，年少时，以为深爱就能长长久久幸福过一辈子，可却是匆匆忙忙破碎的结局收场。后来再遇到喜欢的人，心性也更加稳定了，也懂得一点经营感情，却也需要面临更多现实的原因。失败常有，但促使自己变得更加坚韧不屈的部分也成为自己的一部分。可正因为如此，我们才慢慢明白，所谓成长，从来不是获得什么，而是逐渐学会接受什么。接受不完美的自己，接受有些事情无论多努力都无法改变，接受有些人注定只是人生路上的过客。可接受并不等于妥协，而是学会带着清醒去热爱，带着伤痕去前行。就像那些公式、那些实验——看似枯燥重复，却在一次次失败和修正中逼近真理。人生何尝不是如此？我们不断试错、修正，再试错，再修正，最后也许并没有得到一个“标准答案”，但却在过程中，成为了更完整的自己。所以啊，不怕被质疑，不怕被误解。只要方向不偏，哪怕走得慢一点，也是在往前走。那些挫折和坎坷，终有一天都会变成你眼底的光。\n2、为什么要用旋转位置编码 # 之前，在RoPE之前用的是什么位置编码，为什么不用了\n绝对位置编码（Absolute Positional Encoding）\n先上公式：\n$PE_{i,2k} = \\sin (\\frac{i}{10000^{\\frac{2k}{d_{model} } } } )$\n$PE_{i,2k+1} = \\cos (\\frac{i}{10000^{\\frac{2k}{d_{model} } } } )$\n也就是说：\n每两个维度（2k 和 2k+1）组成一个二维平面；\n在这个平面上，随着位置 i 增加，这个点会“绕圆转动”；\n不同的 k（不同维度对）旋转速度不同（频率不同）。\n换句话说：\n每一对维度 (2k, 2k+1) 代表一个不同频率的“旋转编码器”， 整个 768 维的向量就是由 384 个不同频率的旋转组合而成。\n这里举一个简单的例子（假设d=6）：\n我们有维度编号：0, 1, 2, 3, 4, 5\n→ 共 3 对维度：\n第 1 对：(0,1)\n第 2 对：(2,3)\n第 3 对：(4,5)\n对第 i 个位置：\n结果是：\n前面的维度变化得快（对近距离敏感）；\n后面的维度变化得慢（对远距离敏感）；\n组合起来，模型既能感知局部顺序，也能感知整体位置。\n为什么要用旋转位置编码\nTransformer 的 self-attention 本质上靠 Q·K 来决定“谁关注谁”。如果把位置信息放到 Q 与 K 上，那么注意力分数（点积）可以直接包含相对位置信息。\n旋转（而非加法）实现了这样一个漂亮的代价很低的性质：两个向量分别在位置 p 和 q 旋转后，它们的点积仅依赖 q−p（相对位移）。也就是说注意力权重是相对位置敏感的。\n旋转使用三角函数（cos, sin），是连续的、参数少且容易向更长序列外推（相比纯可学习位置向量更友好）。\n举例：\n“我(1) 喜欢(2) 南巷(3) 的(4) 花猫(5)” —— RoPE 让“喜欢”(2) 和 “我”(1) 的 Q·K 得分只与距离 1 有关，而不是“它们分别在哪里”的绝对索引。\n先来看公式证明吧：\n设 q 和 k 是同一 head 下未旋转的向量（维度被成对看作 2D 块）。对第 i 对（block）规定一个位置依赖的旋转角：\n$\\theta _{p,i} = p\\cdot \\omega _{i} $\n其中 $\\omega _{i} $ 是第 i个频率（通常 $\\omega _{i}=\\frac{1}{10000^{\\frac{2i}{d} } } $ 的某种变体）。\n对每个 2D 块用旋转矩阵 R(θ)：\n$R(\\theta) =\\begin{bmatrix} \\cos \\theta \u0026 -\\sin \\theta \\\\ \\sin \\theta \u0026 \\cos \\theta \\end{bmatrix}$\n把位置 P 的 Q、K 分别旋转:\n$Q_{p} = R(\\theta_{p})Q$, $K_{q} = R(\\theta_{q})K$\n关键代数性质（旋转矩阵的正交性）：\n$Q^T_{p}K_{q} = Q^TR(\\theta_{p})^TR(\\theta_{q})K = Q^TR(\\theta_{q} - \\theta_{p})K$\n因为，$\\theta_{q}-\\theta_{p} = (q-p)w$所以点积只依赖于 q−p（相对位移）——这正是 RoPE 的“魔法”根源。\n（对多个 2D 块分别成立，整体向量的点积是各块总和。)\n复数视角（更直观）：把每对维度视为复数 z=a+ib旋转对应乘以$e^{i\\theta}$.两个复数各乘$e^{i\\theta_{p}}$与$e^{i\\theta_{q}}$。它们的内积只涉及 $e^{i(θ_{q}−θ_{p})}$。\n好吧，我也看的懵懵懂懂。\nRoPE 的核心思想就是：\n把每个词向量按照它在句子里的“位置”旋转一点角度，\n这样模型在计算两个词的相似度（Q·K）时，\n自然能知道它们之间相隔多远（相对位置）。\n举例子：\n于是：\n“我”的向量朝正右；\n“喜欢”的向量朝右下；\n“南巷”的向量朝下；\n“花猫”的向量朝左下。\n你现在看它们的夹角， 就知道它们在句子里相隔多远了。 这就是 RoPE 的妙处： 两个词的角度差，就是它们的相对位置！\n当然，怎么计算就是靠上面这些公式了，得拿起线性代数好好研究一番了。\n这样是不是懂了，哈哈哈哈哈，这个不比纯+好很多吗，天然自带。当然还是实验证明了，这个做法是有效果的。回过头去看看公式，发现学好数学真重要，有idea，却实现不出来。\n前馈网络（Feed Forward Network, FFN） # 1、Feed Forward # 这个就相较于之前的内容，就非常的简单了。\n但是大家不知道想过没有，为什么要一个FFN呢，可以不要吗？\n当然可以啊，模型依然可以跑通，但是效果肯定是会大大折扣的！！！注意力擅长“跨 token 信息交互”，而 FFN 擅长“单 token 内部特征处理”\n如果单纯只放注意力机制，那么\n问题：\n注意力本身是线性的加权求和\n如果没有非线性变换，模型只能做“线性组合”，无法生成复杂的非线性特征\n对单个 token 内部的特征组合能力有限\n在后面接一个FFN可以做到：\n逐 token 处理：对每个 token 独立计算，不依赖其他 token\n非线性增强：通过激活函数（GeLU、SwiGLU 等）引入非线性\n这里同时也说明了一点，比如一个token维度是768，是不是768这些维度信息也会交换处理。让信息之间充分的交互。这里可以回顾一下拆分多头，这里是不是就可以很好的解释，为什么KV矩阵里面信息是会有重合，冗余的，这里是非常关键的一点。\n从代码上也可以很好的看出这一点：\nclass MLP(nn.Module): def __init__(self, dim: int, hidden_dim: int, multiple_of: int, dropout: float): super().__init__() # 如果没有指定隐藏层的维度，我们将其设置为输入维度的4倍 # 然后将其减少到2/3，最后确保它是multiple_of的倍数 if hidden_dim is None: hidden_dim = 4 * dim hidden_dim = int(2 * hidden_dim / 3) hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) # 定义第一层线性变换，从输入维度到隐藏维度 self.w1 = nn.Linear(dim, hidden_dim, bias=False) # 定义第二层线性变换，从隐藏维度到输入维度 self.w2 = nn.Linear(hidden_dim, dim, bias=False) # 定义第三层线性变换，从输入维度到隐藏维度 self.w3 = nn.Linear(dim, hidden_dim, bias=False) # 定义dropout层，用于防止过拟合 self.dropout = nn.Dropout(dropout) def forward(self, x): # 前向传播函数 # 首先，输入x通过第一层线性变换和SILU激活函数 # 然后，结果乘以输入x通过第三层线性变换的结果 # 最后，通过第二层线性变换和dropout层 return self.dropout(self.w2(F.silu(self.w1(x)) * self.w3(x))) 扩维 → 每个 token 的特征被“拆开”成更多维度\n非线性 + 门控 → 不同维度信息进行选择性增强或屏蔽\n降维 → 特征重新混合回原来的维度空间\n2、 SwiGLU是Gated Linear Unit (GLU) 的变种 # 上公式（其实以前我也不喜欢看公式，但是慢慢发现公式其实还更好理解）：\n$SwiGLU(x)=(W_{a}x)⊙SiLU(W_{b}x)$\n$SiLU(x)=x⋅\\frac{1}{1+e^{-x} } $\n线性分支 $a=W_{a}x$ → 线性组合原 token 内部 768 维\n门控分支 $b=SiLU(W_{b}x)$ → 非线性 + 门控\n逐元素相乘 $a⊙b$ → 不同维度信息被选择性放大或抑制\n乘法就像给每个维度加了一个开关：\n如果 $b_{i}≈0$ → 这个维度被屏蔽\n如果 $b_i \\approx 1$ → 这个维度保留\n不同于普通激活，SwiGLU 可以“控制信息流”，让网络学会选择哪些维度重要\n这个比较简单，就不过多解释了。\nLinear层与softmax层 # 1、最后的 Linear 层（输出投影） # 公式：\n$ Z = H W_\\text{out} + b$\n$W_\\text{out} \\in \\mathbb{R}^{d_\\text{model} \\times V}$，V = 词表大小\n$b \\in \\mathbb{R}^{V}$\n作用：把每个 token 的隐藏向量 投影到词表空间\n换句话说：每个 token 的向量 $h_t \\in \\mathbb{R}^{d_\\text{model}}$ → 生成 $v_t \\in \\mathbb{R}^{V}$，每个维度对应一个词在词表中的“未归一化得分（logits）”。\nLinear 层就是把高维语义向量映射到每个词的分数空间 2、 Softmax 层（生成概率） # 公式：\n$P(\\text{token}=i \\mid \\text{context}) = \\frac{\\exp(z_i)}{\\sum_{j=1}^{V} \\exp(z_j)}$\n将 Linear 输出的 logits 转换成 概率分布\nSoftmax 确保所有 token 的概率之和 = 1\n这个概率就是 语言模型预测下一个 token 的依据\n自此我们学会了，所有组件是怎么来的，怎么设计的，为什么要这样设计。我们目前仅仅是学会了模型框架的设计，我们其实更加重要的是知道这个任务形式是怎么样的，我们怎么训练，训练数据是怎么样的，怎么去做loss。对于网络架构来说，可以简单抽象为一个特征提取器。将内容抽象为计算机可以理解的特征。如此而已。\n后续我们详细介绍，每个组件代码是怎么编写的，数据到底是长什么样子，怎么去处理的。\n后面章节就是详细的实际操作，我们的CodeLab\n","date":"2026/06/14","externalUrl":null,"permalink":"/blog/llama2-analysis/","section":"日常记录、技术札记与转载收藏。","summary":"从模型架构、Tokenizer、RMSNorm、GQA、RoPE、FFN 到输出层，系统拆解 LLaMA 2 的核心组件与设计动机。","title":"LLaMA 2 解析","type":"blog"},{"content":"","date":"2026/06/01","externalUrl":null,"permalink":"/tags/lerobot/","section":"Tags","summary":"","title":"LeRobot","type":"tags"},{"content":"","date":"2026/06/01","externalUrl":null,"permalink":"/tags/pi0.5/","section":"Tags","summary":"","title":"Pi0.5","type":"tags"},{"content":"","date":"2026/06/01","externalUrl":null,"permalink":"/tags/robot-manipulation/","section":"Tags","summary":"","title":"Robot Manipulation","type":"tags"},{"content":"","date":"2026/06/01","externalUrl":null,"permalink":"/tags/so-101/","section":"Tags","summary":"","title":"SO-101","type":"tags"},{"content":"","date":"2026/06/01","externalUrl":null,"permalink":"/categories/%E9%A1%B9%E7%9B%AE/","section":"Categories","summary":"","title":"项目","type":"categories"},{"content":"","date":"2026/05/20","externalUrl":null,"permalink":"/tags/action-prediction/","section":"Tags","summary":"","title":"Action Prediction","type":"tags"},{"content":"","date":"2026/05/20","externalUrl":null,"permalink":"/tags/attention/","section":"Tags","summary":"","title":"Attention","type":"tags"},{"content":"","date":"2026/05/20","externalUrl":null,"permalink":"/tags/pi0/","section":"Tags","summary":"","title":"Pi0","type":"tags"},{"content":"","date":"2026/05/10","externalUrl":null,"permalink":"/tags/embodied-ai/","section":"Tags","summary":"","title":"Embodied AI","type":"tags"},{"content":"","date":"2026/05/10","externalUrl":null,"permalink":"/tags/robot-control/","section":"Tags","summary":"","title":"Robot Control","type":"tags"},{"content":"","date":"2026/05/10","externalUrl":null,"permalink":"/categories/%E6%AF%94%E8%B5%9B/","section":"Categories","summary":"","title":"比赛","type":"categories"},{"content":"","date":"2026/05/01","externalUrl":null,"permalink":"/tags/clip/","section":"Tags","summary":"","title":"CLIP","type":"tags"},{"content":"","date":"2026/05/01","externalUrl":null,"permalink":"/tags/image-text/","section":"Tags","summary":"","title":"Image-Text","type":"tags"},{"content":"","date":"2026/05/01","externalUrl":null,"permalink":"/tags/lora/","section":"Tags","summary":"","title":"LoRA","type":"tags"},{"content":"","date":"2026/05/01","externalUrl":null,"permalink":"/tags/vlm/","section":"Tags","summary":"","title":"VLM","type":"tags"},{"content":"","date":"2026/04/15","externalUrl":null,"permalink":"/tags/llama2/","section":"Tags","summary":"","title":"LLaMA2","type":"tags"},{"content":"","date":"2026/04/15","externalUrl":null,"permalink":"/tags/moe/","section":"Tags","summary":"","title":"MoE","type":"tags"},{"content":"","date":"2026/04/15","externalUrl":null,"permalink":"/tags/rag/","section":"Tags","summary":"","title":"RAG","type":"tags"},{"content":"","date":"2026/02/10","externalUrl":null,"permalink":"/tags/feature-fusion/","section":"Tags","summary":"","title":"Feature Fusion","type":"tags"},{"content":"","date":"2026/02/10","externalUrl":null,"permalink":"/tags/lightweight/","section":"Tags","summary":"","title":"Lightweight","type":"tags"},{"content":"","date":"2026/02/10","externalUrl":null,"permalink":"/tags/semantic-segmentation/","section":"Tags","summary":"","title":"Semantic Segmentation","type":"tags"},{"content":"HAFNet 从语义分割网络的层级功能划分出发，分析浅层、中层与深层特征在边界建模、结构感知与语义抽象中的不同作用，针对传统轻量化模型多尺度协同不足的问题进行结构设计。\n网络结构图 # HAFNet 网络结构图。 主要贡献包括：\n提出分层协同的轻量化语义分割架构，明确不同层级特征的功能分工。 在浅层设计 AcquisitionFeature 模块，强化边缘和纹理等低级特征提取并抑制冗余信息。 在中层设计 MS-ACSBlock，融合多尺度建模与注意力机制，提升局部结构与目标轮廓感知能力。 在深层设计 GeoLiteConv 模块，增强语义抽象与全局上下文建模能力。 在 Crack500、MIAD、WHDLD 和 Cityscapes 等数据集上评测；在 WHDLD 遥感航拍场景中以 1.98 GFLOPs、1.19M 参数达到 63.17% mIoU。 代码入口：HAFNet。\n","date":"2026/02/01","externalUrl":null,"permalink":"/publications/hafnet-lightweight-semantic-segmentation/","section":"视觉感知与语义分割方向的科研工作。","summary":"实时轻量化语义分割网络，基于浅层边界、中层结构与深层语义的层级功能划分，结合注意力与多尺度特征融合提升复杂场景分割效果。","title":"HAFNet: Hierarchical Attention and Feature Fusion for Real-Time Lightweight Semantic Segmentation","type":"publications"},{"content":"","date":"2026/02/01","externalUrl":null,"permalink":"/tags/lightweight-segmentation/","section":"Tags","summary":"","title":"Lightweight Segmentation","type":"tags"},{"content":"","date":"2026/02/01","externalUrl":null,"permalink":"/tags/real-time/","section":"Tags","summary":"","title":"Real-Time","type":"tags"},{"content":"","date":"2026/02/01","externalUrl":null,"permalink":"/categories/%E7%A7%91%E7%A0%94%E6%88%90%E6%9E%9C/","section":"Categories","summary":"","title":"科研成果","type":"categories"},{"content":"","date":"2026/01/10","externalUrl":null,"permalink":"/tags/crack-segmentation/","section":"Tags","summary":"","title":"Crack Segmentation","type":"tags"},{"content":"","date":"2026/01/10","externalUrl":null,"permalink":"/tags/mcfa/","section":"Tags","summary":"","title":"MCFA","type":"tags"},{"content":"","date":"2026/01/10","externalUrl":null,"permalink":"/tags/sues-crack/","section":"Tags","summary":"","title":"SUES-CRACK","type":"tags"},{"content":"","date":"2026/01/10","externalUrl":null,"permalink":"/tags/vnet/","section":"Tags","summary":"","title":"VNet","type":"tags"},{"content":"","date":"2026/01/01","externalUrl":null,"permalink":"/tags/industrial-vision/","section":"Tags","summary":"","title":"Industrial Vision","type":"tags"},{"content":"","date":"2026/01/01","externalUrl":null,"permalink":"/tags/multiscale-learning/","section":"Tags","summary":"","title":"Multiscale Learning","type":"tags"},{"content":"V2Net 面向工业裂缝分割中目标尺度变化剧烈、细粒度结构易在多次下采样中丢失的问题，从特征组织结构层面分析 UNet / UNet++ 在多尺度建模上的冗余融合与语义错位问题。\n网络结构图 # V2Net 网络结构图。 主要贡献包括：\n提出嵌套式多尺度特征学习框架，将网络拆解为多个 VNet 子结构，并通过匹配式跳跃连接实现跨尺度单元的语义对齐与特征复用。 设计面向裂缝方向性与细粒度几何特征的渐进式特征增强机制，缓解多尺度融合过程中的关键信息衰减。 在多个裂缝数据集上优于 UNet、UNet++ 等主流方法；在 KHANHHAS 数据集上，相较 UNet 基线提升 41.69% IoU。 代码入口：NxV2Net。\n","date":"2026/01/01","externalUrl":null,"permalink":"/publications/v2net-crack-segmentation/","section":"视觉感知与语义分割方向的科研工作。","summary":"面向工业裂缝分割的嵌套式多尺度特征学习框架，通过 VNet 子结构、匹配式跳跃连接和渐进式特征增强改善尺度变化与细粒度结构表达。","title":"V2Net: A Nested Framework for Crack Segmentation Based on Multiscale Feature Learning","type":"publications"},{"content":"","date":"2025/02/15","externalUrl":null,"permalink":"/tags/classification/","section":"Tags","summary":"","title":"Classification","type":"tags"},{"content":"","date":"2025/02/15","externalUrl":null,"permalink":"/tags/efficientnet/","section":"Tags","summary":"","title":"EfficientNet","type":"tags"},{"content":"","date":"2025/02/15","externalUrl":null,"permalink":"/tags/resnet/","section":"Tags","summary":"","title":"ResNet","type":"tags"},{"content":"","date":"2025/02/15","externalUrl":null,"permalink":"/tags/swanlab/","section":"Tags","summary":"","title":"SwanLab","type":"tags"},{"content":"","date":"2025/02/01","externalUrl":null,"permalink":"/tags/kaggle/","section":"Tags","summary":"","title":"Kaggle","type":"tags"},{"content":"","date":"2025/01/15","externalUrl":null,"permalink":"/tags/medical-image/","section":"Tags","summary":"","title":"Medical Image","type":"tags"},{"content":"","date":"2025/01/15","externalUrl":null,"permalink":"/tags/segmentation/","section":"Tags","summary":"","title":"Segmentation","type":"tags"},{"content":"","date":"2025/01/15","externalUrl":null,"permalink":"/tags/u-net/","section":"Tags","summary":"","title":"U-Net","type":"tags"},{"content":"","date":"2024/09/01","externalUrl":null,"permalink":"/tags/mathematical-modeling/","section":"Tags","summary":"","title":"Mathematical Modeling","type":"tags"},{"content":"","date":"2024/09/01","externalUrl":null,"permalink":"/tags/optimization/","section":"Tags","summary":"","title":"Optimization","type":"tags"},{"content":"","date":"2024/08/01","externalUrl":null,"permalink":"/tags/ai-application/","section":"Tags","summary":"","title":"AI Application","type":"tags"},{"content":"","date":"2024/08/01","externalUrl":null,"permalink":"/tags/algorithm-design/","section":"Tags","summary":"","title":"Algorithm Design","type":"tags"},{"content":"","date":"2024/07/01","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"2024/07/01","externalUrl":null,"permalink":"/tags/robotics/","section":"Tags","summary":"","title":"Robotics","type":"tags"},{"content":"","date":"2024/06/01","externalUrl":null,"permalink":"/tags/innovation/","section":"Tags","summary":"","title":"Innovation","type":"tags"},{"content":"","date":"2024/06/01","externalUrl":null,"permalink":"/tags/project-management/","section":"Tags","summary":"","title":"Project Management","type":"tags"},{"content":"","date":"2024/05/01","externalUrl":null,"permalink":"/tags/campus-blog/","section":"Tags","summary":"","title":"Campus Blog","type":"tags"},{"content":"","date":"2024/05/01","externalUrl":null,"permalink":"/tags/frontend/backend/","section":"Tags","summary":"","title":"Frontend/Backend","type":"tags"},{"content":"","date":"2024/05/01","externalUrl":null,"permalink":"/tags/mobile-web/","section":"Tags","summary":"","title":"Mobile Web","type":"tags"},{"content":"","date":"2024/05/01","externalUrl":null,"permalink":"/tags/pc-web/","section":"Tags","summary":"","title":"PC Web","type":"tags"},{"content":"","date":"2024/04/01","externalUrl":null,"permalink":"/tags/chat-system/","section":"Tags","summary":"","title":"Chat System","type":"tags"},{"content":"","date":"2024/04/01","externalUrl":null,"permalink":"/tags/html/","section":"Tags","summary":"","title":"HTML","type":"tags"},{"content":"","date":"2024/04/01","externalUrl":null,"permalink":"/tags/messaging/","section":"Tags","summary":"","title":"Messaging","type":"tags"},{"content":"","date":"2023/06/01","externalUrl":null,"permalink":"/tags/engineering/","section":"Tags","summary":"","title":"Engineering","type":"tags"},{"content":"","date":"2023/06/01","externalUrl":null,"permalink":"/tags/system-design/","section":"Tags","summary":"","title":"System Design","type":"tags"},{"content":"","date":"2023/05/01","externalUrl":null,"permalink":"/tags/control/","section":"Tags","summary":"","title":"Control","type":"tags"},{"content":"The main content archive is currently maintained in Chinese. Switch to 简体中文 for the complete article list.\n","externalUrl":null,"permalink":"/en/blog/","section":"Blog","summary":"The main content archive is currently maintained in Chinese. Switch to 简体中文 for the complete article list.\n","title":"Blog","type":"blog"},{"content":"","externalUrl":null,"permalink":"/en/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"The main competition archive is currently maintained in Chinese. Switch to 简体中文 for the complete competition list.\n","externalUrl":null,"permalink":"/en/competitions/","section":"Competitions","summary":"The main competition archive is currently maintained in Chinese. Switch to 简体中文 for the complete competition list.\n","title":"Competitions","type":"competitions"},{"content":" I focus on embodied intelligence, multimodal perception, and lightweight visual segmentation. # I am Nanxiang, an M.Eng. student in Electronic Information at Shanghai University of Engineering Science.\nMy work revolves around VLA/robot manipulation, multimodal large models, and visual perception algorithms. I care about the relationship between model architecture, data feedback loops, and real-world system integration: an algorithm should work not only in metrics, but also under noisy data, execution error, and engineering constraints.\nSelected work starts with LeRobot pi0.5 + SO-101, V2Net, HAFNet, and CodeLab-LLaMA2 / Xingyu MoE.\nPortfolio · Publications · Competitions · Blog · GitHub\n","externalUrl":null,"permalink":"/en/","section":"Nanxiang","summary":"I focus on embodied intelligence, multimodal perception, and lightweight visual segmentation. # I am Nanxiang, an M.Eng. student in Electronic Information at Shanghai University of Engineering Science.\nMy work revolves around VLA/robot manipulation, multimodal large models, and visual perception algorithms. I care about the relationship between model architecture, data feedback loops, and real-world system integration: an algorithm should work not only in metrics, but also under noisy data, execution error, and engineering constraints.\n","title":"Nanxiang","type":"page"},{"content":"The main project archive is currently maintained in Chinese. Switch to 简体中文 for the complete project list.\n","externalUrl":null,"permalink":"/en/portfolio/","section":"Portfolio","summary":"The main project archive is currently maintained in Chinese. Switch to 简体中文 for the complete project list.\n","title":"Portfolio","type":"portfolio"},{"content":"The main publication archive is currently maintained in Chinese. Switch to 简体中文 for the complete publication list.\n","externalUrl":null,"permalink":"/en/publications/","section":"Publications","summary":"The main publication archive is currently maintained in Chinese. Switch to 简体中文 for the complete publication list.\n","title":"Publications","type":"publications"},{"content":"","externalUrl":null,"permalink":"/en/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/en/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"}]