[{"content":"这个博客主要记录我在 AI Infra 方向的学习与实践过程。\n学习路线 Transformer 基础：prefill/decode、KV cache、指标（TTFT/TPOT）怎么落到系统观测 推理引擎拆解：调度与 batching、KV/显存管理、多卡通信、可观测与调试闭环 CUDA / Triton 实战：从推理瓶颈出发写/改算子（例如 RMSNorm、RoPE、attention 相关） 由于自己是初学，更新博客内容可能会有误解，如有发现，欢迎沟通：https://github.com/xystart\n","date":"2026-04-07T00:00:00+08:00","permalink":"/p/getting-started/","title":"AI Infra 学习记录"},{"content":"一、 环境安装与配置 💡 核心逻辑\n推荐使用 Conda 进行环境管理。通过创建“虚拟环境”，可以在同一台机器上隔离不同版本的 Python 和 PyTorch，避免不同项目间的依赖冲突。\n1.1 第一步：安装 Conda 版本选择：\nMiniconda：轻量级，仅包含 Conda 和基础包，适合开发者。\nAnaconda：全家桶版，包含大量科学计算包，占用空间较大。\n安装步骤：前往 Miniconda 官网或 Anaconda 官网。\nMac (Apple Silicon): 必须选择 macOS Apple M1/M2/M3 (M-series) 64-bit 版。\nWindows: 选择 Windows 64-bit 版。\n1.2 第二步：(仅限 Windows NVIDIA 用户) 安装 CUDA Windows 若要使用 GPU 加速，必须满足硬件是 NVIDIA 显卡。\n检查显卡：在任务管理器中确认是否有 NVIDIA GPU。\n安装驱动：前往 NVIDIA 官网更新最新的显卡驱动。\nCUDA 版本：通常 PyTorch 会自带简版 CUDA，但建议手动安装 CUDA Toolkit（如 11.8 或 12.1）以获得最佳兼容性。\n1.3 第三步：安装 Python 及 PyTorch 打开终端（Mac 使用 Terminal，Windows 使用 Anaconda Prompt 或 PowerShell）。\n1. 创建并激活虚拟环境\n1 2 3 4 5 # 创建环境：python 3.10 具有最佳的库兼容性 conda create -n torch_env python=3.10 -y # 激活环境 conda activate torch_env torch_env：为所创建的虚拟环境名称。\npython=3.10：为该虚拟环境中安装的 python 版本。\n-y：表示安装过程中的一切询问都默认 yes。\n2. 安装 PyTorch 核心库\n根据硬件平台选择安装命令。请确保已提前激活虚拟环境。\n运行平台 安装命令 硬件说明 Mac (M系列) pip install torch torchvision torchaudio 原生支持 MPS 硬件加速 Windows (NVIDIA) pip install torch torchvision torchaudio --index-url [https://download.pytorch.org/whl/cu121](https://download.pytorch.org/whl/cu121) 需要 CUDA 驱动支持 (示例为 12.1 版) 通用 (仅 CPU) pip install torch torchvision torchaudio 无显卡加速，适合基础逻辑测试 💡 核心参数详解：--index-url\n为什么要手动指定源地址？ pip 默认从 PyPI 下载，但 PyPI 仓库通常只存放通用版。PyTorch 官方源提供的包是针对不同 CUDA 版本专门编译优化的。\n作用：显式指定下载服务器的地址，引导 pip 去特定的仓库“提货”。\n平台差异：\nWindows/Linux (GPU)：必须添加。不加此参数会导致 pip 默认安装 CPU 版本，无法调用 NVIDIA 显卡算力。\nMac (M系列)：通常不加。苹果的 MPS 加速代码已直接集成在 PyTorch 的标准版本中。\n1.4 第四步：跨平台验证脚本 1 2 3 4 5 6 7 8 9 10 import torch print(f\u0026#34;PyTorch 版本: {torch.__version__}\u0026#34;) if torch.cuda.is_available(): print(f\u0026#34;✅ 检测到 NVIDIA GPU，正在使用 CUDA 加速！设备：{torch.cuda.get_device_name(0)}\u0026#34;) elif torch.backends.mps.is_available(): print(\u0026#34;✅ 检测到 Apple Silicon，正在使用 MPS 加速！\u0026#34;) else: print(\u0026#34;ℹ️ 当前使用 CPU 模式。\u0026#34;) 二、 PyTorch 张量基础 (Tensor) 2.1 什么是张量？ 张量是神经网络中最基础的数据结构，可以看作是标量、向量、矩阵向更高维度的推广。\n0 维张量：标量 (Scalar)，即单个数字。\n1 维张量：向量 (Vector)，类似于列表。\n2 维张量：矩阵 (Matrix)，二维数组。\n3 维张量及以上：多维数组。\n2.2 创建 Tensor 1. 常用初始化方法\n1 2 3 4 5 6 7 8 9 10 11 # 随机初始化 (0-1 均匀分布) x = torch.rand(5, 3) # 全 0 矩阵初始化 x = torch.zeros(5, 3, dtype=torch.long) # 全 1 矩阵初始化 x = torch.ones(5, 3, dtype=torch.long) # 从现有数据直接创建 x = torch.tensor([5.5, 3], dtype=torch.float) 2. 基于现有 Tensor 创建\n方法 说明 继承属性 x.new_ones(shape) 需要手动指定新形状 类型 (dtype), 设备 (device), 自动微分 (requires_grad) torch.randn_like(x) 无需指定形状 形状, 类型, 设备 3. 查看属性\n数据类型：x.dtype (同质数据类型)。\n形状/大小：x.shape 或 x.size() (互为别名)。\n所在设备：x.device (CPU/CUDA/MPS)。\n2.3 张量的运算操作 1. 三种加法实现方式\n运算符重载：print(x + y)\n函数调用：torch.add(x, y)\n原地操作 (In-place)：y.add_(x)\n(注意：PyTorch 中以 _ 结尾的方法会直接修改调用者。)\n2. 索引与内存共享 (重要)\nPyTorch 索引遵循 视图 (View) 机制。\n1 2 3 x = torch.rand(5, 3) y = x[:, 0] # 切片第一列 y += 1 # 修改 y 会同步改变 x 的第一列 核心原理：索引结果与原数据共享内存。\n优点：节省显存，避免拷贝。\n风险：修改子集会意外改变原张量。需独立副本请使用 y = x[:, 0].clone()。\n三、 Autograd 自动求导系统 核心定义：Autograd 是 PyTorch 的自动求导引擎，通过构建 动态计算图 (Dynamic Computational Graph) 记录操作历史，计算反向传播梯度。\n核心组件：grad_fn (算子记录) 与 grad (梯度存储)。\n3.1 计算图的基石 1.1 开启梯度追踪\nrequires_grad 默认为 False。设为 True 后开启追踪。\n传递性：参与运算的张量只要有一个为 True，输出也为 True。\n1.2 理解 grad_fn (计算记录器)\n叶子节点 (Leaf)：通过 torch.tensor() 等原始创建的张量，grad_fn 为 None。\n非叶子节点：运算生成的张量，grad_fn 指向具体的 Function 对象（如 AddBackward）。\n1 2 3 4 5 6 7 8 x = torch.randn(3, 5, requires_grad=True) y = torch.randn(3, 5) # 默认为 False z = x + y # z.grad_fn 为 \u0026lt;AddBackward0\u0026gt; t = z ** 2 # t.grad_fn 为 \u0026lt;PowBackward0\u0026gt; print(x.is_leaf) # True print(z.is_leaf) # False 3.2 反向传播：backward() 2.1 梯度计算逻辑\n调用 loss.backward()，Autograd 逆流计算导数并存入叶子节点的 .grad。\n2.2 约束与内存分配\n标量触发：通常对单一数值（如 Loss）调用 backward()。\n非标量触发：非标量需传入形状相同的权重参数。\n内存分配：默认只保留叶子节点的 .grad。中间变量欲保留需调用 z.retain_grad()。\n1 2 3 4 5 6 7 8 x = torch.ones(2, 2, requires_grad=True) y = x + 2 z = y * y * 3 out = z.mean() # 这是一个标量 out.backward() # 反向传播 print(x.grad) # 叶子节点有梯度值 print(y.grad) # 中间变量默认为 None 3.3 梯度脱离：阻止追踪 with torch.no_grad(): 全局屏蔽梯度计算，常用于模型预测/验证。\n.detach() 方法：分离张量，返回 requires_grad=False 的新视图，共享内存。\n1 2 3 4 5 6 7 8 # 方式 1: 上下文管理器 with torch.no_grad(): y = x * 2 print(y.requires_grad) # False # 方式 2: detach x_detach = x.detach() print(x_detach.requires_grad) # False 3.4 💡 进阶补充：陷阱与技巧 梯度累加：.grad 不会自动清零，反向传播时会累加。训练迭代前需手动调用 optimizer.zero_grad()。\nIn-place 操作：可能破坏反向传播所需数据。建议使用 z = x + y 替代 x.add_(y) 以保安全。\n1 2 3 4 5 # 梯度累加演示 out.backward() print(x.grad) out.backward() print(x.grad) # 梯度值会变大，因为发生了累加 四、 使用 CUDA 加速与并行计算 💡 核心逻辑\nPyTorch 提供了简单的方法将模型和数据从 CPU 移动到 GPU。当单块显卡无法满足需求时，可以使用并行计算技术：DP（简单但不均衡）或 DDP（官方推荐，高性能）。\n4.1 CUDA 核心概念介绍 CUDA（Compute Unified Device Architecture，统一计算设备架构）是 NVIDIA 推出的并行计算平台和编程模型。它允许开发者利用 NVIDIA GPU（图形处理器）强大的并行计算能力来处理复杂的计算任务，而不仅仅局限于图形渲染。\n1. 为什么需要 CUDA？（CPU vs GPU）\n在深度学习和科学计算中，GPU 比 CPU 更具优势，其核心差异在于设计理念：\nCPU（中央处理器）：设计目标是处理复杂的逻辑控制和串行任务。它拥有强大的算术逻辑单元（ALU），但数量较少，擅长“一个老师处理一个复杂的数学难题”。\nGPU（图形处理器）：设计目标是处理大规模并行任务。它拥有数以千计的简单核心，擅长“一千个小学生同时做简单的加法题”。\nCUDA 正是连接这种强大硬件能力的桥梁。\n2. CUDA 的核心架构：软件与硬件的映射\nCUDA 的编程模型将任务拆解为多层级结构，以便于在硬件上高效调度：\n逻辑层级 (Software) 硬件实现 (Hardware) 描述 Thread (线程) CUDA Core 计算的最小单位。 Warp (线程束) - CUDA 调度的基本单位，通常包含 32 个线程，执行相同的指令（SIMT 架构）。 Block (线程块) Streaming Multiprocessor (SM) 一组线程的集合，共享同一块“共享内存”（Shared Memory）。 Grid (网格) Entire GPU 一个核函数（Kernel）启动时产生的所有线程集合。 3. CUDA 的工作流程：Heterogeneous Computing\nCUDA 采用“异构计算”模式，即 Host (CPU) 负责逻辑控制，Device (GPU) 负责大规模数值计算。典型的执行步骤如下：\n分配内存：在 GPU 显存上分配空间。\n数据拷贝 (H2D)：将数据从 CPU 内存拷贝到 GPU 显存。\n启动核函数 (Kernel)：CPU 指挥 GPU 启动成千上万个线程进行并行运算。\n数据回传 (D2H)：将计算结果从 GPU 显存拷贝回 CPU 内存。\n释放资源：清理显存。\n4. CUDA 在 PyTorch 中的角色\n在 PyTorch 中，CUDA 是实现张量加速的底层后端。当你执行 tensor.to(\u0026quot;cuda\u0026quot;) 时，实际上发生了以下动作：\nPyTorch 调用 CUDA API 在显存中开辟空间。\n数据通过 PCIe 总线传输至显卡。\n随后的算子（如矩阵乘法）会调用 NVIDIA 提供的 cuBLAS 或 cuDNN 等高度优化的加速库。\n4.2 常用的并行方式 在深度学习中，常用的并行方法主要分为 数据并行、模型并行 和 混合并行。理解它们的关键在于：任务（数据或参数）是如何被拆分并分发到不同设备上的。\n1. 数据并行 (Data Parallelism, DP)\n这是最常用、最基础的并行方式。它的核心思想是 “模型不动，数据拆分”。\n拆分方式：\n模型副本：每个 GPU 上都完整地拷贝一份相同的模型参数。\n数据切分：将一个大的 Batch（批次）数据平均拆分成 N 份（N 为 GPU 数量）。例如，Batch Size 为 64，有 4 张显卡，则每张显卡分到 16 个样本。\n工作流程：\n分发：主设备将不同的数据子集发给各 GPU。\n并行计算：每个 GPU 独立进行前向传播和反向传播，计算出各自的梯度。\n梯度同步：各 GPU 将计算出的梯度进行汇总（All-Reduce），取平均值。\n参数更新：所有 GPU 使用相同的平均梯度同步更新自己的模型参数，确保下一轮迭代时模型依然一致。\n2. 模型并行 (Model Parallelism, MP)\n当模型极其巨大（如千亿级参数量），单张显卡的显存连模型本身都装不下时，就需要模型并行。它的核心思想是 “数据不动，模型拆分”。\n模型并行又细分为 张量并行 和 流水线并行：\n2.1 张量并行 (Tensor Parallelism, TP)\n拆分方式：将模型中的某个层（如大型矩阵乘法层）拆开。\n并行逻辑：将一个巨大的权重矩阵 W 拆分为 W_1 和 W_2，分别放在 GPU 0 和 GPU 1 上。计算时，数据会同时进入两张显卡进行局部计算，最后通过通信合并结果。\n特点：通信非常频繁，通常要求显卡之间有极高的带宽（如 NVLink）。\n2.2 流水线并行 (Pipeline Parallelism, PP)\n拆分方式：按模型的层（Layer）进行纵向切割。\n并行逻辑：将模型的前一半层放在 GPU 0，后一半层放在 GPU 1。\n工作流程：数据像流水线一样，先在 GPU 0 处理，结果传给 GPU 1 继续处理。\n优化：为了避免 GPU 空闲（泡泡/Bubble），通常会将数据进一步切分为 Micro-batches（微批次），让不同 GPU 同时处理不同微批次的任务。\n3. 混合并行 (Mixed Parallelism)\n在训练超大规模模型（如 GPT-4, Llama 3）时，通常会将上述方法结合使用，构建成一个 3D 并行 体系：\n数据并行：扩大训练规模，增加吞吐量。\n张量并行：解决单个超大层（如 Attention 层）的显存瓶颈。\n流水线并行：跨节点连接不同的模型切片。\n4. 总结与对比\n方法 拆分对象 解决的问题 通信开销 数据并行 (DP/DDP) 数据 (Samples) 提高训练速度，增加 Batch Size 中等（同步梯度） 张量并行 (TP) 层内参数 (Tensors) 单层参数量过大，显存溢出 极高（同步神经元输出） 流水线并行 (PP) 网络层数 (Layers) 模型层数过多，单卡装不下整个模型 较低（同步层间激活值） 在实际操作中，PyTorch 提供的 DDP (Distributed Data Parallel) 是最推荐的通用数据并行工具；而对于大模型训练，则通常使用 DeepSpeed 或 Megatron-LM 这种高度集成的混合并行框架。\n4.3 单卡训练 (Single GPU) 要让 GPU 运行，必须确保模型和数据都在同一块显存中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 if torch.cuda.is_available(): device = torch.device(\u0026#34;cuda\u0026#34;) # NVIDIA GPU elif torch.backends.mps.is_available(): device = torch.device(\u0026#34;mps\u0026#34;) # Apple Silicon GPU else: device = torch.device(\u0026#34;cpu\u0026#34;) # 默认 CPU # 1. 搬运模型（一次性） model = Net().to(device) # 2. 搬运数据（在训练循环中） for image, label in dataloader: image = image.to(device) label = label.to(device) 4.4 DP vs DDP 1. DataParallel (DP)\n1 2 3 4 5 6 7 # --- Linux/Windows 版本 --- if torch.cuda.device_count() \u0026gt; 1: model = torch.nn.DataParallel(model) model.to(device) # --- macOS 版本 --- # Mac M 系列暂不支持显卡间的 DataParallel，通常直接使用 .to(device) 运转方式：单机多卡\nDP 的核心是“单进程多线程”模式。\n内存空间的限制：在单机上，所有的线程都运行在同一个进程的内存空间内，主线程可以非常方便地通过 Python 内部指令去指挥不同的显卡线程。\n通信协议的缺失：DP 并没有设计一套能够跨越物理机器、通过网络（如以太网或 InfiniBand）进行数据交换的协议。它只能利用单机内部的 PCIe 总线进行极速通信。\n在 DataParallel (DP) 模式下，存在一个“主卡”（通常是 device_id 序列中的第一块卡，默认是 cuda:0），它扮演着“调度中枢”的角色。\n深度理解：单进程多线程\n单进程：当你运行 Python 脚本时，操作系统只启动了一个 Python 进程。这意味着所有的显卡控制逻辑都挤在这个进程里。\n多线程：为了同时操作多张显卡，该进程会启动多个线程，每个线程负责控制一块显卡。\n瓶颈：由于 Python 存在 GIL（全局解释器锁），在同一时间只能有一个线程执行 Python 字节码。这导致 DP 在控制多块显卡时，线程切换的开销很大，无法发挥显卡的最高效率。\n主卡（Master Node）的具体职责\n在 DP 模式下，主卡不仅要参与计算，还要承担繁重的“管理工作”：\n分发 (Scatter)：主卡负责将从磁盘读取的一个 Mini-Batch 数据，切分成更小的块（Sub-batches）。主卡通过 PCIe 总线将这些数据块分发给其他的从卡（Slave Nodes）。\n模型同步 (Replicate)：主卡将自己的模型参数（Weights）完整地拷贝到每一块从卡上，确保大家算的是同一个模型。\n计算 (Parallel Apply)：所有卡（包括主卡）并行进行前向传播（Forward Pass）。\n汇总 (Gather \u0026amp; Reduce)：关键点：所有从卡计算出的输出（Outputs）必须传回主卡。主卡负责收集所有的输出，计算总的 Loss，并分发给各卡进行反向传播计算梯度。最后，主卡再次收集所有卡算出的梯度，进行平均（Reduce），并在主卡上更新参数。\n为什么会“负载不均衡”？\n由于上述的分发和汇总全部由主卡完成，你会观察到以下现象：\n显存占用不均：主卡因为要暂存所有卡的输出和梯度，其显存占用（Memory Usage）通常比其他卡高出很多。\n计算浪费：当显卡数量增多时，主卡忙于通信和汇总，会导致其他显卡进入短暂的“空转”等待状态。\n笔记补充：Linux 与 Mac 的差异\n维度 Linux/Windows (DP) macOS (M 系列) 进程模型 单进程多线程 单进程单线程 主从结构 存在明确的主卡（Master） 不存在，因为通常只有一块集成 GPU 内存逻辑 显存与内存独立，存在 H2D/D2H 拷贝 统一内存架构 (UMA)，无需跨设备拷贝数据 总结\nDataParallel 就像是一个中央集权的组织：主卡是决策者，负责分发任务、收集结果并总结。而 DistributedDataParallel (DDP) 则像是分布式联邦：每个 GPU 都是独立的进程，通过彼此交换信息（Ring-AllReduce）达成共识，因此效率更高、负载更稳。这种“单进程”与“多进程”的区别，正是 DP 逐渐被 DDP 取代的核心原因。\n2. DistributedDataParallel (DDP)\nDistributedDataParallel (DDP) 是 PyTorch 官方强烈推荐的并行训练方案。与传统的 DataParallel (DP) 相比，它在底层设计上有着本质的飞跃，是目前工业界处理大规模模型训练的标准工具。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # --- Linux/Windows (NVIDIA NCCL) --- import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP dist.init_process_group(backend=\u0026#39;nccl\u0026#39;) # NVIDIA 专用高速后端 local_rank = dist.get_rank() torch.cuda.set_device(local_rank) device = torch.device(\u0026#34;cuda\u0026#34;, local_rank) model = model.to(device) model = DDP(model, device_ids=[local_rank]) # --- macOS (Gloo 后端) --- # Mac 不支持 NCCL，若需跑通 DDP 流程进行调试，需使用 gloo 后端 if torch.backends.mps.is_available(): dist.init_process_group(backend=\u0026#39;gloo\u0026#39;) # Mac 仅支持 gloo # 注：DDP 在 Mac MPS 上的加速效果有限，通常仅用于代码调试 运转方式：多机多卡\nDDP 的核心设计：多进程模式\nDDP 的精髓在于 “多进程”。\n进程分配：DDP 会为每一块显卡启动一个独立的 Python 进程。例如，如果你有 8 张显卡，系统就会运行 8 个独立的程序副本。\n消除 GIL 锁瓶颈：由于每个进程都有自己独立的 Python 解释器，因此彻底避开了 Python 全局解释器锁（GIL）的限制，让每块显卡都能满载运行。\n去中心化：不同于 DP 需要一个“主卡”来汇总所有数据，DDP 中的每个进程地位平等，它们通过相互通信来同步梯度。\n核心技术：Ring-AllReduce 算法\nDDP 高效的关键在于梯度同步的方式。它不采用“汇总到中心再分发”的模式，而是采用 Ring-AllReduce 算法：\n环形通信：所有显卡连接成一个逻辑上的环。\n切片交换：每个显卡将自己的梯度切成小块，只传递给环中的下一个邻居。\n带宽利用率高：无论显卡数量如何增加，每块显卡承担的通信量基本恒定。这使得 DDP 在显卡数量极多时（如百卡、千卡集群）依然能保持极高的效率。\nDDP 的工作流程\n初始化：启动 N 个进程，通过网络建立连接（使用 nccl 或 gloo 后端）。\n数据分发：使用 DistributedSampler 确保每个进程读取的数据子集是不重叠的。\n前向传播：每个进程在自己的显卡上运行模型，计算输出。\n梯度计算与同步：在反向传播期间，各进程通过 AllReduce 异步交换梯度并取平均值。\n更新参数：所有进程使用完全相同的平均梯度更新本地模型参数，确保下一轮训练开始前，所有显卡上的模型依然是镜像一致的。\n跨平台适配建议 (Linux vs Mac)\n特性 Linux/Windows (NVIDIA) macOS (Apple Silicon) 通信后端 NCCL (NVIDIA 专用，性能最强) Gloo (Apple M系列目前仅支持此后端) 启动方式 推荐使用 torchrun 工具 推荐使用 torchrun 或多进程脚本 优势 极高的吞吐量，支持多机多卡 主要用于分布式代码本地调试 进阶知识点：DistributedSampler\n在 DDP 模式下，必须配合使用 DistributedSampler。\n原理：如果直接使用普通 DataLoader，每个进程都会从第一条数据开始读，导致所有 GPU 都在跑同样的数据，白白浪费算力。DistributedSampler 会根据当前的 rank（进程编号）和 world_size（总进程数）对数据集进行切分，确保“各跑各的”。 保存模型的注意事项\n在使用 DDP 后，模型会被包装在 DistributedDataParallel 对象中。\n保存：必须使用 torch.save(model.module.state_dict(), PATH)，加上 .module 才能剥离分布式外壳，保存纯净的模型参数。\n加载：加载时也要注意，如果是在单卡环境加载 DDP 保存的模型，可能需要处理键名中的 module. 前缀。\n2.3.1 DDP 核心概念：进程组（Process Group） 在分布式计算中，DDP 通过“进程组”来管理多个计算单元。\nGROUP: 进程组，默认为全局组（World）。可以通过 new_group 创建子集进行精细通信。\nWORLD_SIZE: 全局总进程数。\n单机多卡：GPU 总数。\n多机多卡：机器数 × 每台机器的 GPU 数。\nRANK: 全局进程序号（0 到 WORLD_SIZE-1）。Rank 0 通常作为 Master 节点。\nLOCAL_RANK: 单个节点（机器）内的 GPU 编号。由启动工具自动分配。\n2.3.2 DDP 代码编写流程 在使用 DDP 时，代码的逻辑结构需要进行“分布式改造”：\n1. 自动获取 Local Rank\n不再手动指定 GPU ID，而是通过环境变量或命令行参数接收启动工具传入的编号。\n1 2 3 4 5 6 7 8 9 10 11 12 13 import argparse import os # 推荐：从环境变量自动获取（torchrun 模式） local_rank = int(os.environ.get(\u0026#34;LOCAL_RANK\u0026#34;, 0)) # 必须设置当前进程使用的设备 if torch.cuda.is_available(): torch.cuda.set_device(local_rank) device = torch.device(\u0026#34;cuda\u0026#34;, local_rank) elif torch.backends.mps.is_available(): # Mac 虽不支持 NCCL 并行，但可用于代码逻辑调试 device = torch.device(\u0026#34;mps\u0026#34;) 2. 初始化进程组（后端选择）\n选择合适的通信后端（Backend）是性能的关键：\n后端 硬件环境 建议场景 NCCL NVIDIA GPU 最佳选择。支持 InfiniBand 和 Ethernet，性能最高。 GLOO CPU / Mac CPU 分布式首选。在 Mac 上调试 DDP 时使用。 MPI 高性能计算集群 仅在有特殊 MPI 需求的环境下使用。 1 2 3 # 初始化 # Mac 用户请将 backend 改为 \u0026#39;gloo\u0026#39; torch.distributed.init_process_group(backend=\u0026#39;nccl\u0026#39; if torch.cuda.is_available() else \u0026#39;gloo\u0026#39;) 3. 数据集划分：DistributedSampler\n为了确保 N 个进程不跑重复的数据，必须对数据进行“切片”。\n1 2 3 4 5 6 7 8 # 仅训练集需要 Sampler，测试集通常不需要 train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset) train_loader = torch.utils.data.DataLoader( train_dataset, batch_size=16, sampler=train_sampler # 传入采样器 ) 4. 包装模型\n1 2 3 # 使用 DDP 包装 model = model.to(device) model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank] if torch.cuda.is_available() else None) 2.3.3 如何启动 DDP 训练 DDP 不能直接用 python main.py 启动，需要使用专门的启动器来同步开启多个进程。\nA. Linux/Windows (单机多卡)\n使用 torchrun（PyTorch 官方目前推荐，取代了旧版的 launch）：\n1 2 # 使用 0,1,2,3 号显卡，每台机器 4 个进程 CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 main.py B. macOS (本地逻辑调试)\n由于 Mac 只有一块集成 GPU，多进程并行的意义在于调试分布式代码：\n1 2 # 在 Mac 上模拟 2 个进程（使用 GLOO 后端） torchrun --nproc_per_node=2 main.py 💡 经验之谈 (Obsidian Tips)\n网络接口设置\n如果服务器有多块网卡（如同时有以太网和 InfiniBand），NCCL 可能会找错接口导致挂起。\n可以在代码中手动指定：\n1 2 # 找到 ifconfig 中对应的网卡名，如 eth0 os.environ[\u0026#39;NCCL_SOCKET_IFNAME\u0026#39;] = \u0026#39;eth0\u0026#39; DDP vs DP 的性能\n即使在单机多卡环境下，也优先选择 DDP。\nDDP 没有单进程的 GIL 锁限制。\nDDP 的梯度同步（Ring-AllReduce）比 DP 的主卡汇聚快得多。\nDDP 支持跨机器扩展，而 DP 只能死守单机。\n4.5 💡 并行计算对比表 方案 易用性 性能 场景建议 DP ⭐⭐⭐⭐⭐ ⭐⭐⭐ 快速实验、双卡轻量训练 DDP ⭐⭐⭐ ⭐⭐⭐⭐⭐ 工业界标准、大型模型训练 五、 AI 硬件加速设备 💡 核心逻辑\n深度学习对算力的渴求推动了硬件从 通用（CPU） 向 并行（GPU） 再向 专用（ASIC/TPU/NPU） 的演进。\nCPU：全能管家，逻辑控制强，计算效率低。\nGPU：并行专家，浮点运算强，功耗高，依赖 CPU。\nASIC：定制工匠，为特定算法设计，性能功耗比极高。\n5.1 通用处理器：CPU vs GPU 特性 CPU (中央处理器) GPU (图形处理器) 设计目标 处理复杂指令、分支跳转、逻辑控制 处理海量、简单的数据并行运算 架构特点 控制单元大，晶体管主要用于缓存和逻辑 大量晶体管组成专用电路和流水线 计算瓶颈 冯·诺依曼结构导致频繁读取指令/存储 功耗高，无法独立工作，需 CPU 调用 5.2 TPU (Tensor Processing Unit, 张量处理器) TPU 是谷歌为优化 TensorFlow 框架而定制的 ASIC 芯片，专为神经网络计算设计。\n1. 核心架构：脉动阵列 (Systolic Array)\n矩阵乘法单元 (MMU)：TPU 的心脏。包含 $256 \\times 256$ 个 MAC 部件，每个周期可执行 65,536 次 8 位乘加操作。\n计算逻辑：数据像血液脉动一样流过芯片。多个运算逻辑单元（ALU）串联，复用一个寄存器的读取结果，极大降低了内存带宽压力。\n性能指标：以 700MHz 运行，每秒可执行约 92 万亿次矩阵运算。\n2. 技术特点\nAI 加速专用：特定领域架构 (DSA)，指令集精简，深度学习效率极高。\n确定性功能：舍弃了缓存、分支预测等复杂逻辑。运行时间可精准预测，使芯片能以接近峰值的吞吐量运行。\n大规模片上内存：一代 TPU 拥有占芯片面积 35% 的内存（24MB 局部内存 + 4MB 累加器内存），节约了访存能耗。\n5.3 NPU (Neural-network Processing Unit, 神经网络处理器) NPU 采用“数据驱动并行计算”架构，旨在解决冯·诺依曼结构中存储与处理分离导致的效率瓶颈。\n1. 寒武纪 DianNao (电脑) 系列演进\n型号 定位与特点 核心组件 (NFU) DianNao 基础加速器，模拟神经元工作 NFU-1: 256个乘法器\nNFU-2: 加法树\nNFU-3: 激活函数单元 DaDianNao 多核升级版，专攻训练任务 $16 \\times 16$ 尺寸 NFU，数据通路更灵活 ShiDianNao 机器视觉专用，2D 格点结构 唯一考虑运算单元级数据重用的 2D 阵列 PuDianNao 异构加速器，支持多种机器学习算法 MLU (机器学习单元) + ALU (处理通用计算) 2. NPU 的运算逻辑 (以 DianNao 为例)\n向量/卷积运算：NFU-1 完成元素相乘，NFU-2 完成结果累加，NFU-3 完成激活映射。\n池化运算：利用 NFU-2 完成最大值或平均值提取。\nPuDianNao 的多算法支持：支持神经网络、SVM、朴素贝叶斯、K-Means 等 7 种算法，是该系列中功能最灵活的单元。\n💡 核心知识点总结表\n硬件类型 核心优势 主要缺点 适用场景 CPU 极高的灵活性、逻辑处理 算力密度低、访存延迟高 逻辑控制、前处理 GPU 通用并行计算、成熟生态 功耗巨大、数据吞吐效率上限 通用深度学习训练 TPU 极速矩阵运算、低延迟 仅限特定框架、灵活性差 云端大规模推理/训练 NPU 存储处理一体化、低功耗 硬件逻辑固定、开发门槛高 手机/监控等移动端 AI 六、 PyTorch 各模块的使用 6.1 从机器学习到深度学习 1.1 什么是机器学习 (ML)？ 机器学习是人工智能的一个子集，其核心在于“不通过显式编程来赋予计算机学习能力”。它通过算法从数据中寻找模式，并利用这些模式对未知数据进行预测。\n特征工程：传统机器学习（如 SVM、随机森林）极其依赖人工设计的特征。如果特征选得不好，模型性能上限会很低。 1.2 什么是深度学习 (DL)？ 深度学习是机器学习的一种特殊形式，利用多层人工神经网络来学习数据的高阶抽象表示。\n端到端学习：它最大的优势是自动特征提取。你只需输入原始数据（如像素），神经网络会自动学习从简单线条到复杂轮廓的特征。 1.3 核心对比总结 维度 机器学习 深度学习 数据量 少量数据即可起步 极度依赖海量数据（大数据） 硬件 普通 CPU 即可 依赖高性能 GPU/TPU 特征工程 人工干预多，需专家知识 自动化程度高，端到端学习 可解释性 较强（如决策树） 较弱（“黑盒”模型） 6.2 深度学习项目标准流程 (PyTorch 视角) 在安装 PyTorch 时，官方通常建议通过一行命令同时安装 torch、torchvision 和 torchaudio。这三者构成了 PyTorch 开发的“三剑客”，分别负责底层核心、视觉任务和音频任务。\n以下是它们的详细解析及用途：\ntorch (核心引擎) 这是 PyTorch 的主体库，也是所有操作的基础。\n核心功能：\n张量计算 (Tensors)：提供类似于 NumPy 但支持 GPU 加速的多维数组运算。\n自动求导 (Autograd)：深度学习的灵魂，自动计算梯度以实现反向传播。\n神经网络模块 (nn.Module)：定义层、损失函数和各种架构。\n用途：它是所有 AI 模型运行的底座，负责处理数学运算和硬件调度（如前文提到的 CPU、GPU、TPU 资源分配）。\ntorchvision (计算机视觉专家) 这是一个专门为图像和视频处理设计的扩展库，极大地简化了视觉任务的开发。\n核心功能：\n标准数据集 (datasets)：内置了如 MNIST、CIFAR10 等常用数据集，并提供了 ImageFolder 等工具来读取按目录存放的图片数据。\n图像变换 (transforms)：提供缩放、裁剪、归一化等预处理功能。\n底层 IO：如 read_image 函数，可直接将图片文件读取为 PyTorch 张量。\n预训练模型：内置了 ResNet、VGG、YOLO 等经典的成熟模型，可以实现“拎包入住”式的迁移学习。\n用途：只要你的任务涉及图片识别、目标检测或视频分析，就必然会用到它。\ntorchaudio (音频处理专家) 类似于 torchvision，它是专门针对音频和信号处理的扩展库。\n核心功能：\n音频加载：支持 wav、mp3 等格式的加载，并自动转换为张量。\n信号变换：可以轻松将声波转换为梅尔频谱 (Mel-spectrogram) 等模型更易理解的形式。\n专业算法：内置了重采样、滤波、波形增强等音频专用算法。\n用途：适用于语音识别、音乐生成、声纹识别等任务。\n总结对比\n模块名称 专注领域 核心作用 torch 通用深度学习 提供张量运算、自动求导和硬件加速。 torchvision 图像/视频处理 图片数据、执行数据增强、调用预训练视觉模型。 torchaudio 音频/语音 加载音频文件、转换频谱、处理音频信号。 💡 关于 Dataset 与 DataLoader 的“户口”归属\n这是一个非常关键的逻辑点，决定了你代码的组织方式：\n核心规则在 torch：Dataset 基类与 DataLoader 工具的源代码其实都在 torch 核心库中（位于 torch.utils.data）。它们定义了 PyTorch 处理数据的通用游戏规则：无论你处理的是图片、文字还是音频，都必须遵循“定义仓库 (Dataset)”和“架设传送带 (DataLoader)”的流程。\n现成素材在 torchvision / torchaudio：这两个模块是根据上述规则，针对特定领域提供的样板间。例如，torchvision.datasets.ImageFolder 就是一个已经按照 torch 的规范写好的 Dataset 子类，专门用来读图片。\n深度总结：数据如何流转？\n我们可以用下面这个流程图来理解它们是如何协同工作的：\n现实世界：原始的 .jpg 图片或 .wav 音频文件。\n翻译阶段 (torchvision / torchaudio)：利用这些模块中的工具（如 read_image 或自定义的 MyDataset）将原始文件读取、裁剪、缩放。\n标准化阶段 (Dataset)：数据被包装成统一的格式，并明确“一共有多少”和“怎么拿一个”。\n分发阶段 (DataLoader)：利用 torch 自带的传送带，将数据打包成 Batch，并决定是否打乱顺序。\n计算阶段 (torch)：数据最终以 Tensor（张量） 的形式进入显卡，交给 torch 进行疯狂的矩阵运算。\n核心结论：torch 负责计算，不挑食；torchvision/torchaudio 负责洗菜切菜，把原始食材加工成 torch 爱的张量。 虽然我们在做视觉任务时经常引用 torchvision，但请记住，DataLoader 永远是你从 torch 主库里请来的“搬运工”。\n2.1 基本配置 (Basic Configuration) 这是项目的“地基”。\n环境设置：包括导入 torch 库，设置随机种子以确保结果可复现。\n设备分配：检测并指定运行设备（cpu、cuda 或 mps）。\n超参数定义：统一管理学习率 (LR)、Batch Size、训练轮数 (Epochs) 等关键参数。\n2.2 数据读入 (Data Loading) PyTorch 的数据读入是通过 Dataset 与 DataLoader 协作完成的。\nDataset：定义了数据的“仓库规范”。它规定了数据在哪、有多少、每一份长什么样。\nDataLoader：定义了数据的“分发规则”。它负责按批次（Batch）抓取数据、打乱顺序（Shuffle）以及多进程加速。\n1. 构建自己的数据读取流程\n要实现灵活的数据读取，我们需要定义一个继承自 torch.utils.data.Dataset 的类。这个自定义类必须包含以下三个核心函数：\n__init__：用于向类中传入外部参数（如文件路径、变换操作），并定义样本集。\n__len__：返回数据集中的样本总数。\n__getitem__：用于逐个读取样本集合中的元素。在这里可以进行数据变换，并返回训练或验证所需的 (data, label) 对。\n2. 常见的数据读取方式\n根据数据的复杂程度，通常有以下三种处理方案：\n方案一：简单情况 —— 使用 PyTorch 内置数据集\n如果你处于学习阶段，正在练习经典模型，torchvision.datasets 已经为你封装好了现成的类。\n适用场景：MNIST、CIFAR-10、COCO 等标准学术数据集。\n优点：一行代码即可实现自动下载、解析和加载。\n1 2 3 4 5 6 7 8 9 from torchvision import datasets, transforms # 以 MNIST 为例（如果你想用 CIFAR10，只需把 MNIST 改掉即可） train_data = datasets.MNIST( root=\u0026#39;./data\u0026#39;, # 1. 存储路径 train=True, # 2. 训练集 vs 测试集 download=True, # 3. 是否自动下载 transform=transforms.ToTensor() # 4. 数据变换（最关键的一步） ) 参数详解\n参数名 作用 深度解析 root 存储路径 指定数据集下载后存放在本地的哪个文件夹。如果文件夹不存在，它会自动创建。建议统一放在 ./data 下方便管理。 train 模式选择 True：加载训练集（通常 60,000 张）；False：加载测试集（通常 10,000 张）。你需要分别创建这两个对象。 download 自动下载 True：如果 root 路径下没找到数据，就去官网下载。如果已经有了，它会跳过下载直接加载，非常智能。 transform 数据变换 必考点。原始下载的数据通常是 PIL 图片格式，模型看不懂。ToTensor() 将其转为 Tensor 并将像素值归一化到 [0, 1] 之间。 为什么 transform 这一步必不可少？\n你可能会问：“我不设置 transform 行吗？” 答案是：不行。\n格式不兼容：如果不加 transform，Dataset 返回的是 PIL 图片，当你把它喂给 DataLoader 时，程序会因为无法把图片打包成“张量批次”而报错。\n维度重排（Dimension Reordering）：原始图片（PIL 或 NumPy）通常是 HWC 格式（高度、宽度、通道）。PyTorch Tensor 要求必须是 CHW 格式（通道、高度、宽度）。ToTensor() 会自动把通道维度（如 RGB 的 3 层）挪到最前面。\n数据类型转换（Type Conversion）：将原本是整数类型（uint8，范围 0-255）的像素点，转化为浮点数张量（FloatTensor）。\n归一化缩放（Scaling/Normalization）：它会将像素值从原本的 [0, 255] 自动缩放到 [0.0, 1.0] 之间。这对于防止神经网络在训练初期发生梯度爆炸至关重要。\n“全自动”与“纯手工”的对比\n使用 datasets.MNIST：你不需要关心数据集是从哪个 URL 下载的，不需要关心图片是怎么存在二进制文件里的，甚至不需要关心标签是怎么对齐的。PyTorch 全帮你做了。\n自定义 MyDataset：当你面对公司内部的私有数据（比如 CSV 里的医疗影像）时，你就需要手动实现 __init__ 和 __getitem__ 来告诉 PyTorch 如何读取。\n方案二：标准情况 —— 使用 ImageFolder\n如果你的图片数据已经按照“一个类别一个文件夹”的格式整齐排好了，PyTorch 提供了“懒人神器”。\n逻辑：它会自动扫描目录，将子文件夹名直接映射为标签（Label）。\n代码实现：\n1 2 3 4 5 6 7 8 from torchvision import datasets # 目录结构：data/train/cat/*.jpg, data/train/dog/*.jpg train_data = datasets.ImageFolder( root=\u0026#39;data/train\u0026#39;, transform=data_transform ) # 此时 train_data.classes 会自动识别为 [\u0026#39;cat\u0026#39;, \u0026#39;dog\u0026#39;] 在 PyTorch 中，原始图像（通常是不同尺寸、不同光照的图片文件）在喂给神经网络之前，必须经过统一的清洗和格式转换。这个“清洗和转换”的过程，就被统称为 Transform（变换）。\ndata_transform 是如何定义的？\n通常我们会使用 torchvision.transforms.Compose 将一系列操作像“串糖葫芦”一样串起来。\n1 2 3 4 5 6 7 8 9 10 11 from torchvision import transforms data_transform = transforms.Compose([ transforms.Resize((224, 224)), # 1. 统一尺寸：管你原图多大，全变成 224x224 transforms.RandomHorizontalFlip(), # 2. 数据增强：随机左右翻转，增加模型鲁棒性 transforms.ToTensor(), # 3. 核心转换：把图片转为 Tensor，并归一化到 [0, 1] transforms.Normalize( # 4. 标准化：让数据符合正态分布，加速收敛 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) 它在整个流程中起什么作用？\n在你的代码 datasets.ImageFolder(root=..., transform=data_transform) 中，一旦你绑定了这个变量，就会触发以下连锁反应：\n按需加工：每当 DataLoader 准备去抓取一张图时，这张图会先经过 data_transform 里的所有步骤。\n数据增强（Data Augmentation）：通过随机翻转、裁剪、旋转等操作，同一张图在每一轮训练中看起来可能略有不同，这能有效防止模型“死记硬背”，提升泛化能力。\n格式对齐：神经网络对输入非常挑剔。data_transform 确保了进入模型的所有数据都是同样的大小、同样的数值范围、同样的 Tensor 格式。\n方案三：真实/复杂情况 —— 必须自定义 Dataset\n在实际项目或科研中，数据往往是碎片化的（如标签在 CSV 里、输入是图文混排、或者是音频/点云等）。此时必须通过继承 Dataset 类来自定义逻辑。\n核心“函数”：\n你需要在一个类中实现三个关键动作：\n__init__：初始化仓库（读取路径、读取 CSV 标签）。\n__len__：告诉程序一共有多少条数据。\n__getitem__：根据索引 idx 拿出一条数据（并在这里做数据清洗/变换）。\n代码模板：\n假设你的标签存储在一个 CSV 文件中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import os import pandas as pd from torch.utils.data import Dataset # 导入 Dataset 基类 from torchvision.io import read_image # 专门用于将图片读取为张量的工具 # 自定义类必须继承 torch.utils.data.Dataset class MyDataset(Dataset): def __init__(self, annotations_file, img_dir, transform=None): \u0026#34;\u0026#34;\u0026#34; __init__: 初始化阶段，主要负责加载元数据（如 CSV 标签） annotations_file: 包含图片文件名和标签的 CSV 路径 img_dir: 图片文件夹的实际存放路径 transform: 预定义的数据变换流水线（可选项） \u0026#34;\u0026#34;\u0026#34; self.img_labels = pd.read_csv(annotations_file) # 使用 pandas 读取 CSV self.img_dir = img_dir # 记录图片目录 self.transform = transform # 记录变换操作 def __len__(self): \u0026#34;\u0026#34;\u0026#34; __len__: 告诉 PyTorch 这个数据集里总共有多少个样本 \u0026#34;\u0026#34;\u0026#34; return len(self.img_labels) # 返回 CSV 文件的行数 def __getitem__(self, idx): \u0026#34;\u0026#34;\u0026#34; __getitem__: 核心函数，根据索引 idx 抓取并返回一个样本 idx: DataLoader 传过来的索引 \u0026#34;\u0026#34;\u0026#34; # 1. 拼接图片的完整磁盘路径 # self.img_labels.iloc[idx, 0] 取的是 CSV 中第 idx 行、第 0 列的内容（文件名） img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0]) # 2. 读取图片并直接转化为 Tensor (张量) # torchvision.io.read_image 会自动处理底层 IO image = read_image(img_path) # 3. 获取对应的标签 # self.img_labels.iloc[idx, 1] 取的是 CSV 中第 idx 行、第 1 列的内容（标签数字） label = self.img_labels.iloc[idx, 1] # 4. 如果定义了 transform，则在返回前对图片进行加工 # 这一步非常灵活，你可以同时处理图片、文字或音频 if self.transform: image = self.transform(image) # 最后返回一个 (数据, 标签) 的元组 return image, label 3. 最终投喂：DataLoader\n不论你用哪种方案生成的 train_data，最后都要交给 DataLoader 这个“传送带”：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from torch.utils.data import DataLoader # 实例化 DataLoader：架设传送带 train_loader = DataLoader( dataset=train_data, # 1. 告诉传送带从哪个仓库（Dataset 实例）取货 batch_size=64, # 2. 批次数：一次打包多少个样本。64 是常用的经验值 shuffle=True, # 3. 洗牌：每一轮（Epoch）开始前是否打乱顺序。防止模型死记硬背 num_workers=4, # 4. 搬运工数量：开启多少个子进程并行读取数据 drop_last=True # 5. 舍弃尾数：如果总数不能被 64 整除，是否丢弃最后那个“不满员”的包 ) # --- 验证读取流程 --- # iter(train_loader) 将传送带转换为一个可迭代的“生成器” # next(...) 则是从这个生成器中手动“抓取”第一个包（第一个 Batch） images, labels = next(iter(train_loader)) # 打印查看这个包的“规格” print(f\u0026#34;当前 Batch 的形状: {images.shape}\u0026#34;) # 预期输出类似: torch.Size([64, 3, 224, 224]) 一句话总结\ntorch 制定规则（Dataset/DataLoader），torchvision/audio 提供素材处理。 掌握了自定义 Dataset，你就拥有了处理任何异构数据的能力。\n4. 验证数据加载情况\n可以通过 iter 和 next 手动查看加载的数据形状及内容。\n1 2 3 4 5 6 7 8 9 10 import matplotlib.pyplot as plt # 获取第一个批次 images, labels = next(iter(val_loader)) print(f\u0026#34;图像张量形状: {images.shape}\u0026#34;) # [batch_size, channels, height, width] # 可视化第一张图片 # 注意：PyTorch 张量是 (C, H, W)，绘图需转为 (H, W, C) plt.imshow(images[0].transpose(1, 2, 0)) plt.show() 经验之谈：在使用 matplotlib 绘图时，一定要记得使用 .transpose(1, 2, 0)。这是因为 PyTorch 默认的通道顺序是 (C, H, W)，而常用的绘图库要求 (H, W, C)。\n6.3 模型构建 (Model Construction) 神经网络的兴起受益于卷积神经网络（CNN）和反向传播算法（BP）的实现。在 PyTorch 中，所有的模型构造几乎都是基于 nn.Module 类完成的，它提供了极高的灵活性。\n1. 神经网络的构造基础\nModule 类是 torch.nn 模块提供的模型构造基类。创建一个模型通常需要重载两个关键函数：\n__init__：创建模型参数（如定义各种层）。\nforward：定义前向计算逻辑（数据怎么流转）。\n示例：多层感知机 (MLP)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import torch from torch import nn class MLP(nn.Module): def __init__(self, **kwargs): # 现代写法：直接使用 super()，Python 3 会自动处理继承链 super().__init__(**kwargs) # 定义全连接层：784 是输入维度（例如 28x28 像素展平后），256 是隐藏层神经元数 self.hidden = nn.Linear(784, 256) self.act = nn.ReLU() # 激活函数层 self.output = nn.Linear(256, 10) # 输出层：对应 10 个类别（如 0-9 数字） def forward(self, x): \u0026#34;\u0026#34;\u0026#34; 定义前向传播逻辑：数据 x 如何穿过网络 \u0026#34;\u0026#34;\u0026#34; x = self.hidden(x) x = self.act(x) return self.output(x) # 实例化与测试 net = MLP() X = torch.rand(2, 784) # 模拟 2 个样本，每个样本 784 个特征 print(net(X).shape) # 输出预期：torch.Size([2, 10]) 关键点：你不需要定义 backward 函数。由于 PyTorch 的 Autograd 机制，系统会自动生成反向传播逻辑。\n2. 自定义参数\n在自定义层时，如果你只是定义一个普通的 torch.Tensor，模型在训练时是看不见它的。你必须告诉 PyTorch：“这是一个需要优化的权重。”\n什么是 nn.Parameter？\n本质：它是 Tensor 的子类。\n自动注册：只要一个 Tensor 被定义为 Parameter，它就会被自动添加到模型的参数列表里。\n训练标记：这意味着当你调用 net.parameters() 时，PyTorch 能找到它；当你运行优化器时，它会被更新。\n为什么需要 ParameterList 和 ParameterDict？\n如果你有很多参数，想用列表或字典存起来，不能直接用 Python 原生的 list 或 dict。\n普通 List/Dict：存放在里面的 Parameter 不会被模型识别，训练时梯度不会更新。\n专用容器：必须使用 nn.ParameterList 或 nn.ParameterDict。它们像“带登记功能的抽屉”，确保放在里面的每一个参数都完成了“入职登记”。\n代码示例：自定义含参层\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import torch from torch import nn class MyCustomLayer(nn.Module): def __init__(self): super().__init__() # 1. 直接定义 Parameter：最常见的方式 self.weight = nn.Parameter(torch.randn(4, 4)) # 2. 使用 ParameterList：像列表一样存储多个参数 # 适合层数不固定的场景 self.list_params = nn.ParameterList([ nn.Parameter(torch.randn(4, 4)) for _ in range(2) ]) # 3. 使用 ParameterDict：通过键值对管理参数 # 适合需要通过名称灵活调用参数的场景 self.dict_params = nn.ParameterDict({ \u0026#39;linear_weight\u0026#39;: nn.Parameter(torch.randn(4, 4)), \u0026#39;bias\u0026#39;: nn.Parameter(torch.randn(4)) }) def forward(self, x, choice=\u0026#39;linear_weight\u0026#39;): # 使用 ParameterList 中的参数进行计算 for p in self.list_params: x = torch.mm(x, p) # 根据键名灵活选择参数 x = torch.mm(x, self.dict_params[choice]) return x # 验证：打印模型参数 net = MyCustomLayer() for name, param in net.named_parameters(): print(f\u0026#34;已识别参数: {name}\u0026#34;) 3. 神经网络中常见的层\n深度学习的魅力在于各式各样的层（全连接、卷积、池化等）。在深度学习中，神经网络的每一层都各司其职，像一条精密工业流水线。在写代码之前，理解这些层的逻辑分工至关重要。\n宏观架构：输入、隐藏与输出\n无论神经网络多复杂，都可以划分为这三大主要区域：\n输入层 (Input Layer)：\n定义：数据的入口。在代码中，它通常不是一个具体的类（如 nn.Linear），而是你传入 forward(self, x) 中的张量 x 的形状 (Shape)。\n作用：确定模型能接收多大的图片或多少个特征。\n隐藏层 (Hidden Layer)：\n定义：模型中用于“学习”和“提取特征”的所有中间层。\n成员：卷积层、池化层以及中间的全连接层都属于隐藏层。\n作用：负责把原始像素转化为高级语义特征（例如从像素点识别出“猫耳朵”）。\n输出层 (Output Layer)：\n定义：模型的最后一层。\n成员：通常是一个全连接层 (Linear)，其输出神经元个数等于你的分类数量。\n作用：给出最终的预测结果（例如：这张图 90% 的概率是猫）。\n卷积层 (Convolutional Layer) —— “特征提取专家”\n逻辑归属：隐藏层。\n卷积层是视觉任务的核心。它通过一组可学习的“过滤网”（卷积核）在图像上滑动，提取局部特征。\n作用：提取局部特征（如边缘、纹理、形状）。\n核心参数：\n卷积核 (Kernel)：进行二维互相关运算的矩阵。\n填充 (Padding)：在输入边缘补 0，常用于保持输出与输入形状一致。\n步幅 (Stride)：滑动的快慢。步幅越大，输出维度减小越快。\n池化层 (Pooling Layer) —— “数据压缩专家”\n逻辑归属：隐藏层。\n池化层不包含任何可学习的参数，它的计算是固定的。\n作用：压缩特征图尺寸，减少计算量，并提高模型对平移的鲁棒性（即物体挪动一点也能识别出来）。\n常见类型：\n最大池化 (MaxPool)：取窗口内的最大值，保留最明显的特征。\n平均池化 (AvgPool)：取窗口内的平均值。\n全连接层 (Fully Connected / Linear Layer) —— “分类决策专家”\n逻辑归属：隐藏层 或 输出层。\n全连接层中的每一个神经元都与前一层的所有神经元相连，进行全局的信息汇总。\n作用：将之前通过卷积和池化提取到的特征进行组合，映射到最终的分类空间。\n数学本质：一个仿射变换（Affine operation），公式为 $y = xW^T + b$。\n输出层应用：在模型的最后，全连接层的输出神经元个数通常等于类别数。\n4. nn.Sequential\n在 PyTorch 的神经网络构建中，如果说 nn.Module 是你的大地基，那么 nn.Sequential 就是一个预装好的“自动化流水线”或“逻辑胶囊”。\n它是一个有序的容器，神经网络模块将按照在构造函数中传递的顺序依次被添加到计算图中，并按顺序执行。\n为什么需要 nn.Sequential？\n在不使用它的情况下，你需要在 __init__ 里定义每一个层，然后在 forward 里手动写出数据流转的过程（如 x = self.layer1(x)，x = self.layer2(x) 等）。使用 nn.Sequential 的核心优势在于：\n代码简洁：将功能相关的层打包在一起，大大缩短了 forward 函数的长度。\n逻辑清晰：可以将模型划分为明显的块（例如：特征提取块、分类块），让结构一目了然。\n自动前向传播：你只需要把输入丢给这个容器，它会自动按照你定义的顺序把数据传给内部的每一个层。\n现代代码示例：AlexNet 中的应用\n参考你提供的 AlexNet 实现，我们可以看到它使用了两个 nn.Sequential 容器，分别管理“卷积特征提取”和“全连接分类”：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import torch from torch import nn class ModernAlexNet(nn.Module): def __init__(self): super().__init__() # 现代 Python 3 写法 # 1. 卷积特征提取层（Hidden Layers - Features） self.conv_pipeline = nn.Sequential( # 输入 1 通道，输出 96 通道，卷积核 11x11，步幅 4 nn.Conv2d(1, 96, 11, 4), nn.ReLU(), nn.MaxPool2d(3, 2), # 更多的卷积层串联 nn.Conv2d(96, 256, 5, 1, 2), nn.ReLU(), nn.MaxPool2d(3, 2), nn.Conv2d(256, 384, 3, 1, 1), nn.ReLU(), nn.Conv2d(384, 256, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(3, 2) ) # 2. 全连接分类层（Hidden \u0026amp; Output Layers - Classifier） self.fc_pipeline = nn.Sequential( nn.Linear(256 * 5 * 5, 4096), nn.ReLU(), nn.Dropout(0.5), # 缓解过拟合 nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 10) # 最终输出层 ) def forward(self, x): # 数据直接流过第一条流水线 x = self.conv_pipeline(x) # 展平：将多维卷积输出转为一维向量进入全连接层 x = x.view(x.size(0), -1) # 数据直接流过第二条流水线 return self.fc_pipeline(x) 注意事项与限制\n虽然 nn.Sequential 非常好用，但它也不是万能的：\n单一输入输出：它只支持数据按照直线运行（单一输入 $\\rightarrow$ 单一输出）。 如果你的模型需要复杂的“跳跃连接”（如 ResNet 的残差结构）或多输入多输出，则不能单纯依赖 Sequential，必须在 forward 中手动控制流转。\n调试粒度：因为层被打包了，如果你想在中间某一层停下来查看张量的形状，Sequential 会显得不够灵活（虽然可以通过 register_forward_hook 解决，但操作较复杂）。\n5. 经典模型示例\n核心模型 A：LeNet\nLeNet 是一个简单的前馈神经网络，经典的“卷积+池化+全连接”结构。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 5) # 1个输入通道，6个输出通道，5x5卷积核 self.conv2 = nn.Conv2d(6, 16, 5) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) x = F.max_pool2d(F.relu(self.conv2(x)), 2) x = x.view(-1, 400) # 展平操作 x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) return self.fc3(x) 核心模型 B：AlexNet\n相比 LeNet，AlexNet 使用了更深的网络、更小的卷积核、以及 nn.Sequential 容器，并引入了 Dropout 来缓解过拟合。\n1 2 3 4 5 6 self.conv = nn.Sequential( nn.Conv2d(1, 96, 11, 4), # 步幅为4 nn.ReLU(), nn.MaxPool2d(3, 2), # ... 更多卷积层 ) 💡 避坑与进阶 Tips\n批量处理限制：torch.nn 只支持小批量 (Mini-batches) 输入。如果只有一个样本，请使用 input.unsqueeze(0) 增加一个“假”的批大小维度。\n梯度清零：在进行反向传播前，务必调用 net.zero_grad() 清空之前的梯度缓存。\n权重更新公式：$weight = weight - learning_rate \\times gradient$\nModule vs Layer：在 PyTorch 中，Module 既可以是一个单独的层，也可以是一个庞大的模型，甚至可以是模型的一个子部件。\n6.4 模型初始化 (Model Initialization) 在神经网络训练中，权重的初始值直接决定了模型的收敛速度和最终精度。\n1. 为什么不能全初始化为 0？\n虽然 nn.Linear 等层默认会有随机初始化，但我们通常会手动干预。\n规避对称性：如果权重全为 0，网络中每个神经元的表现将完全一致，导致梯度消失或模型无法学习复杂的特征。\n加速收敛：合理的初始化能让损失函数从一个更靠近“底谷”的地方开始下降。\n2. PyTorch 的初始化工具箱：torch.nn.init\nPyTorch 在 torch.nn.init 模块中提供了丰富的原地操作（In-place）函数。\n注意：函数名后缀带有下划线 _ 的，表示会直接修改传入的张量。\n常见的初始化方法：\n基础型：uniform_ (均匀分布)、normal_ (正态分布)、constant_ (常数)。\n极简型：ones_ (全1)、zeros_ (全0)。\n进阶型（最常用）：\nXavier 初始化：适用于 Sigmoid 或 Tanh 激活函数，保持输入输出的方差一致。\nKaiming (He) 初始化：专为 ReLU 及其变体设计，是目前视觉模型的主流选择。\n增益值 (Gain) 参考表：\n不同的激活函数对梯度的影响不同，我们需要通过增益值进行修正：\n激活函数 增益值 (Gain) Linear / Identity $1$ Sigmoid $1$ Tanh $5/3$ ReLU $\\sqrt{2}$ Leaky ReLU $\\sqrt{\\frac{2}{1 + \\text{negative_slope}^2}}$ 3. 实战：如何优雅地初始化模型\n在实际开发中，我们不会给每一个层手动写初始化代码，而是通过 isinstance() 判断层的类型，并使用 model.apply() 进行一键处理。\n初始化函数封装：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import torch from torch import nn def initialize_weights(m): \u0026#34;\u0026#34;\u0026#34; m: 传入的模块（层） \u0026#34;\u0026#34;\u0026#34; # 1. 如果是卷积层：使用 Kaiming 初始化 if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight.data, nonlinearity=\u0026#39;relu\u0026#39;) if m.bias is not None: nn.init.constant_(m.bias.data, 0) # 2. 如果是全连接层：使用正态分布初始化 elif isinstance(m, nn.Linear): nn.init.normal_(m.weight.data, mean=0, std=0.01) if m.bias is not None: nn.init.zeros_(m.bias.data) # 3. 如果是批归一化层：权重设为 1，偏置设为 0 elif isinstance(m, nn.BatchNorm2d): m.weight.data.fill_(1) m.bias.data.zero_() # --- 使用方法 --- class MyMLP(nn.Module): def __init__(self): super().__init__() self.conv = nn.Conv2d(1, 16, 3) self.fc = nn.Linear(16*26*26, 10) def forward(self, x): return self.fc(self.conv(x).view(x.size(0), -1)) model = MyMLP() # 核心操作：调用 apply 遍历模型所有子模块并应用初始化函数 model.apply(initialize_weights) 在 PyTorch 中，apply 函数是 nn.Module 提供的一个非常强大的递归工具。如果把你的神经网络比作一棵大树，apply 就像是一个高效的“巡检员”，它会顺着树干（主模型）一直走到每一片叶子（子模块/层）上执行你指定的动作。\n核心定义：apply(fn) 函数的作用是将函数 fn 递归地应用到模型的所有子模块（Submodules）上。这里的子模块包括模型本身、定义的各个层、以及 nn.Sequential 里的每一个部件。\n为什么要用 apply？\n在处理复杂模型（如 AlexNet 或 ResNet）时，模型内部嵌套了很多层。如果你想给每一层做初始化，手动去写 self.conv1, self.fc2 会非常繁琐且容易遗漏。\n一键操作：通过 model.apply(fn)，你只需要写一个针对单层的处理函数，它会自动帮你跑遍全场。\n解耦逻辑：你可以将“构建模型”的代码和“初始化模型”的代码分开，让程序结构更清晰。\n💡 关键 Tips\n原地修改：m.weight.data 配合带下划线的函数（如 normal_）是修改参数的正确姿势。\n不要“贪零”：除非是偏置（Bias），否则权重尽量不要初始化为 0。使用一个小正数（如 0.01）或 Kaiming 初始化效果会好得多。\n现代框架的默认值：PyTorch 的内置层其实自带了不错的默认初始化（通常是 LeCun 或 Xavier 的变体），但在复现特定论文或遇到训练不收敛时，手动初始化是你的第一件武器。\n深度思考：初始化其实是在帮模型“打破僵局”。既然你已经给模型设置好了完美的起跑姿势，接下来我们要不要看看损失函数，学习如何衡量模型在跑道上到底有没有跑偏？\n6.5 损失函数 (Loss Function) 损失函数是模型的“负反馈”来源，它衡量预测值与真实标签之间的差距。\n1. 分类任务 (Classification)\n这类函数用于衡量概率分布之间的差异。\nBCELoss (二分类交叉熵)\n计算二分类任务的交叉熵，要求输入必须是概率形式（通常经过 Sigmoid）。\n关键参数：weight (类别权重)、reduction (计算模式)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import torch from torch import nn # 模拟：3个样本的预测概率和真实标签 m = nn.Sigmoid() loss_fn = nn.BCELoss(reduction=\u0026#39;mean\u0026#39;) # 默认取平均 input = torch.randn(3, requires_grad=True) target = torch.empty(3).random_(2) # 随机生成 0 或 1 output = loss_fn(m(input), target) output.backward() print(f\u0026#39;BCELoss 计算结果: {output.item():.4f}\u0026#39;) CrossEntropyLoss (多分类交叉熵)\n最常用的分类损失，它在内部整合了 LogSoftmax 和 NLLLoss。\n关键参数：ignore_index (忽略某个类别的计算)、weight。 1 2 3 4 5 6 7 # 模拟：3个样本，5个类别的预测得分 loss_fn = nn.CrossEntropyLoss() input = torch.randn(3, 5, requires_grad=True) target = torch.empty(3, dtype=torch.long).random_(5) # 类别索引 output = loss_fn(input, target) print(f\u0026#39;CrossEntropyLoss 计算结果: {output.item():.4f}\u0026#39;) 2. 回归任务 (Regression)\n用于预测具体的连续数值。\nL1 \u0026amp; MSE \u0026amp; SmoothL1 (距离度量)\nL1Loss：计算绝对值差。\nMSELoss：计算平方差。\nSmoothL1Loss：误差小时用平方，大时用绝对值，减轻离群点影响。\n1 2 3 4 5 6 7 8 9 10 11 12 13 input = torch.randn(3, 5, requires_grad=True) target = torch.randn(3, 5) # L1 损失 l1_fn = nn.L1Loss() # MSE 损失 mse_fn = nn.MSELoss() # SmoothL1 损失，beta 控制平滑阈值 smooth_fn = nn.SmoothL1Loss(beta=1.0) print(f\u0026#39;L1: {l1_fn(input, target).item():.4f}\u0026#39;) print(f\u0026#39;MSE: {mse_fn(input, target).item():.4f}\u0026#39;) print(f\u0026#39;SmoothL1: {smooth_fn(input, target).item():.4f}\u0026#39;) 3. 相似度与特殊任务\nKLDivLoss (KL 散度)\n计算相对熵，用于衡量两个概率分布的接近程度。\n关键参数：reduction='batchmean' (在 Batch 维度求平均)。 1 2 3 4 5 6 7 loss_fn = nn.KLDivLoss(reduction=\u0026#39;batchmean\u0026#39;) # 输入通常需要 log_softmax 形式 inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]]).log() target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]]) output = loss_fn(inputs, target) print(f\u0026#39;KLDivLoss 计算结果: {output.item():.4f}\u0026#39;) TripletMarginLoss (三元组损失)\n用于拉近正样本距离，推开负样本距离。\n关键参数：margin (边界距离值)、p (范数阶数)。 1 2 3 4 5 6 7 8 triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2) # 三元组：锚点、正例、负例 anchor = torch.randn(100, 128, requires_grad=True) positive = torch.randn(100, 128, requires_grad=True) negative = torch.randn(100, 128, requires_grad=True) output = triplet_loss(anchor, positive, negative) print(f\u0026#39;TripletLoss 计算结果: {output.item():.4f}\u0026#39;) CTCLoss (连接时序分类)\n用于解决如语音识别、OCR 等时序数据对齐问题。\n1 2 3 4 5 6 7 8 9 10 11 ctc_loss = nn.CTCLoss(blank=0) # T:序列长度, N:批大小, C:类别数 T, N, C = 50, 16, 20 input = torch.randn(T, N, C).log_softmax(2).requires_grad_() target = torch.randint(1, C, (N, 30), dtype=torch.long) input_lengths = torch.full((N,), T, dtype=torch.long) target_lengths = torch.randint(10, 30, (N,), dtype=torch.long) loss = ctc_loss(input, target, input_lengths, target_lengths) print(f\u0026#39;CTCLoss 计算结果: {loss.item():.4f}\u0026#39;) 💡 核心参数 reduction 总结\n所有的损失函数几乎都有这个参数，它决定了返回值的形式：\n'mean' (默认)：计算 Batch 的平均损失，返回标量。\n'sum'：计算 Batch 的总损失，返回标量。\n'none'：不合并，返回每个样本独立的 Loss，形状与输入一致。\n6.6 训练和评估 (Training and Evaluation) 在 PyTorch 中，训练和评估的逻辑框架非常相似，核心区别在于是否更新参数以及是否记录梯度。\n1. 核心开关：模型状态切换\n模型中有一些特殊的层（如 Dropout 和 BatchNorm）在训练和测试时的表现是完全不同的。因此，在开始逻辑前必须先设置状态：\nmodel.train()：训练模式。开启 Dropout 随机失活，开启 BatchNorm 的均值方差统计更新。\nmodel.eval()：评估/测试模式。关闭 Dropout，固定 BatchNorm。\n2. 标准流程对比\n训练和评估就像是“开卷考试”和“闭卷考试”的区别：\n步骤 训练阶段 (Training) 验证/测试阶段 (Validation) 模式设置 model.train() model.eval() 梯度计算 必须计算 关闭 (with torch.no_grad():) 梯度清零 optimizer.zero_grad() 不需要 前向传播 output = model(data) output = model(data) 计算损失 loss = criterion(output, label) loss = criterion(output, label) 反向传播 loss.backward() 跳过 更新权重 optimizer.step() 跳过 3. 训练与验证代码\nA. 训练逻辑封装\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def train_one_epoch(model, loader, optimizer, criterion, device): model.train() # 切换到训练模式 running_loss = 0.0 for data, label in loader: # 1. 移动数据到指定设备（GPU或CPU） data, label = data.to(device), label.to(device) # 2. 梯度清零：防止旧梯度累加影响当前步 optimizer.zero_grad() # 3. 前向传播 output = model(data) # 4. 计算损失 loss = criterion(output, label) # 5. 反向传播：计算每个参数的梯度 loss.backward() # 6. 更新权重：根据梯度调整螺丝钉 optimizer.step() # 统计损失 (loss.item() 提取标量值，防止显存爆炸) running_loss += loss.item() * data.size(0) return running_loss / len(loader.dataset) B. 验证逻辑封装\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def validate(model, loader, criterion, device): model.eval() # 切换到评估模式 val_loss = 0.0 correct = 0 # 关键：强制关闭梯度计算，节省大量内存和计算资源 with torch.no_grad(): for data, label in loader: data, label = data.to(device), label.to(device) output = model(data) loss = criterion(output, label) val_loss += loss.item() * data.size(0) # 计算准确率：获取概率最大的类别索引 preds = output.argmax(dim=1) correct += (preds == label).sum().item() avg_loss = val_loss / len(loader.dataset) accuracy = correct / len(loader.dataset) return avg_loss, accuracy 4. 模型效果的“体检报告”：Metrics\n除了简单的 Loss 和 Accuracy，我们通常使用 sklearn 来生成更详细的报告（精确率、召回率、F1值）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from sklearn.metrics import classification_report def get_full_report(model, loader, device, class_names): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for data, label in loader: data = data.to(device) output = model(data) preds = output.argmax(dim=1) # 收集结果，注意要转回 CPU 并转为 numpy all_preds.extend(preds.cpu().numpy()) all_labels.extend(label.numpy()) # 生成分类报告 print(classification_report(all_labels, all_preds, target_names=class_names)) 💡 避坑与进阶 Tips\nloss.item() 的妙用：在累加 Loss 时，一定要用 .item() 提取数值。如果直接累加 loss 张量，由于它带着计算图，会导致显存逐渐堆积直至 OOM（内存溢出）。\n.to(device) vs .cuda()：推荐使用 .to(device)。你可以在代码开头写一句 device = torch.device(\u0026quot;cuda\u0026quot; if torch.cuda.is_available() else \u0026quot;cpu\u0026quot;)，这样代码在有无 GPU 的机器上都能跑。\ntorch.no_grad()：在验证环节千万别漏了它！它不仅能提速，还能防止验证集的数据“污染”了模型的梯度，保证评估的客观性。\n6.7 可视化 (Visualization) 在 PyTorch 的世界里，可视化工具主要分为基础绘图、实时监控和云端实验管理三大类。\n6.7.1 常用可视化工具介绍\n工具名称 类型 适用场景 核心特点 Matplotlib 基础绘图库 静态图表、实验总结 Python 绘图的鼻祖，最通用，适合把训练好的结果画成论文插图。 TensorBoard 实时监控 监控训练过程 原本是 TensorFlow 的工具，现在已成为 PyTorch 的官方标配。适合看实时 Loss 曲线。 Weights \u0026amp; Biases (W\u0026amp;B) 云端管理 团队协作、大规模实验 现代化的“炼丹记录仪”。自动同步实验数据到云端，支持多人协作。 Visdom 实时监控 轻量级监控 Facebook 团队开发的工具，支持多种数据窗口展示，适合较老或轻量级的项目。 6.7.2 深度解析：必备双剑\n1. Matplotlib：你的“多功能画板”\n它是你笔记里最常见的工具。无论是在 Jupyter Notebook 里看一张图片，还是对比两个损失函数的数学曲线，它都是首选。\n优点：不需要额外开启服务器，随写随画。\n局限性：难以处理实时动态数据，如果训练跑了好几天，你很难用它一直盯着曲线。Matplotlib 最适合在训练结束后，对收集到的数据进行统一汇总和对比。\n代码示例：对比训练与验证 Loss\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import matplotlib.pyplot as plt # 1. 模拟训练过程中记录的数据 epochs = range(1, 6) train_losses = [0.9, 0.7, 0.5, 0.3, 0.2] val_losses = [0.95, 0.8, 0.65, 0.5, 0.45] # 2. 创建画布 plt.figure(figsize=(8, 5)) # 3. 绘制曲线 plt.plot(epochs, train_losses, label=\u0026#39;Train Loss\u0026#39;, marker=\u0026#39;o\u0026#39;) # marker 添加点标记 plt.plot(epochs, val_losses, label=\u0026#39;Val Loss\u0026#39;, marker=\u0026#39;s\u0026#39;) # 4. 装饰图表 plt.title(\u0026#39;Training and Validation Loss Over Epochs\u0026#39;) plt.xlabel(\u0026#39;Epoch\u0026#39;) plt.ylabel(\u0026#39;Loss\u0026#39;) plt.legend() # 显示右上角的图例 plt.grid(True) # 显示网格线，方便对齐数值 # 5. 显示图像 plt.show() 关键说明：\n适用性：适合在本地环境或 Jupyter Notebook 中快速查看结果。\n操作模式：它是“非交互式”的。如果你想更新图像，必须重新运行代码生成新的图表。\n2. TensorBoard：你的“实时监视器”\n在 PyTorch 中，通过 from torch.utils.tensorboard import SummaryWriter 即可调用。\n核心逻辑：\n在代码中创建一个 Writer。\n每训练一步，用 add_scalar 把 Loss 或 Accuracy 写进日志文件。\n在终端输入 tensorboard --logdir=logs，打开浏览器就能看到跳动的曲线。\n优点：可以一边训一边看，还能查看网络结构图、直方图。TensorBoard 的精髓在于它能一边训练一边写日志，你只需要刷新浏览器就能看到最新的曲线。\n代码示例：在训练循环中集成 TensorBoard\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import torch from torch.utils.tensorboard import SummaryWriter # 1. 创建写入器，指定日志保存的目录 writer = SummaryWriter(\u0026#39;logs/fit_experiment_1\u0026#39;) # 模拟一个简单的训练循环 for epoch in range(100): # 模拟计算出的 Loss 和 Accuracy loss = 1.0 / (epoch + 1) acc = 1 - (1.0 / (epoch + 1)) # 2. 将数据写入日志文件 # 参数含义：(标签名, 数值, 当前步数/Epoch) writer.add_scalar(\u0026#39;Loss/train\u0026#39;, loss, epoch) writer.add_scalar(\u0026#39;Accuracy/train\u0026#39;, acc, epoch) # 甚至可以把模型结构图存进去（假设 model 已定义） # writer.add_graph(model, input_to_model) # 3. 训练结束，关闭写入器 writer.close() 如何查看：\n在你的项目目录下打开终端（Terminal）。\n输入命令：tensorboard --logdir=logs。\n点击终端给出的链接（通常是 http://localhost:6006/），在浏览器中查看动态曲线。\n关键说明：\n层级管理：在标签名中使用斜杠（如 Loss/train），TensorBoard 会自动帮你把同类指标归纳到同一个面板下。\n实时性：即便训练还在跑，你也可以随时打开网页看曲线的变化趋势。\n6.8 PyTorch 优化器 (PyTorch Optimizer) 深度学习模型的训练本质上是一个寻找最优解的过程。面对拥有数千万参数的复杂模型（如 ResNet-50），我们无法暴力穷举，而是依赖反向传播（BP）与优化器（Optimizer）来逐步逼近最优参数。\n优化器根据网络反向传播得到的梯度信息来更新参数，其核心目标是降低损失函数（Loss）的值，使模型输出不断接近真实标签。\n6.8.1 优化器的基石：torch.optim\nPyTorch 的 torch.optim 库提供了多种现成的优化算法（如 SGD, Adam, RMSprop, AdamW 等），它们全都继承自基类 Optimizer。\n优化器的三大核心属性：\ndefaults：存储优化器的默认超参数（如 lr 学习率、momentum 动量等）。\nstate：存储参数的缓存信息（如动量缓冲区 momentum_buffer），用于记录训练过程中的状态。\nparam_groups：管理参数组的列表。每一组都是一个字典，可以为不同的层设置不同的学习率或超参数。\n6.8.2 优化器的五大核心方法\n方法 作用 备注 zero_grad() 清空所管理参数的梯度 PyTorch 梯度会累加，每次更新前必须手动清零。 step() 执行一步梯度更新 根据当前梯度和算法逻辑修改参数数值。 add_param_group() 动态添加参数组 可用于微调模型或给新层设置特定优化策略。 state_dict() 获取当前状态字典 包含参数和超参数，用于模型保存。 load_state_dict() 加载状态字典 用于断点续训，恢复上次训练的完整状态。 6.8.3 现代实战：标准训练逻辑与技巧\n1. 基础训练循环\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import torch from torch import optim # 1. 实例化模型与优化器 model = MyNet() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(EPOCHS): for data, label in train_loader: # 梯度清零 optimizer.zero_grad() # 前向传播与计算 Loss output = model(data) loss = criterion(output, label) # 反向传播计算梯度 loss.backward() # 梯度更新 optimizer.step() 2. 进阶技巧：差异化学习率\n你可以为不同的网络层配置不同的优化强度，这在迁移学习中非常有用：\n1 2 3 4 5 # 给 fc 层设置默认学习率，给 layer4 的卷积层设置更高的学习率 optimizer = optim.SGD([ {\u0026#39;params\u0026#39;: net.fc.parameters()}, {\u0026#39;params\u0026#39;: net.layer4[0].conv1.parameters(), \u0026#39;lr\u0026#39;: 1e-2} ], lr=1e-5) 6.8.4 优化器参数说明\n1. 通用参数（几乎所有优化器都有）\n在深入具体的优化器之前，这两个参数是你的“必修课”：\nlr (Learning Rate, 学习率)：最重要的参数。决定了每一步走的距离。\n太大：模型会在最优解附近反复横跳，甚至直接“起飞”（梯度爆炸）。\n太小：模型走得太慢，可能还没走到终点训练就结束了。\nweight_decay (权重衰减)：正则化技术（通常对应 L2 正则）。\n作用：像给模型戴上“紧箍咒”，防止权重数值变得过大，从而有效缓解过拟合。 2. SGD (随机梯度下降)\nSGD 是最经典的优化器，它的参数非常像物理世界的运动。\nmomentum (动量)：模拟物体的惯性。原理：它会积累之前的运动方向。如果之前一直在下坡，它会越滚越快，帮助模型冲出“平坦区域”或“局部最小值”。通常设为 0.9。\nnesterov (牛顿动量)：布尔值。原理：开启后，它会先根据惯性往前“看一眼”，然后再计算梯度。这让它在弯道处更加灵敏，不容易冲出赛道。\n3. Adam (自适应矩估计)\nAdam 是目前的“万金油”，它的参数主要用于控制如何自动调整步长。\nbetas (平滑常数)：通常是一个元组 (0.9, 0.999)。\n$\\beta_1$ (0.9)：控制一阶矩（类似动量），决定了梯度的平滑程度。\n$\\beta_2$ (0.999)：控制二阶矩（梯度的平方），决定了它是如何针对每个参数调整步长的。通常不需要改动。\neps (Epsilon)：一个极小的数（默认 1e-8）。作用：防止在数学运算中除以 0。除非你遇到了数值不稳定的问题，否则永远别动它。\n4. AdamW (Adam 的改进版)\n核心区别：它是目前在 Transformer 和 现代视觉模型 中最推荐的优化器。它修改了 weight_decay 的作用方式，让权重衰减真正独立于梯度更新，能获得更好的泛化效果。 5. RMSprop (均方根传播)\n常用于循环神经网络（RNN）。\nalpha：平滑常数（默认 0.99）。类似于 Adam 中的 $\\beta_2$，决定了对近期梯度的“记忆深度”。 核心对比表\n优化器 建议学习率 (lr) 必调参数 形象比喻 SGD 1e-1 或 1e-2 momentum 像一个滚下山的铁球，靠惯性冲过小坑。 Adam 1e-3 或 3e-4 lr 像一个带导航的赛车，自动在弯道减速、直道加速。 AdamW 1e-3 weight_decay Adam 的进化版，不仅跑得快，还不容犯错。 💡 怎么调最有效？\n先用 Adam：如果你是新手，或者在测试一个新模型，先用 optim.Adam(lr=1e-3)。它通常能给你一个不错的结果。\n后期切 SGD：如果你在参加竞赛或追求极致的精度，可以在训练后期换成带 momentum=0.9 的 SGD，配合细致的学习率调整，往往能磨出更高的分数。\n大模型必选 AdamW：如果你在玩 ViT、ResNet 变体或 Transformer，请直接使用 AdamW。\n6.8.5 如何选择优化器？\n优化器的选择会显著影响模型的收敛速度和最终效果。\nSGD (随机梯度下降)：虽然收敛可能较慢，但通常能找到更优的泛化解，适合调优精细的模型。\nAdam：结合了动量和自适应学习率，收敛极快，是绝大多数任务的默认首选。\nAdamW：修复了 Adam 在权重衰减（Weight Decay）上的问题，在计算机视觉任务中表现优异。\n核心心得\n优化器的性能高度依赖于模型和数据。没有绝对最好的优化器，只有最合适的组合。通常建议从 Adam 开始快速验证，后期再考虑使用带动量的 SGD 进行极限精度的打磨。\n","date":"2026-05-02T13:28:58+08:00","permalink":"/p/pytorch/","title":"Pytorch"},{"content":"如果说 2017 年的《Attention Is All You Need》是现代自然语言处理的“创世神话”，那么到了 2024 年，当我们拆开 GPT-4、Llama 3、Qwen 等千亿参数巨兽的引擎盖时，你会发现：它们早就不长当年那个样子了。\n随着模型参数量从亿级（BERT）飙升到万亿级，原版 Transformer 架构在 “显存开销”、“长文本处理”和“深层训练稳定性”上面临着巨大的物理灾难。\n现代大模型的发展史，本质上是一场 “工程向算力妥协”的减法美学。这篇博客主要盘点：现在的 LLM，到底对 Transformer 做了哪些改进？\n第一篇：GPT 系列演化与“三阶段”开发范式 当 2022 年 11 月 ChatGPT 掀起全球大模型技术狂潮时，很多人惊叹于它的“横空出世”。但事实上，这是一场长达四年的技术演进的必然结果。\n从 2018 年初代 GPT-1 诞生开始，OpenAI 就坚定地在一条技术路线上持续迭代。这期间，模型能力上限的不断突破，得益于四个核心维度的进化：模型规模（Scaling Law）、训练语料、网络架构以及训练范式。\n这四大维度的突破，不仅构筑了 ChatGPT 的技术基石，更标志着大语言模型正式跨越了学术研究与实际应用之间的鸿沟。今天，我们就来系统梳理 GPT 家族的发展脉络，探寻大模型“智能涌现”背后的核心机制。\nGPT-1 概述 GPT-1（Generative Pre-trained Transformer-1）是 OpenAI 于 2018 年 6 月 发布的首个生成式预训练语言模型，其研究成果发表于论文《Improving Language Understanding by Generative Pre-Training》。\nGPT-1 系统化地验证并推广了“预训练（Pre-training）+ 微调（Fine-tuning）”的训练范式：模型先在大规模无标注语料上进行语言建模预训练，再利用少量有监督数据在特定任务上微调。该方法显著提升了模型的迁移能力，使单一预训练模型能够适配多种自然语言任务。\n架构 总体架构 GPT-1 采用了 Transformer Decoder-only 结构，也就是说只保留了 Transformer 模型的解码器（Decoder）部分，并在此基础上进行了适配性调整。\n具体结构如下图所示，整个模型可分为输入层、Transformer Block层与输出层三个部分：\n![[Pasted image 20260417195251.png]]\n输入层（Text \u0026amp; Position Embedding）\n将文本序列映射为词向量，并加入位置嵌入，用于表示词序信息。\nTransformer Block\n每个Transformer Block包含 Masked Multi-Head Self-Attention 和 Feed Forward Network 两个核心子层，并通过 LayerNorm 与残差连接保持稳定的梯度传播。\n输出层\nText Prediction用于在预训练阶段输出下一个词的概率分布；Task Classifier用于在微调阶段适配下游任务。\n架构创新 Decoder-only 结构\n从 Encoder-Decoder 架构简化为Decoder-only 结构，以适配自回归生成任务。\n可学习位置嵌入\n使用可学习位置嵌入，取代正弦位置编码，使模型能够在训练过程中自适应地学习词序关系。\n权重共享机制\n输入嵌入层与输出词表权重共享，减少参数量并提升训练稳定性。\n主要参数 参数项 数值 模型层数（Layers**）** 12 隐藏维度（Hidden Size**）** 768 注意力头数（Attention Heads**）** 12 前馈层维度（FFN Size**）** 3072 参数总量 1.17 亿 训练 GPT-1 的训练采用了“预训练（Pre-training） +微调（Fine-tuning）”的两阶段范式。\n预训练阶段\n在预训练阶段（Pre-training），使用高质量英文语料 BooksCorpus（约 7,000 本小说，约 8 亿词）作为训练数据，以“给定前文预测下一个词（Next-Token Prediction）”为目标，在长篇连续文本中捕捉句法结构、词汇共现和语义依赖等特征，从而获得通用的语言建模能力。这一阶段让模型具备了理解与生成自然语言的基础能力。\n微调阶段\n在微调阶段（Fine-tuning），通过在模型顶部加入轻量的任务分类层（Task Classifier），并在少量标注数据上进行有监督训练，模型能够快速适应不同任务场景。结果表明，微调后的 GPT-1 在多个 NLP 任务上显著优于从零训练的传统模型，验证了“预训练—微调”范式的有效性与通用性。\nGPT-2 概述 GPT-2是 OpenAI 于 2019 年发布的第二代生成式预训练语言模型，相关研究发表于论文《Language Models are Unsupervised Multitask Learners》。\nGPT-2 延续了GPT-1的 Transformer Decoder-only 架构，但参数规模提升了10倍。\nGPT-2 首次证明了大型语言模型具备零样本（Zero-shot）任务泛化能力。模型无需任何下游任务微调，仅凭自然语言提示（Prompt）即可完成问答、翻译、摘要等任务，标志着语言模型开始具备跨任务的通用智能特征。\n此外，GPT-2 的实验系统揭示了显著的规模效应（Scaling Effect）：随着模型参数与数据规模的增长，模型性能在多种任务上持续提升。这一发现奠定了后续超大规模模型（如 GPT-3 与 ChatGPT）的发展基础。\n![[Pasted image 20260417201858.png]]\n架构 总体架构 GPT-2 延续了 GPT-1 的 Transformer Decoder-only 架构。\n架构创新 规模扩展\n参数量相比 GPT-1 提升约十倍，最大模型达到 15.42 亿参数\nLayerNorm\n采用Pre-LayerNorm，缓解大模型训练中梯度不稳定问题。\n![[Pasted image 20260417202117.png]]\n主要参数 参数项 数值 模型层数（Layers**）** 48 隐藏维度（Hidden Size**）** 1600 注意力头数（Attention Heads**）** 25 前馈层维度（FFN Size**）** 6400 参数总量 15.42亿 训练 GPT-2 的训练仍采用 自回归语言建模（Autoregressive Language Modeling） 目标，即在给定前文的条件下预测下一个词（Next Token Prediction），以学习文本的语法与语义规律。\n模型使用了 OpenAI 构建的 WebText 数据集，规模约 40GB，包含约 800 万篇网页内容，涵盖新闻、小说、科技、娱乐、社交等多个领域，为模型提供了更广泛的语言风格与知识来源。\nGPT-3 概述 GPT-3是 OpenAI 于 2020 年 5 月发布的第三代生成式预训练语言模型，研究成果发表于论文《Language Models are Few-Shot Learners》。\nGPT-3 延续了 Transformer Decoder-only 架构，但参数规模较 GPT-2 提升约百倍，达到了1750亿。\nGPT-3 的核心创新在于首次系统提出并验证了“上下文学习（In-Context Learning）” 的概念：模型在使用阶段无需额外训练，仅依靠输入文本中的任务描述及少量示例即可理解并完成任务，如下图所示。\n![[Pasted image 20260417202508.png]]\nGPT-3 的成功展示了大规模预训练模型在能力上的显著扩展，为后续更高层次的语言智能研究奠定了重要基础。\n架构 总体架构 GPT-3 延续了 GPT-1 的 Transformer Decoder-only 架构。\n架构创新 架构方面，与 GPT-2 相比，GPT-3 的主要区别在于在各个 Transformer Block 中交替使用稠密注意力（Dense Attention）和局部带状稀疏注意力（Locally Banded Sparse Attention）模式。\n局部带状稀疏注意力是一种仅在局部窗口内计算注意力权重的稀疏化机制，能够显著降低时间和空间复杂度。通过稠密与稀疏注意力的交替使用，GPT-3 能够兼顾全局信息和计算效率。\n主要参数 参数项 数值 模型层数（Layers**）** 96 隐藏维度（Hidden Size**）** 12288 注意力头数（Attention Heads**）** 96 前馈层维度（FFN Size**）** 49152 参数总量 1750 亿 训练 GPT-3 依旧采用 自回归语言建模（Autoregressive Language Modeling） 方式进行训练。\n训练数据约 570GB，包含约3000亿token，覆盖新闻、百科、文学与学术等领域。\n模型在数千块 NVIDIA V100 GPU 上进行分布式并行训练，耗时数周完成。\nInstructGPT 概述 InstructGPT 是 OpenAI 于 2022 年初推出的生成式预训练语言模型，是 GPT-3 的改进版本，也是后续 ChatGPT 的直接前身。其研究成果发表于论文《Training language models to follow instructions with human feedback》。\nGPT-3 在自然语言生成方面取得了突破性进展，能够通过精心设计的提示词（Prompt）完成多种任务，但其生成结果并非总能令人满意，常常与用户的真实需求存在偏差，甚至会产生一些不真实、有害或无用的内容。\n其根本原因在于，GPT-3 的训练目标仅是在大规模互联网文本上预测下一个单词，模型学习到的是语言的统计规律，而非如何理解和执行人类指令。换言之，模型尚未与人类意图保持一致（Alignment，对齐）。\n为解决这一问题，InstructGPT 在 GPT-3 基础上引入指令对齐训练，从而使模型能够更准确地理解并执行人类指令。\n架构 总体架构 InstructGPT延续了GPT-3的架构。\n架构创新 InstructGPT并未在网络结构上进行新的改动。其主要创新点不在于模型设计，而在于训练范式的变革。\n主要参数 参数项 数值 模型层数（Layers**）** 96 隐藏维度（Hidden Size**）** 12288 注意力头数（Attention Heads**）** 96 前馈层维度（FFN Size**）** 49152 参数总量 1750 亿 训练 InstructGPT 的训练以GPT-3的预训练语言模型为基础，进一步采用了三阶段训练范式，具体步骤如下：\n监督微调（Supervised Fine-tuning, SFT）\n在第一阶段，OpenAI 收集了由人工标注者撰写的高质量 “指令-回答” 样本，组成训练数据集。这些样本覆盖常见的用户指令及对应的理想响应，具有较高的语言质量。\n在训练过程中，模型在这些“指令-回答”对上进行监督微调，训练目标仍为“给定前文预测下一个词”（Next Token Prediction）。\n![[Pasted image 20260417203834.png]]\n通过该阶段，模型初步具备了理解并执行自然语言指令的能力，能够较好地完成指令驱动的任务响应。\n奖励模型（Reward Model, RM）\n第二阶段构建奖励模型，用于后续强化学习过程中的反馈评估器。\n操作流程如下：\n使用经过 SFT 微调后的模型，针对同一条指令生成多个候选回答；\n人工标注者根据回答的质量、相关性、礼貌性、有用性等维度，对这些回答进行排序；\n利用排序结果训练一个奖励模型，使其能够为任意给定回答输出一个偏好评分。奖励模型的核心目标是模仿人类的偏好判断，为语言模型的输出提供方向性反馈。\n强化学习（Reinforcement Learning from Human Feedback, RLHF）\n这一阶段的核心目标是：通过人类偏好引导模型输出更加符合人类意图的回答。它以第二阶段训练好的奖励模型为评分工具，使用强化学习算法（PPO），对第一步经过SFT微调的语言模型进行进一步优化。\n具体步骤如下：\n指令输入\n从预构建的指令数据集中选取一条指令，作为模型输入。\n模型生成回答\n使用经过SFT微调的模型对指令生成回答。\n奖励模型评分\n使用奖励模型对生成的回答进行打分。\n强化学习优化\n模型根据奖励模型打分的结果，调整自身参数，使其更倾向于生成高分回答。优化过程中使用了强化学习算法 PPO（Proximal Policy Optimization），目标是最大化奖励模型的评分。\n经过这一阶段的训练，模型能够更准确地理解人类指令，并生成更符合人类偏好、更加真实、有用且安全的回答，从而显著提升了语言模型在实际应用中的表现能力。\nChatGPT 概述 ChatGPT 是 OpenAI 于 2022 年 11 月发布的一种具备对话能力的大型语言模型。它支持连续的自然语言交互，能够回答后续问题、承认错误、质疑用户前提、拒绝不当请求，在用户体验与安全性方面取得显著改进。\n架构 总体架构 ChatGPT 是与 InstructGPT 同一技术路线下的兄弟模型，架构设计与 InstructGPT 类似，但具体细节未公开。\n架构创新 InstructGPT并未在网络结构上进行新的改动。其主要创新点不在于模型设计，而在于训练范式的变革。\n训练 ChatGPT 的训练方法采用与 InstructGPT 相同的人类反馈强化学习（RLHF）策略，其核心区别在于数据集的设计与处理方式。\nChatGPT 使用了特别构建的对话格式数据集。这些数据由标注人员通过模拟用户与 AI 助手的对话生成，内容更加贴近真实交互场景。相比 InstructGPT 所使用的“指令-单轮回答”数据，ChatGPT 所采用的数据具有多轮对话结构，强调上下文保持与连续问答能力。\n![[Pasted image 20260417204644.png]]\n此外，原有 InstructGPT 的数据也被统一转换为对话格式，与新收集的数据集成混合使用，以增强模型的对话泛化能力。整体数据处理流程重点在于适配对话场景，提升模型的多轮交互表现。\n总结 GPT 系列的发展推动了现代大语言模型训练范式的逐步成熟，形成了以 “预训练—监督微调—对齐” 为核心的三阶段开发框架。如下：\n预训练（Pre-training）\n基于超大规模无标注语料进行自监督学习，使模型获得通用语言建模能力、广泛的世界知识以及基本的推理与泛化能力。\n监督微调（Supervised Fine-tuning, SFT）\n利用人工构建的指令—响应示例或高质量对话数据对模型进行进一步训练，使其能够更好地理解指令，并输出更加规范、稳定且贴合任务需求的内容。\n对齐（Alignment）\n通过引入人类偏好、行为规范、安全约束与价值观等因素，使模型的行为更符合用户期望。对齐方式包括RLHF（奖励模型 + 强化学习）以及DPO、ORPO、KTO 等无需强化学习的偏好优化方法。对齐阶段的目标是让模型在真实应用场景中表现得更有帮助、更安全、更可靠。\n这一“三阶段”开发范式在实践中得到广泛验证，已成为业界主流的大语言模型训练框架。\n第二篇：现代 LLM 的底层组件深度演进 LLM架构 大型语言模型（LLM）大多基于 Transformer 架构构建，其中最常见的是 Decoder-only 架构。这一架构最早由 GPT 系列采用，并逐渐成为当前大多数主流模型的标准设计。LLM 的核心能力——理解语言、建模上下文、生成文本，都源于 Transformer 的模块化结构与高度可扩展性。\n从整体上看，一个典型的 LLM 可以分为 输入层、Transformer Block 堆叠层和输出层 三个部分，如下所述。\n输入层 输入层负责将原始文本序列转化为模型可处理的向量表示。文本首先经过分词处理，每个 token 会被映射为对应的词向量（Token Embedding），形成模型的基础语义空间。为了使模型能够感知序列顺序，还需要加入位置编码（Position Encoding）。输入层的作用是为后续 Transformer Block 提供初始的语义与位置信息。\nTransformer Block Transformer Block 是大型语言模型的核心计算单元，通常由几十到上百层堆叠而成。每个 Block 内部主要包含自注意力机制和前馈网络两部分：\n自注意力机制使模型能够在处理当前 token 时参考序列中其他位置的语义信息，从而捕捉长距离依赖关系； 前馈网络则对注意力输出进行非线性变换，提高模型的表达能力。为了确保深层结构的训练稳定性，Transformer Block 内部普遍采用残差连接与归一化技术，使梯度能够顺畅传播。大量堆叠的 Block 共同构成了 LLM 的主体结构，是模型理解语言和推理能力的来源。 输出层 输出层用于将 Transformer Block 最终生成的隐含向量映射为词表中的概率分布，从而执行下一个 token 的预测。\n典型做法是使用一个线性投影层将向量映射到词表维度，并通过 Softmax 得到每个 token 的概率。现代模型通常采用输入词向量与输出投影层权重共享的方式，以减少参数规模并提升训练效果。输出层的结果被用于自回归生成，每次输出一个 token 并将其作为下一步的输入，使模型能够逐步生成连贯自然的文本。\nLLM架构演进 概述 自 ChatGPT 问世以来，各类大语言模型不断涌现，但其主体架构仍然基于 Transformer 的解码器，整体结构并未发生革命性变化。\n模型真正的演进主要体现在内部组件层面，包括注意力机制、前馈网络、归一化与残差结构以及位置编码等模块的持续改进。\n下面介绍各个组件的改进方向。\nAttention 概述 注意力机制（Attention）是 Transformer 中最核心的模块，用于刻画序列中不同位置之间的依赖关系，决定模型在生成下一个 token 时应如何利用上下文。其能力直接影响模型的语义理解、长程依赖建模与文本生成质量。\n随着模型规模不断扩大、推理序列越来越长，传统的多头注意力的问题逐渐暴露出来。为了解决这些问题，业界提出了一系列改进结构，在保持模型能力的同时显著提升了推理效率。\nMHA（Multi-Head Attention） MHA是Transformer最初采用的注意力机制，其通过将输入分别映射为多组 Query、Key、Value，并在多个注意力头上并行计算注意力分布，使模型能够从不同子空间捕捉序列关系，从而提升表达能力。\n尽管 MHA 在早期模型中表现良好，但随着模型规模不断扩大，其结构逐渐暴露出明显的工程瓶颈。要理解 MHA 的限制，需要先明确推理阶段的一个关键概念——KV Cache。\n在自回归生成过程中，模型需要逐 token 地生成输出。对于每一个新 token，模型都会与所有历史 token 进行注意力计算。如果每一步都从头计算历史序列的 Key 和 Value，这将造成巨大的重复计算开销，如下图所示：\n![[Pasted image 20260418221241.png]]\n为了避免这种重复计算，模型都会在推理阶段将历史 token 的 Key 和 Value 缓存下来，供后续步骤直接使用。这一机制就是 KV Cache。通过缓存 K/V，模型在生成下一个 token 时只需计算当前 token 的 Q、K、V，再与缓存的 K/V 做一次注意力匹配即可，大幅减少了不必要的计算量，如下图所示：\n![[Pasted image 20260418221358.png]]\nKV Cache 的引入，虽然减少了重复计算，但需要持续保存过往 token 的 Key 和 Value。随着序列变长、模型层数增加，缓存量本身就会不断累积。并且在多头注意力（MHA）中，每个注意力头都会独立生成并存储一组 Key 和 Value，使得缓存规模在原有基础上进一步放大。这也构成了 MHA 在大模型推理中的主要问题。\nMQA（Multi-Query Attention） MQA（Multi-Query Attention，多查询注意力）是一种优化注意力机制的方案，旨在减少推理阶段 KV Cache 的存储与内存带宽开销。由Google于 2019 年 11 月发表在论文《Fast Transformer Decoding: One Write-Head is All You Need》。\nMQA 的核心思想是让多个注意力头共享同一套 Key 和 Value，而不是像传统 MHA 那样为每个头分别维护独立的 K/V。如下图所示：\n![[Pasted image 20260418221707.png]]\n这种共享方式大幅减少了需要缓存和读取的 K/V 张量量级，显著降低存储需求与内存带宽压力，从而大幅提升了推理速度。\n但这种共享机制会略微削弱注意力头的表达能力，使其精度略逊于传统 MHA，但整体损失非常小。 总体来看，MQA 以极小的精度代价换来了巨大的推理效率提升，是现代大模型推理加速的重要基础结构之一。\nGQA（Group-Query Attention） GQA（Group-Query Attention，分组查询注意力）是在 MQA 基础上的进一步改进方案，由 Google 在论文 《GQA: Training Generalized Multi-Query Transformer Layers》中提出。该方法旨在在提升推理效率的同时，弥补 MQA 由于所有注意力头共享同一套 K/V 而造成的表达能力下降问题。\nGQA 的核心思想是：将注意力头划分为多个组（Group），每组内部的多个 Query 共享同一套 Key 和 Value，而不同组之间则使用独立的 K/V，如下图所示：\n![[Pasted image 20260418222044.png]]\n这种结构本质上在 MHA 与 MQA 之间取得了折中：\n相比 MHA：不再为每个头维护独立 K/V，从而大幅减少 KV Cache 的存储需求与内存带宽压力；\n相比 MQA：不再所有头共享唯一 K/V，而是分组共享，使注意力头间仍能保持更高的多样性和表达能力。\n因此，GQA 在 推理效率 和 表达能力 之间实现了更优平衡。实际应用中，它比 MQA 能更好地处理复杂语义关系，同时仍保持较高的推理速度。\n总体来看，GQA 在仅增加极小计算与存储成本的前提下，显著增强了模型能力，因此被LLaMA、Qwen 等主流大模型所采用，在长序列推理和高并发场景中表现极为出色。\nFeed Forward Network 概述 前馈网络（FFN）是 Transformer Block 中继注意力之后的第二大核心组件，用于对 token 表示进行独立的非线性变换。其典型的“升维 → 激活 → 降维”结构，使模型能够在每个位置上学习高维特征，是 Transformer 表达能力的重要来源。\nFFN 的演进主要集中在激活函数和整体结构形式两个方向。\n激活函数 激活函数为 FFN 引入非线性，是提升模型表达能力与稳定性的关键模块。随着模型规模不断扩大，激活函数在设计上经历了从简单线性截断到平滑非线性，再到具备门控能力的结构化形式的演进。\nReLU\nReLU（Rectified Linear Unit）是深度学习中最早广泛应用的激活函数之一，其定义非常简单：\n$ReLU(x) = max(0, x)$ 其函数曲线如下图所示：\n![[Pasted image 20260418225155.png]]\nReLU 在正区间保持恒定梯度，使梯度能够高效传播，大大缓解了早期网络中的梯度消失问题，因而在浅层或中等规模神经网络中表现优异。然而，ReLU 对负区间进行“硬截断”，会导致梯度完全为零，从而引发“死亡神经元”现象，即神经元在训练过程中一旦进入负区间可能永久失活；此外，其非线性较弱，在深层 Transformer 中无法提供足够的梯度平滑性与稳定性，容易造成训练不稳定。\n随着模型深度和规模的大幅提升，ReLU 已难以满足大模型的稳定性要求，因此在现代大型语言模型中已经基本退出主流。\nGELU\nGELU（Gaussian Error Linear Unit）是早期Transformer预训练模型（例如Bert、GPT-2/3）中曾广泛采用的平滑非线性激活函数。\nGELU的核心思想是：不再进行硬性截断，而是通过一种 随输入大小平滑变化的缩放系数 来柔性控制激活强度，使得输入在负区间、中间区间、正区间分别呈现自然的衰减、过渡与放大。\n其数学定义为：\n$\\text{GELU}(x) = x \\cdot \\Phi(x)$ 其中 $\\Phi(x)$ 表示标准正态分布的累积分布函数。\n随着 $x$ 增大，$\\Phi(x)$ 趋向 1，输出趋近于 $x$；随着 $x$ 减小，$\\Phi(x)$ 趋向 0，输出则逐渐衰减为 0；对于接近 0 的输入，函数会在 0 与 $x$ 之间进行平滑过渡，如下图所示：\n![[Pasted image 20260418230227.png]] 相比 ReLU，GELU 在全域连续可导，能在深层网络中提供更稳定的梯度传播，且不会出现 “神经元死亡”问题，因此曾在中大型 Transformer 中一度成为主流。\nSiLU\nSiLU（Sigmoid Linear Unit）也是一种连续可导的激活函数，其基本思想是使用 sigmoid 函数对线性输入进行加权，从而得到一种柔和的非线性变换。\n其数学定义如下：\n$\\text{SiLU}(x) = x \\cdot \\sigma(x)$ 当 较大时， 趋近于 1，输出接近 ；当 较小时， 接近于 0，输入会被有效抑制；在接近 0 的区域，函数呈现平滑的过渡，而不是直接截断为零，如下图所示：\n![[Pasted image 20260418231502.png]]\n得益于这种连续可导的结构，SiLU 在深层网络训练中同样具有良好的优化稳定性，并在多个任务中表现出与 GELU 相近的经验性能。\nSwish\nSwish 可视为 SiLU 的一般化形式，其数学定义为：\n$\\text{Swish}(x) = x \\cdot \\sigma\\left(\\beta x\\right)$ 其中 $\\beta$ 可以是固定或可学习参数。当 $\\beta = 1$ 时，Swish 与 SiLU 完全等价。尽管原论文提出让 可学习，但实际收益有限，并可能带来训练不稳定性，因此主流实现通常将 $\\beta$ 固定为 1。\n基于这一等价关系，在工程实践中 Swish 与 SiLU 往往混用，不作严格区分。\nGLU及其变体\nGLU（Gated Linear Unit）及其一系列变体是当前大型语言模型（LLM）中最常用的 FFN 激活结构。与传统 ReLU、GELU 等单路激活不同，GLU 类结构采用 “主分支 × 门控分支” 的双分支设计，通过引入门控机制，使模型能够对信息流进行更加细致的筛选与调控。如下图所示：\n![[Pasted image 20260421184411.png]]\n门控结构的关键区别在于 门控分支所采用的门控函数。不同的函数会导致门控行为、梯度特性和最终模型性能的差异，因此形成了多种 GLU 变体。常见的门控结构包括\nGLU：使用 Sigmoid 作为门控函数\nGEGLU：使用GELU作为门控函数\nSwiGLU：使用SiLU作为门控函数\n在众多变体中，SwiGLU在性能与训练稳定性方面表现最优，因此成为当前主流 LLM（LLaMA、Qwen、DeepSeek 等）的默认门控结构。\nMoE 概述\nMoE（Mixture of Experts，混合专家模型）是在传统 FeedForward 模块基础上的一种结构扩展。其核心思想是：使用多个并行的 FeedForward 专家替代单一的 FeedForward 层，并通过 Router（路由器）根据输入 Token 的特征选择其中少量最合适的专家参与计算。这样一来，不同类型的输入能够由擅长处理该类模式的专家负责，从而显著增强模型的表达能力与适应性。MoE 已成为当前主流大语言模型（LLM）中广泛采用的、用于提升性能与效率的关键结构之一。\n![[Pasted image 20260421185422.png]]\n工作流程\nMoE的工作流程如下图所示：\n![[Pasted image 20260421191253.png]]\n具体步骤如下：\n路由得分计算\nRouter 接收 Token 的隐藏表示，并通过线性映射为所有专家生成一组得分。\n选择 Top-k 专家\n从得分中选取得分最高的 k 个专家，其余专家在本次计算中不参与处理。\n计算路由权重\n对被选中的专家得分执行 softmax，得到归一化权重，用于表示各专家在最终输出中的贡献比例。\n专家执行前向计算\n被激活的专家分别对输入 Token 进行独立计算，生成各自的输出。\n加权合并输出\n将所有激活专家的输出按照路由权重加权求和，得到 Token 在 MoE 层的最终表示。\n能力与优势\nMoE 结构通过引入稀疏激活机制，显著提升了模型的能力和效率，主要具备以下三个核心优势：\n高容量、低计算的效率结构\nMoE 模型通过稀疏激活机制，使每次前向传播仅有少数 个专家参与计算。这种设计在不增加实际计算量的前提下，大幅提升了模型的总参数量（模型容量），实现了高效的“高容量、低计算”结构。\n专家分工协作，提升泛化与适应性\n通过路由器（Router）机制的动态分配，不同的输入 Token 会被导向最适合处理它们的专家。这种机制促使专家自动形成功能分化，每个专家专注于学习特定的模式或数据子集，从而显著增强了模型的表达能力和泛化性能。\n天然适合大规模分布式扩展\nMoE 结构中的专家模块是相互独立的，这使得专家可以轻松地分布到大规模计算集群的不同设备上并行运行，极大地提升了模型的可扩展性（Scalability）和在大规模环境下的训练与推理效率。\n残差连接与归一化 概述 残差结构与归一化机制是 Transformer 稳定训练的基础。随着模型深度与规模增加，传统的结构逐渐暴露出梯度路径不稳定的问题。近年来，研究者通过一系列优化改进手段，使模型能够在更高深度、更大参数规模下保持稳定训练。\nRMSNorm RMSNorm（Root Mean Square Normalization，均方根归一化） 是对传统 LayerNorm 的一种简化变体。它去除了均值标准化操作，仅基于输入特征的均方根（RMS）进行缩放，从而在保持归一化效果的同时降低计算复杂度。实践表明，均值项在深层 Transformer 中对稳定性贡献有限，因此去除均值不会影响模型性能，反而带来更稳定、更高效的归一化形式。目前，RMSNorm 已成为主流 LLM 中最常用的归一化方式。\nRMSNorm 的核心公式如下：\n$\\text{RMSNorm}(x) = \\frac{x}{\\text{RMS}(x)} \\cdot \\gamma$ 其中：\n$\\text{RMS}(x) = \\sqrt{\\frac{1}{d}\\sum_{i=1}^{d} x_i^2 + \\varepsilon}$ $\\boldsymbol{x \\in \\mathbb{R}^d}$：输入向量 $ - $\\boldsymbol{\\gamma}$：可学习的缩放参数 $ - $\\boldsymbol{\\varepsilon}$：防止除零的微小数 归一化放置位置 在 Transformer 中，归一化层的放置位置会显著影响训练稳定性、梯度传播效率以及模型在更大深度和规模下的可扩展性。随着架构不断演进，研究者在实践中提出了多种归一化布置方式，通过在残差路径内部或模块内部调整 Norm 的位置，以获得更好的数值特性与训练表现。\n![[Pasted image 20260423150814.png]]\nPost Norm\nPost Norm 是 Transformer 中最初采用的归一化放置方式，其结构是在子层（如 Self-Attention 或 FFN）计算完成并与残差相加之后，再对结果进行归一化处理\n$\\boldsymbol{y = \\text{Norm}\\big(x + F(x)\\big)}$\nPost Norm 存在一个核心问题：它会削弱残差连接中用于稳定训练的那条恒等梯度通道，从而在深层网络中导致梯度回传不稳定。要理解这一点，需要先回顾残差连接的梯度传播机制。\n对于标准残差结构：\n$\\boldsymbol{y = x + F(x)}$ 反向传播过程中，梯度会沿着两条路径回传至输入 $x$：\n$\\frac{\\partial y}{\\partial x} = 1 + \\frac{\\partial F(x)}{\\partial x}$ 其中的“$1$”是一条不改变梯度方向和大小的恒等映射路径，为深层网络提供了稳定的“直通”梯度通道。\n然而在 Post Norm 中，反向传播形式变为：\n$\\frac{\\partial y}{\\partial x} = \\frac{\\partial \\text{Norm}(z)}{\\partial z} \\cdot \\left(1 + \\frac{\\partial F(x)}{\\partial x}\\right),\\quad z = x+F(x)$ 由于归一化操作会对输入进行缩放、标准化等处理，梯度在回流时也会被相应扰动，使原本应保持恒等的梯度路径不再保持为 1，从而削弱了残差连接提供的稳定梯度通道。\n在深层 Transformer 中，这种梯度扰动会层层累积，使训练更加不稳定。因此，Post Norm 在现代大规模模型中已基本不再使用。\nPre Norm\nPre Norm 是大多数大语言模型中采用的主流归一化方式。其结构是在进入子层（如 Self-Attention 或 FFN）之前先对输入进行归一化，然后再执行子模块计算并与残差相加：\n$\\boldsymbol{y = x + F\\big(\\text{Norm}(x)\\big)}$ 与 Post Norm 相比，Pre Norm 的设计充分保留了残差连接的关键特性：提供一条未受扰动的恒等梯度路径：\n$\\frac{\\partial y}{\\partial x} = 1 + \\frac{\\partial F\\big(\\text{Norm}(x)\\big)}{\\partial x}$ 正因如此，Pre Norm 在现代 LLM 中成为了事实上的标准结构，被广泛采用。\nPost Norm in Residual\nPost Norm in Residual 是近年来在部分模型中出现的一种归一化放置方式。与传统 Post Norm 的相比，它依然将归一化作用在子层输出之后，但归一化的位置被移动到残差路径内部：\n$\\boldsymbol{y = x + \\text{Norm}\\big(F(x)\\big)}$ 这种方式同样能够提供一条恒等梯度路径：$\\leftarrow$\n$\\frac{\\partial y}{\\partial x} = 1 + \\frac{\\partial \\text{Norm}\\big(F(x)\\big)}{\\partial x}$ 位置编码 概述 由于 Transformer 的自注意力机制（Self-Attention）并不包含序列顺序信息，模型无法仅依靠注意力计算来判断 token 在序列中的相对位置。因此，需要向输入中显式加入位置编码（Position Encoding），以补充序列的位置信息。\n随着模型规模扩大与任务需求多样化，位置编码的设计也在不断演进，以适应更长的上下文、更灵活的序列建模方式以及更稳定的训练特性。\n正余弦位置编码 概述\n正余弦位置编码（Sinusoidal Positional Encoding）是原始 Transformer 中采用的位置编码方式，它通过一组预定义的正弦与余弦函数为每个序列位置生成唯一向量：\n$\\boldsymbol{PE_{(pos,2i)} = \\sin\\left(\\frac{pos}{10000^{\\frac{2i}{d}}}\\right)}$ $\\boldsymbol{PE_{(pos,2i+1)} = \\cos\\left(\\frac{pos}{10000^{\\frac{2i}{d}}}\\right)}$ 其中：\n$\\boldsymbol{pos}$：Token 的绝对位置 $\\boldsymbol{i}$：维度索引 $\\boldsymbol{d}$：向量总维度 特点\n正余弦位置编码具有以下特点：\n无需训练\n编码由固定函数生成，不引入任何可学习参数，使用简单，不会随训练发生漂移。\n可外推\n由于编码基于数学函数，不依赖训练语料长度，模型可在推理阶段处理比训练时更长的序列。\n隐含相对位置信息\n正余弦位置编码的结构使其能够携带 token 之间的相对位置信息。\n正余弦位置编码的各维度是按频率成对排列的，每一对由同一频率对应的正弦值和余弦值组成。现在，我们分别从位置 和位置 的位置编码中，任意选取一对频率为 的维度进行观察。如下：\n$$\\boldsymbol{PE_\\omega(m) = \\begin{bmatrix} \\sin(\\omega m) \\\\ \\cos(\\omega m) \\end{bmatrix}}$$$$\\boldsymbol{PE_\\omega(n) = \\begin{bmatrix} \\sin(\\omega n) \\\\ \\cos(\\omega n) \\end{bmatrix}}$$\t在 Transformer 中，注意力权重由 q 向量与 k 向量的内积决定： $\\boldsymbol{\\text{AttentionScore}(m,n) = q_m^\\top k_n}$ 因此，位置编码之间的内积会直接参与注意力打分。基于这一点，我们考察上述两组位置编码分量的内积 $\\boldsymbol{PE_\\omega(m)^\\top PE_\\omega(n) = \\sin(\\omega m)\\sin(\\omega n) + \\cos(\\omega m)\\cos(\\omega n)}$ 利用三角恒等式可得： - 缺陷\n尽管正余弦位置编码具有良好的数学结构，其向量间的内积可直接反映相对位置关系，但在实际的 Transformer 注意力计算中，这种优势并不能完全有效发挥。$\\leftarrow$\n注意力计算中，注意力打分为：\n$\\boldsymbol{\\text{score}(m,n) = q_m^\\top k_n}$ 其中\n$q_m = W_Q\\big(e_m + \\text{PE}_m\\big),\\quad k_n = W_K\\big(e_n + \\text{PE}_n\\big)$\n代入后可得：\n$\\text{score}(m,n) = \\big(e_m + \\text{PE}_m\\big)^\\top W_Q^\\top W_K \\big(e_n + \\text{PE}_n\\big)$ 令 $\\boldsymbol{A = W_Q^\\top W_K}$，可将上式展开为：\n$$\\begin{aligned} \\text{score}(m,n) \u0026= e_m^\\top A e_n \\\\ \u0026+ e_m^\\top A \\text{PE}_n \\\\ \u0026+ \\text{PE}_m^\\top A e_n \\\\ \u0026+ \\text{PE}_m^\\top A \\text{PE}_n \\end{aligned}$$其中，仅有最后一项 $\\boldsymbol{\\text{PE}_m^\\top A \\text{PE}_n}$ 明确体现了位置编码之间的相互作用，因此也是唯一直接与相对位置相关的部分。但由于矩阵 $\\boldsymbol{A = W_Q^\\top W_K}$ 是可学习参数，会在训练中不断变化，原本正余弦编码所具备的几何结构难以稳定保持。换句话说，模型必须通过额外的学习来重新塑造或校正这种相对位置信息，而这种重建过程往往并不可靠，导致正余弦位置编码在理论上的优势无法在实践中完全体现出来，也影响了训练的稳定性和模型的泛化表现。\n可学习位置编码 概述\n可学习位置编码（Learned Positional Embedding）是另一种常见的位置编码方式，曾广泛应用于早期的Transformer预训练模型（Bert，GPT-1/2等）中。在这种方法中，模型为序列中的每个位置分配一个独立的可训练向量，这些向量在训练过程中与模型的其他参数一同更新，使模型能够从实际任务数据中自动学习位置表示。\n特点\n可学习位置编码的主要特点如下：\n灵活性高\n位置向量完全由数据驱动学习，不受固定数学函数约束，因此能够更贴合训练语料的分布特点。\n训练方式简单\n其优化方式与词向量一致，实现成本低，不需要额外的结构设计，便于直接集成到 Transformer 中。\n缺陷\n可学习位置编码的缺陷如下：\n缺乏长度外推能力\n可学习位置编码只在训练过的位置范围内定义，超出最大序列长度的位置没有对应的嵌入，导致模型无法在推理阶段处理更长的输入序列。\n参数量随序列长度线性增长\n位置向量数量与最大序列长度成正比，当需要支持长上下文时，参数开销迅速增大。\n旋转位置编码 第三篇：场景适配：Prompt、RAG 与微调 (LoRA/QLoRA) 面对实际业务需求，开发者该如何选择最省钱、最有效的模型适配路径。\n适配路线图：Prompt Engineering vs RAG vs Fine-tuning 的选型策略。\n微调实战理论：Base Model 与 Instruct Model 的核心差异及选择策略。\n参数高效微调 (PEFT)：深入 LoRA 与 QLoRA 的数学原理——如何在单张 24G 消费级显卡上微调大模型 。\n数据质量：Chat Template（如 ChatML 格式）在微调中的决定性作用与数据格式规范。\n第四篇：分布式训练与 ZeRO 显存管理 当单张显卡装不下千亿参数模型时，算力集群是如何“打配合”的。\n并行矩阵：数据并行、流水线并行（以及解决气泡问题的微批次策略）与张量并行。\nZeRO 深度解析：微软 ZeRO 系列优化器（Stage 1/2/3）如何通过分片存储彻底消除内存冗余 。\n硬件选型估算：不同微调策略下的 VRAM 显存占用硬核计算公式（含模型参数、梯度、优化器状态与激活值） 。\n第五篇：高性能推理 (vLLM) 与模型评测 模型训练完成后，如何实现每秒百词的高并发线上部署 。\nvLLM 魔法：PagedAttention 算法揭秘——像操作系统管理虚拟内存一样管理 KV Cache。\n推理实战：使用 vLLM 部署 OpenAI 风格的 API 接口并实现多轮对话调用。\n闭环评估：使用 EvalScope 框架进行模型压测（并发量、吞吐量、延迟）与综合能力评估。\n","date":"2026-04-16T17:10:00+08:00","permalink":"/p/modern-llm-architecture-evolution/","title":"从 Transformer 到现代 LLM：大模型架构的“减法”美学"},{"content":"Transformer 的出现 在 Transformer 出现之前，自然语言处理的发展时间线如下：\n阶段一：传统序列模型 ($RNN \\rightarrow LSTM \\rightarrow GRU$) 这一阶段主要是为了解决模型“如何记住时序信息”的问题。\n时序信息 如何理解一句话 要理解一句话的含义，应该是从头向后，先理解第一个词语，然后把前两个词语组合，理解这个组合，在把前三个词语组合，理解这个组合，以此类推；在理解句意时，每个字或词出现的时间顺序不同会带来不一样的含义，所以理解一句话不应该是把句子拆分成单个字来看。\n词袋模型 在早期的自然语言处理中，有一种方法叫“词袋模型”（Bag of Words）。它就像把一句话里的字全部拆散，扔进一个布袋子里摇匀。 在词袋模型看来：\n“我爱你” = [我, 爱, 你] “你爱我” = [你, 爱, 我] RNN的出现 因为袋子里的字完全一样，机器会觉得这两句话是一个意思！但这显然是荒谬的。传统序列模型（RNN）的发明，就是为了打破这个袋子，让机器明白“谁在前面、谁在后面”决定了句子的生死。\nRNN (循环神经网络): 最早的序列模型。 如何工作 RNN 会逐个读取句子中的词语，并在每一步结合当前词和前面的上下文信息，不断更新对句子的理解。通过这种机制，RNN 能够持续建模上下文，从而更准确地把握句子的整体语义。因此 RNN 曾是序列建模领域的主流模型，被广泛应用于各类 NLP 任务。\n基础结构\n实际结构： 在 PyTorch 的底层代码中，RNN 只有一个运算模块（也就是包含 $W, U$ 权重和 $\\tanh$ 的那个细胞），它以时间步（time step）为单位，依次处理输入序列中的每个 token。 怎么工作： 它本质上是一个 for 循环。不管句子有 10 个字还是 1000 个字，它们都必须排队，依次进入这唯一的一个模块被处理。 核心特征： 权重共享 (Weight Sharing)。从句首到句尾，机器始终在复用同一套参数矩阵来提取特征。 沿时间展开图 在每个时间步，RNN 接收当前 token 的向量 $x_t$ 和上一个时间步的隐藏状态 $h_{t-1}$（即隐藏层的输出），计算并生成新的隐藏状态 $h_t$，并将其传递到下一时间步。\n说明 上图详细展示了基础 RNN 的结构，但 RNN 还存在更复杂的结构形式。\n多层结构： 为了让模型捕捉更复杂的语言特征，可以将多个 RNN 层按层次堆叠起来，使不同层学习不同层次的语义信息。\n底层网络： 更容易捕捉局部模式（如词组、短语），底层的神经元直接接触最原始的输入数据（比如一个个词向量），因此它们的注意力主要集中在局部的、基础的、物理层面的特征上。 高层网络： 能学习更抽象的语义信息（如句子主题或语境），高层 RNN 接收的是底层已经初步“消化”过的特征序列。它们不再纠结于具体的字词长什么样，而是站在更高的视角，关注全局的、抽象的、深层含义。 注：多层 RNN 结构中，每一层的输出序列会作为下一层的输入序列，最底层 RNN 接收原始输入序列，顶层 RNN 的输出作为最终结果用于后续任务。\n双向结构： 基础的 RNN 在每个时间步只输出一个隐藏状态，该状态仅包含来自上文的信息，而无法利用当前词之后的下文。对于一些任务而言，这是一个明显的限制。比如在序列标注任务中，模型需要为每个 token 预测一个标签，如果只能参考前文信息，往往难以做出准确判断。\n⚠️ 痛点：\n梯度消失： 底层机制（梯度消失/爆炸）： 在训练 RNN 时，我们使用的是“沿时间反向传播算法”（BPTT）。因为状态是不断往后传递的，数学上这涉及到连乘效应。如果权重矩阵里的值小于 $1$，连乘几十次后，梯度就会趋近于 $0$（梯度消失，模型不再更新）；如果大于 $1$，又会趋近于无穷大（梯度爆炸，模型直接崩溃）。 信息传递路径太长： 在 RNN 中，句首单词和句尾单词之间的距离是 $\\mathcal{O}(N)$（$N$ 是序列长度）。这意味着两个词隔得越远，它们建立联系需要跨越的计算步骤就越多，信号衰减得就越厉害。 LSTM (长短期记忆网络) 按时间展开结构\n改进 为了缓解 RNN 梯度消失或者梯度爆炸的问题，Hochreiter 和 Schmidhuber 于 1997 年提出了长短期记忆网络（Long Short-Term Memory, LSTM），LSTM 在 RNN 的基础上引入了四个新的结构。\n记忆单元 (Cell State, $C_t$) 记忆单元负责在序列中长期保存关键信息。它相当于一条“信息通道”，在多个时间步之间直接传递信息。（记忆单元是缓解梯度消失和梯度爆炸问题的核心） 这是 LSTM 区别于普通 RNN 最伟大的发明。传统的 RNN 只有 $h_t$ 这一条路，记忆要在里面被反复运算（矩阵乘法），导致梯度消失。而 $C_t$ 是长期的、全局的记忆。 在这条直通车上，数据只发生极其简单的线性运算（乘法和加法）。\n遗忘门 (Forget Gate, $f_t$) 决定要从过去的记忆上擦除什么信息。 例如历史输入为：“小帅是一名程序员，他每天都加班；”，然后当前时间步的输入为“小美”，意味着当前的主语变为了“小美”，后续应该生成和“小美”相关的内容，所以此时记忆单元就应该忘记之前的主语信息“小帅”。遗忘门会根据上一个时间步的隐藏状态 $h_{t-1}$ 和当前时间步的输入 $x_t$，生成一个 $0$ 到 $1$ 之间的控制系数，然后与上一个时间步的记忆单元状态相乘，从而动态调整哪些信息应该被遗忘。\n计算过程： 把当前的输入 $x_t$ 和上一步的短期记忆 $h_{t-1}$ 放在一起看，然后通过一个 Sigmoid 函数，输出一个 $0$ 到 $1$ 之间的数字（比例）。 接近 $0$：代表“这部分老记忆没用了，彻底忘掉！”（比如主语从“他”变成了“她”，老的性别特征就被清空）。 接近 $1$：代表“这个信息很重要，继续保留” 公式： $$f_t = \\sigma(W_f \\cdot x_t + U_f \\cdot h_{t-1} + b_f)$$ 输入门 (Input Gate, $i_t$) 输入门控制要从当前时间步的输入向记忆单元存入多少新的信息。例如上述案例中，当前时间步的输入为“小美”，所以此时记忆单元就应该存入新的主语信息“小美”。当前时间步的信息由当前输入 $x_t$ 和上一个隐藏状态 $h_{t-1}$ 计算而成，同时输入门也由当前输入 $x_t$ 和上一个隐藏状态 $h_{t-1}$ 计算而成，然后新的信息和输入门相乘得到需要存入记忆单元的信息。\n计算过程： 生成备选新知识 ($\\tilde{C}_t$)： 使用 $\\tanh$ 函数，把当前的新词和语境，提炼成一份“候选记忆草稿”（数值在 $-1$ 到 $1$ 之间）。 决定写入比例 ($i_t$)： 使用 Sigmoid 函数，算出一个 $0$ 到 $1$ 之间的“写入阀门值”。它决定了刚才那份草稿里，有多少内容是真的有价值、值得被正式写进传送带的。 公式： 算阀门： $$i_t = \\sigma(W_i \\cdot x_t + U_i \\cdot h_{t-1} + b_i)$$ 写草稿： $$\\tilde{C}_t = \\tanh(W_c \\cdot x_t + U_c \\cdot h_{t-1} + b_c)$$ 老记忆被遗忘门乘法砍掉一部分，新记忆草稿被输入门过滤后加进来： $$C_t = (f_t \\ast C_{t-1}) + (i_t \\ast \\tilde{C}_t)$$ 输出门 (Output Gate, $o_t$) 输出门控制从记忆单元中读取多少信息作为当前时间步的隐藏状态进行输出。例如上述输入法智能提示案例中，记忆单元中存入新主语信息“小美”之后，当前时间步就应该从记忆单元中提取该主语信息，生成与“小美”相关的内容。输出门同样由当前时间步的输入 $x_t$ 和上一个时间步 $h_{t-1}$ 的隐藏状态计算而成，如下图所示。\n运作过程： 算过滤阀门 ($o_t$)： 同样通过 Sigmoid 函数，结合当前输入 $x_t$ 和上步状态 $h_{t-1}$，算出一个 $0$ 到 $1$ 的比例。这就相当于一张遮罩/滤网，决定了长期记忆里的哪些部分在当前这个时间步是需要暴露出来的。 生成最终输出 ($h_t$)： 把最新的细胞状态 $C_t$ 用 $\\tanh$ 压扁处理一下（确保数值稳定在 $-1$ 到 $1$ 之间），然后和刚才的滤网 $o_t$ 相乘。 算出来的这个 $h_t$，就是当前时间步的短期记忆。它有两个去处：一是传递给下一个时间步，二是往上送给外层的 Linear 层去预测当前的单词！ 公式： 算滤网： $$o_t = \\sigma(W_o \\cdot x_t + U_o \\cdot h_{t-1} + b_o)$$ 对外发言： $$h_t = o_t \\ast \\tanh(C_t)$$ ⛔ 缺点\n参数大爆炸： LSTM 新增了遗忘门、输入门、输出门、记忆单元四个结构。为了实现这四个模块，LSTM 内部设置了 4 套完全独立的权重矩阵（$W$ 和 $U$）。 如果隐藏层维度比较大，LSTM 的参数量会是普通 RNN 的 4 倍。这直接导致它在运行时极其消耗 GPU 显存。 GRU (门控循环单元): 改进 LSTM 的“精简版”。把 LSTM 的三个门简化成了两个门（重置门、更新门），取消了 LSTM 中独立的记忆单元，只保留隐藏状态，在保持效果差不多的前提下，大大减少了计算量，训练速度更快。\n按时序展开结构\n重置门 ($r_t$) 在面对当前的新单词时，决定过去的记忆有多少参考价值，帮模型生成一份新的记忆。\n案例： 假设模型正在阅读一篇小说。 场景 A（上下文连贯）： 上文是“小明走进厨房”，当前词 $x_t$ 是“拿起了”。此时，重置门评估后觉得：“前面的‘厨房’和现在‘拿起了’高度相关，可能是拿起了菜刀或苹果。”于是，重置门全开，让过去的记忆全部参与到新记忆的起草中。 场景 B（话题突转）： 上文是长篇大论的“量子力学原理解析”，当前词 $x_t$ 是“突然，”。此时，重置门发现话题画风突变，前面的物理学知识对理解接下来的故事毫无用处。于是，重置门果断关闭，把前面的记忆屏蔽掉（重置），只根据“突然，”这个词去起草接下来的语境。 公式： 在生成候选草稿（$\\tilde{h}t$）时，重置门 $r_t$ 悄悄乘在了过去的隐状态 $h{t-1}$ 上： $$\\tilde{h}_t = \\tanh(W \\cdot x_t + U \\cdot (r_t \\ast h_{t-1}))$$ 更新门 ($z_t$) 更新门会在计算当前时间步最终的隐藏状态 $h_t$ 时，分别作用在上一时刻的隐藏状态 $h_{t-1}$ 和当前新计算出的候选隐藏状态 $\\tilde{h}_t$，用于控制保留多少旧信息，以及引入多少新信息。\n⚠️ 痛点 尽管 GRU 极其精简高效，但他还是存在无法解决的问题。\nRNN 计算公式： $$h_t = \\tanh(W \\cdot x_t + U \\cdot h_{t-1} + b)$$ $$y_t = V \\cdot h_t + c$$产生一个输出 $y_t$，必须依赖一个当前的隐藏状态 $h_t$；而产生 $h_t$，又必须生吃进去一个当前的输入单词 $x_t$。 这就好比机器内部有两个完全咬合的齿轮，输入转一格，输出才跟着转一格。没有输入的推力，输出齿轮根本转不动；输入转了，输出也绝对不能停。这就是所谓的“时序强绑定”。 不管内部怎么优化，GRU 依然是一个标准的循环网络。标准 RNN 在处理任务时，要求 输入序列和输出序列必须是严格对齐的（长度相等，词序对应）。这在真实的机器翻译中会遇到问题，现实世界中的翻译很少能有输入输出长度相等，词序对应的。\n阶段二：框架的诞生 (Seq2Seq 架构, 2014年) 在此之前，RNN 系列模型很难处理“输入和输出长度不一致”的问题（比如机器翻译，中文的“我爱你”是3个字，英文的“I love you”也是3个单词，但“桌子上的苹果”和“The apple on the table”长度就不一样了）。 为了解决这个问题，Google 提出了 Seq2Seq (Sequence to Sequence) 架构，也叫 Encoder-Decoder (编码器-解码器) 架构。\n关键点 Seq2Seq 不是凭空造出来的新网络，它的内部使用的就是 RNN、LSTM 或 GRU。\n工作原理 * Encoder 负责把一整句话吃进去，压缩成一个固定长度的向量（也就是上下文向量 Context Vector）。\nDecoder 拿到这个向量，逐步生成目标序列。 模型结构\n编码器 - Encoder Encoder 底层通常就是一个标准的 RNN、LSTM 或 GRU。\n工作流程： 只听不说，浓缩精华。 它的任务是阅读一段长度为 $N$ 的输入序列，理解里面的所有词法、句法和逻辑关系，然后把它压缩成一个包含整句话含义的隐藏状态，当接受完最后一个词，并且看到代表句子结束的特殊符号 \u0026lt;EOS\u0026gt;（End of Sentence）时，它会输出最后一个隐藏状态——$h_{final}$，就是 上下文向量 (Context Vector, $C$)。 按时间步展开结构\n解码器 - Decoder 它同样是一个标准的 RNN、LSTM 或 GRU。但在参数上，它和 Encoder 是完全独立的（两人不共享权重，因为一个懂中文，一个懂英文）。\n工作流程： 只说不听，展开思想。 将编码器生成的上下文向量，在没有任何外界输入提示的情况下，“解压缩”成一种新的序列。 在生成开始时，循环神经网络以上下文向量作为初始隐藏状态，并接收一个特殊的起始标记 \u0026lt;sos\u0026gt;（start of sentence）作为第一个时间步的输入，用于预测第一个 token。 随后，在每一个时间步，模型都会根据前一时刻的隐藏状态和上一步生成的 token，预测当前的输出。这种“将前一步的输出作为下一步输入”的方式被称为自回归生成（Autoregressive Generation），它确保了生成结果的连贯性。 生成过程会持续进行，直到模型生成了一个特殊的结束标记 \u0026lt;eos\u0026gt;（end of sentence），表示句子生成完成。 按时间步展开结构\n模型的训练和推理 Seq2Seq 在训练（Training）和推理（Inference，也就是实际使用）这两个阶段，差别非常大。编码器（Encoder）在两个阶段的表现是一模一样的。真正的区别，全部集中在解码器（Decoder）接收输入的方式上。\n训练\n编码器： 编码器接收源语言序列“我喜欢你。”，通过嵌入层和循环神经网络（RNN / LSTM / GRU）的逐步处理，将整句编码为上下文向量。 解码器： 训练时目标是让模型快速收敛。如果让解码器像真实情况下那样“自回归”（把上一步的输出当成下一步的输入），假如第一步预测错了（把 I 预测成了 He），那么第二步就会拿着错误的 He 继续往下猜，导致后面全盘皆输。模型会在错误的道路上越走越远，根本学不到东西。 为了解决这个问题，引入了一个极其重要的训练技巧：Teacher Forcing（教师强制），如下图所示。 运作机制 就像一个严厉的老师握着学生的手写字。 第一步：输入 \u0026lt;SOS\u0026gt;，模型瞎猜了一个 He。 第二步：老师强行介入！ 老师不管上一步猜出了什么，老师直接把**标准答案（Ground Truth）**里的第一个词 I，硬塞给模型作为第二步的输入。 第三步：老师再次介入，把标准答案里的 like 塞给模型作为第三步的输入。\n优势 即使模型在中间某一步预测错了，错误也不会像滚雪球一样累积。模型始终在“正确的上下文”引导下学习，训练速度和稳定性大幅提升。\n损失计算 解码器每一步输出一个 token 的概率分布，通过交叉熵损失函数衡量模型对真实词的预测质量。训练过程中，每一个时间步都会产生一个损失值。该样本的总损失，就是所有时间步的损失值逐步累加的结果。\n推理\n运作机制： 这就是上一节详细讨论过的自回归（Autoregressive）。 第一步：输入 \u0026lt;SOS\u0026gt;，模型结合上下文向量，吐出 I。 第二步：模型只能依靠自己，老老实实把刚刚吐出的 I 拿过来，作为这一步的输入，推导出 like。 直到模型自己决定吐出 \u0026lt;EOS\u0026gt; 为止。 ⚠️ 痛点\n在上述 Seq2Seq 架构中，编码器会将整个源句压缩为一个固定长度的上下文向量，并将其作为解码器生成目标序列的唯一参考。这种“压缩再解压”的方式虽然结构简洁，但在实际任务中暴露出两个核心问题：\n**信息压缩困难，语义表达受限：**对于编码器而言，用一个定长向量去表达任意复杂的句子，是一项非常困难的任务。尤其在面对长句时，信息很容易在压缩过程中丢失，导致语义表达不完整。这种“信息瓶颈”限制了模型在处理长文本或复杂语义结构时的表现。 **缺乏动态感知，解码难以精准生成：**解码器始终只能基于同一个上下文向量进行生成。但在实际生成过程中，不同位置的目标词，往往依赖源句中不同的关键信息：生成主语时，可能更依赖源句的开头；生成谓语或宾语时，可能需要参考句中或句末内容。然而在固定表示下，解码器无法“有选择地关注”输入序列的不同部分，只能一视同仁地处理所有信息，从而降低了生成的准确性与灵活性。 阶段三：Transformer 的直接前身 (Seq2Seq + Attention, 2014-2015年) 早期的 Seq2Seq 有一个致命缺陷：信息瓶颈。Encoder 必须把几百个字的整篇文章压缩成一个固定大小的向量，这导致长句子的信息被严重丢失。 于是，Attention（注意力机制） 被发明出来了（最早由 Bahdanau 等人提出）：\n突破： Decoder 在生成每一个词的时候，不再依赖那个单一的压缩包，而是回头去看 Encoder 输入的所有词，并且自己决定当前应该把“注意力”集中在哪些词上。 这个时候的顶配序列模型是：以 LSTM/GRU 为底座的 Seq2Seq + Attention 机制。\n工作原理 注意力机制的核心思想，是解码器在生成目标序列的每一步时，动态地从编码器的各个时间步的隐藏状态中提取当前所需的信息，而不再只依赖一个固定的上下文向量。\n这一机制包含以下四个步骤：\n相关性计算： 在目标序列生成的每一步，解码器都会计算当前时间步的隐藏状态与编码器各个时间步输出之间的相关性。这些相关性衡量了源句中每个位置对当前生成内容的重要程度，从而决定模型应将多少注意力分配给不同的源位置。相关性的计算依赖于特定的函数，通常被称为注意力评分函数（attention scoring function）。 注意力权重计算： 得到所有源位置的注意力评分后，使用 Softmax 函数将其归一化为概率分布，作为注意力权重。得分越高的位置，其对应的权重越大，代表模型在当前生成中更关注该位置的信息。 上下文向量计算： 将所有编码器输出按照注意力权重进行加权求和，得到一个上下文向量。这个向量就表示当前时间步，模型从源句中提取出的关键信息。 解码信息融合： 在得到上下文向量后，解码器将其与当前时间步的隐藏状态进行拼接，以融合两者信息，最终通过线性变换和 Softmax，生成当前时间步目标词的概率分布。 ⚠️ 痛点\n尽管注意力机制极大地增强了 Seq2Seq 模型的建模能力，但由于其核心依然依赖于 RNN 结构，仍面临两个根本性问题：\n计算过程无法并行： RNN 的时间步之间存在强依赖，必须顺序执行，限制了训练效率和硬件资源的利用率。 长期依赖问题仍未根除： 模型需要跨多个时间步传递信息，对于超长序列，训练过程中容易出现梯度消失，难以有效建模长距离依赖关系。 阶段四：革命与颠覆 (Transformer, 2017年) 到了 2017 年，Google 发表了著名的论文《Attention Is All You Need》：\n保留了 Seq2Seq 的 Encoder-Decoder 架构。 但是，他们彻底抛弃了里面的 RNN/LSTM/GRU 细胞，全部用“自注意力机制 (Self-Attention)”和前馈神经网络来代替。 通过这种方式，Transformer 不仅显著提升了训练效率，也增强了模型对长距离依赖的建模能力。Transformer 的提出对自然语言处理产生了深远影响。\n整体结构 Transformer 在宏观上，完美继承了 Seq2Seq 模型的经典设计理念：“编码器-解码器（Encoder-Decoder）”架构。\n在这个架构中，两者的分工依然明确：\n编码器（Encoder）： 负责阅读、理解和表征输入的原文序列（提取核心思想）。 解码器（Decoder）： 负责拿着编码器提炼的核心思想，一步步生成目标序列（如翻译后的外语）。 Transformer 具备以下两个特点：\n层层堆叠 在传统的 RNN 时代，往往只用一层（或者很少的几层）网络，靠着在时间轴上不断循环来提取信息。而 Transformer 彻底抛弃了时间轴的循环，它选择在空间深度上疯狂堆砌。 它的编码器和解码器，都不再是单一的模块，而是分别由多个结构完全相同的“层（Layer）”垂直堆叠而成（在标准的 Transformer 原始论文中，包含了 6 个编码器层和 6 个解码器层）。\n为什么要堆叠这么多层？ 你可以把它想象成一个流水线车间。当“我爱你”这三个字同时进入第 1 层时，模型可能只看懂了词性（“我”是代词，“爱”是动词）。然后结果传给第 2 层，模型看懂了主谓宾结构；传到第 4 层，看懂了时态；传到第 6 层时，模型就已经提取出了极其深度的语义特征和情感逻辑。层数越深，模型对复杂语言现象的建模能力就越强。 “全量回顾”的自回归解码 这是 Transformer 在工程实现上与 RNN 最大的不同点！ 像 RNN 一样，Transformer 的解码器依然是自回归（Autoregressive）的——也就是说，它必须一个词一个词地往外吐，直到吐出结束标记 \u0026lt;eos\u0026gt; 为止。 但是它每一次生成新词时的输入条件：\nRNN 是怎么做的： RNN 是有“记忆（隐藏状态 $h_t$）”的。当它要生成第 4 个词时，它只需要输入第 3 个词，因为前 3 个词的信息已经浓缩在它的记忆里了。 Transformer 是怎么做的： Transformer 为了追求极致的并行计算速度，彻底切除了类似 RNN 的“记忆体”。 既然没有记忆，它怎么知道自己刚才说了什么？ 答案是全量输入：当它准备预测第 4 个词时，它必须把前面已经生成的第 1、2、3 个词，作为一整个序列同时重新输入进去！ 模型会对着这 3 个词进行一波疯狂的注意力计算，最后输出一个长度同样为 3 的预测序列。但我们在代码里，只取这个输出序列的“最后一个位置”的结果，作为我们想要的第 4 个词。 编码器 编码器由多个结构相同的编码器层（Encoder Layer）堆叠而成。\n每个 Encoder Layer 的主要任务都是对其输入序列进行上下文建模，使每个位置的表示都能融合来自整个序列的全局信息。每个 Encoder Layer 都包含两个子层（sublayer），分别是自注意力子层（Self-Attention Sublayer）和前馈神经网络子层（Feed-Forward Sublayer）。\nSelf-Attention 子层 用于捕捉序列中各位置之间的依赖关系，自注意力机制（Self-Attention）是 Transformer 编码器的核心结构之一，它的作用是在序列内部建立各位置之间的依赖关系，使模型能够为每个位置生成融合全局信息的表示。\n之所以被称为“自”注意力，是因为模型在计算每个位置的表示时，所参考的信息全部来自同一个输入序列本身，而不是来自另一个序列。\n自注意力计算过程\n生成 Query、Key、Value 向量 自注意力机制的第一步，是将输入序列中的每个位置表示映射为三个不同的向量，分别是查询（Query）、键（Key）和值（Value）。 这些向量的作用如下：\nQuery： 表示当前词的用于发起注意力匹配的向量； Key： 表示序列中每个位置的内容标识，用于与 Query 进行匹配； Value： 表示该位置携带的信息，用于加权汇总得到新的表示。 自注意力的核心思想是：每个位置用自身的 Query 向量，与整个序列中所有位置的 Key 向量进行相关性计算，从而得到注意力权重，并据此对对应的 Value 向量加权汇总，形成新的表示。 三个向量的计算公式如下： $$Q = X \\cdot W^Q$$ $$K = X \\cdot W^K$$ $$V = X \\cdot W^V$$ 其中 $W^Q, W^K, W^V$ 均为可学习的参数矩阵。\n计算位置相关性 完成 Query、Key、Value 向量的生成后，模型会使用每个位置的 Query 向量与所有位置的 Key 向量进行相关性评分。 评分函数采用向量点积形式。由于在高维空间中，点积的数值可能过大，会影响 Softmax 的稳定性，因此在实际计算中对结果进行了缩放。最终的评分函数为： $$score(i,j) = \\frac{q_i \\cdot k_j}{\\sqrt{d_k}}$$ 其中 $d_k$ 是 Key 向量的维度，用于缩放点积的幅度。这个分数越大，表示第 $i$ 个位置越应该关注第 $j$ 个位置的信息。 对于整个序列，可以通过矩阵运算一次性计算所有位置之间的评分，计算公式如下图所示：\n计算注意力权重 在得到每个位置与所有位置之间的相关性评分后，模型会使用 Softmax 函数进行归一化，确保每个位置对所有位置的关注程度之和为 1，从而形成一个有效的加权分布。 对于整个序列，模型要做的是对之前得到的注意力评分矩阵的每一行进行 Softmax 归一化。\n加权汇总生成输出 最后，模型会根据注意力权重对所有位置的 Value 向量进行加权求和，得到每个位置融合全局信息后的新表示。 对于整个序列，同样可以通过矩阵运算一次性计算所有位置的输出，如下图所示：\n综上所述，可得整个自注意力机制的完整的计算公式如下\n对应原始论文中的： $$\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V$$多头自注意力计算过程 自注意力机制通过 Query、Key 和 Value 向量计算每个位置与其他位置之间的依赖关系，使模型能够有效捕捉序列中的全局信息。 然而，自然语言本身具有高度的语义复杂性，一个句子往往同时包含多种类型的语义关系。例如，句子“那只动物没有过马路，因为它太累了”中就涉及多个层面的语言关系：\n“它”指代“那只动物”，属于跨句的代指关系； “因为”连接前后两个分句，体现语义上的因果逻辑； “过马路”构成动词短语，属于固定的动宾结构。 要准确理解这类句子，模型需要同时识别并建模多种层次和类型的依赖关系。但这些信息很难通过单一视角或一套注意力机制完整捕捉。 为此，Transformer 引入了多头注意力机制（Multi-Head Attention）。其核心思想是通过多组独立的 Query、Key、Value 投影，让不同注意力头分别专注于不同的语义关系，最后将各头的输出拼接融合。 多头注意力的计算过程如下：\n分别计算各头注意力： 每个 Self-Attention Head 独立计算一套注意力输出。 合并多头注意力： 多个输出矩阵按维度拼接，再乘以 $W^O$ 得到最终多头注意力的输出。 前馈神经网络层 前馈神经网络（Feed-Forward Network，简称 FFN）是 Transformer 编码器中每个子层的重要组成部分，紧接在多头注意力子层之后。它通过对每个位置的表示进行逐位置、非线性的特征变换，进一步提升模型对复杂语义的建模能力。\n一个标准的 FFN 子层包含两个线性变换和一个非线性激活函数，中间通常使用 ReLU 激活。其计算公式如下： $$\\text{FFN}(x) = \\max(0, xW_1 + b_1)W_2 + b_2$$ 计算图如下：\n残差连接与层归一化 在 Transformer 的每个编码器层中，每个子层，包括自注意力子层和前馈神经网络子层，其输出都要经过残差连接（Residual Connection）和层归一化（Layer Normalization）处理。这两者是深层神经网络中常用的结构，用于缓解模型训练中的梯度消失、收敛困难等问题，对于 Transformer 能够堆叠多层至关重要。\n残差连接 残差连接（Residual Connection，也称“跳跃连接”或“捷径连接”）最初在计算机视觉领域被提出，用于缓解深层神经网络中的梯度消失问题。其核心思想是： 将子层的输入直接与其输出相加，形成一条跨越子层的“捷径”，其数学形式为： $$y = x + \\text{SubLayer}(x)$$ 具体计算过程如图所示：\n残差连接确保反向传播时，梯度至少有一条稳定通路可回传，是深层网络可稳定训练的关键结构。\n层归一化 每个子层在残差连接之后都会进行层归一化（Layer Normalization，简称 LayerNorm）。它的主要作用是规范输入序列中每个 token 的特征分布（某个 token 的表示可能在不同维度上有较大数值差异），提升模型训练的稳定性。 先把数据拉回正态分布，然后给模型两个可学习参数，让模型根据自己的情况去决定最终的分布情况。\n该操作会将每个 token 的向量调整为均值为 $0$、方差为 $1$ 的规范分布，具体效果如下图所示：\n具体的计算公式如下： 假如某个 token 的特征向量为 $x = [x^1, x^2, x^3, x^4, \u0026hellip;, x^d]$，\n均值计算 计算该向量在所有特征维度上的平均值 $$\\mu = \\frac{1}{d} \\sum_{i=1}^{d} x^i$$ 其中 $d$ 为特征维度（向量长度）。\n标准差计算 计算向量各维度的标准差 $$\\sigma = \\sqrt{\\frac{1}{d} \\sum_{i=1}^{d} (x^i - \\mu)^2}$$ 标准化变换 将每个特征值转换为均值为 $0$、方差为 $1$ 的标准正态分布； $$\\hat{x}^i = \\frac{x^i - \\mu}{\\sigma + \\varepsilon}$$ $\\varepsilon$ 为一个小的常数，防止出现除以 0 的情况。\n缩放与平移 为了让模型可以学习在归一化后的基础上进行适当的调整，保证归一化不会限制模型的表示能力。 $$\\text{LayerNorm}(x^i) = \\gamma^i \\cdot \\hat{x}^i + \\beta^i$$ 位置编码 Transformer 模型完全摒弃了 RNN 结构，意味着它不再按顺序处理序列，而是可以并行处理所有位置的信息。尽管这带来了显著的计算效率提升，却也引发了一个问题：Transformer 无法像 RNN 那样天然地捕捉词语之间的顺序关系。换句话说，在没有额外机制的情况下，Transformer 无法区分“猫吃鱼”和“鱼吃猫”这类语序不同但词汇相同的句子。 为了解决这一问题，Transformer 引入了一个关键机制——位置编码（Positional Encoding）。该机制为每个词引入一个表示其位置信息的向量，并将其与对应的词向量相加，作为模型输入的一部分。这样一来，模型在处理每个词时，既能获取词义信息，也能感知其在句子中的位置，从而具备对基本语序的理解能力。\n位置编码最直接的方式是使用绝对位置编号来表示每个词的位置，例如第一个词用 $0$，第二个词用 $1$，依此类推：\n这样做虽然简单，但有一个明显的问题，越靠后的 token 位置编码就越大，若直接与词向量相加，会造成数值倾斜，让模型更关注位置，而忽视词义。\n为缓解这一问题，可以考虑将位置编号归一化为 $[0, 1]$ 区间，例如用 $\\frac{pos}{T-1}$ 表示位置，其中 $T$ 是句子长度。\n这种方式虽然使数值范围更平稳，但也引入了一个严重的问题： 相同位置的词在不同长度句子中的位置编码不再一致。 例如，位置 5 在长度为 10 的句子中被编码为 $\\frac{5}{9}$，在长度为 1000 的句子中则为 $\\frac{5}{999}$。这种依赖输入长度的表示方式会导致模型难以形成稳定的位置感知能力。理想的做法是：每个位置都拥有一个唯一且一致的编码，与句子长度无关。 为了解决上述问题，Transformer 使用了一种基于正弦（sin）和余弦（cos）函数的位置编码方式，具体定义如下：\n$$ \\begin{aligned} PE_{(pos, 2i)} \u0026= \\sin\\left( \\frac{pos}{10000^{\\frac{2i}{d_{model}}}} \\right) \\\\ PE_{(pos, 2i+1)} \u0026= \\cos\\left( \\frac{pos}{10000^{\\frac{2i}{d_{model}}}} \\right) \\end{aligned} $$其中：\n$pos$ 是当前词在序列中的位置； $i$ 用于表示位置编码向量的维度索引，$2i$ 表示偶数维，$2i + 1$ 表示奇数维； $d_{model}$ 是词向量的维度大小。 序列中的每个位置 $pos$ 对应一个长度为 $d_{model}$ 的位置编码向量。该向量的偶数维度通过正弦函数生成，奇数维度通过余弦函数生成，如下图所示\nTransformer 提出的这种编码方式不依赖任何可学习参数，数值稳定，并具备以下优势：\n所有值都在 $[-1,1]$ 范围内，数值稳定 编码方式固定、可预计算，无需训练； 相同位置的编码在不同句子中保持一致； 编码之间具有数学规律，便于模型在注意力机制中感知词语之间的相对位置关系。 编码器完整结构\n解码器 Transformer 解码器的主要功能是：根据编码器的输出，逐步生成目标序列中的每一个词。其生成方式采用自回归机制（autoregressive）：每一步的输入由此前已生成的所有词组成，模型将输出一个与当前输入长度相同的序列表示。我们只取最后一个位置的输出，作为当前步的预测结果。这一过程会不断重复，直到生成特殊的结束标记 \u0026lt;eos\u0026gt;，表示序列生成完成。\n编码器也由多个结构相同的解码器层堆叠组成。\n每个 Decoder Layer 都包含三个子层，分别是 Masked 自注意力子层、编码器-解码器注意力子层（Encoder-Decoder Attention）和前馈神经网络子层（Feed-Forward Network）。\nMasked 自注意力子层（Masked Self Attention） 该子层的主要作用是：建模目标序列中当前位置与前文之间的依赖关系，为当前词的生成提供上下文语义支持。 由于 Transformer 不具备像 RNN 那样的隐藏状态传递机制，无法在序列生成过程中保留上下文信息，因此在生成每一个词时，必须将此前已生成的所有词作为输入，通过自注意力机制重新建模上下文关系，以预测下一个词。 此外，从结构上看，Transformer 编解码器都具有一个典型特性：输入多少个词，就输出多少个表示。需要注意的是，在推理阶段，我们只使用解码器最后一个位置的输出作为当前步的预测结果，如下图所示：\n如果训练阶段也完全按照推理流程进行，就必须将每个目标序列拆分成多个训练样本，每个样本输入一段前文，只预测一个词。如下图所示：\n这种方式虽然逻辑合理，但训练效率极低，完全无法利用 Transformer 并行计算的优势。 为提升效率，Transformer 采用了并行训练策略：一次性输入完整目标序列，同时预测每个位置的词。如下图所示：\n但如果不加限制，这种方式会让模型在预测每个位置时“看到”后面的词，即提前访问未来信息，破坏生成任务的因果结构，如下图所示：\n为解决这个问题，解码器在自注意力机制中引入了遮盖机制（Mask）。该机制会在计算注意力时，阻止模型访问当前位置之后的词，只允许它依赖自身及前文的信息。这样，即使在并行训练时，模型也只能像逐词生成一样“看见”它应该看到的内容，从而保持训练与推理阶段的一致性。如下图所示：\nMask 机制的实现非常简单：只需将注意力得分矩阵中当前位置对其后续位置的评分设置为 $-\\infty$，如下图所示：\n这样，在经过 Softmax 运算后，这些位置的权重会趋近于 $0$。最终在加权求和时，来自未来位置的信息几乎不会参与计算，从而实现了“当前词只能看到它前面的词”的约束。如下图所示：\n编码器-解码器注意力子层 该子层的主要作用是：建模当前解码位置与源语言序列中各位置之间的依赖关系，帮助模型在生成目标词时有效地参考输入内容，相当于 Seq2Seq 模型中的注意力机制。 编码器-解码器注意力的核心机制与前面讲过的自注意力机制完全一致，区别仅在于：\n$Query$ 来自解码器当前的输入表示，即当前生成状态； $Key$ 和 $Value$ 来自编码器的输出表示，即整个源序列的上下文。 也就是说，当前生成位置使用自己的 Query，去“询问”编码器输出中的哪些位置最相关。注意力机制会根据 Query 与所有 Key 的相似度，为每个源位置分配一个权重，然后用这些权重对 Value 进行加权求和，得到当前生成词所需的上下文信息。\n解码器完整结构\n","date":"2026-04-08T17:04:05+08:00","permalink":"/p/transformer/","title":"Transformer 原理梳理"}]