<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>chen</title>
    <link>https://ckxkx.github.io/</link>
    <description>记录代码，也记录生活</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, 朱会琛</copyright>
    <lastBuildDate>Sun, 01 Nov 2026 04:23:13 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <image>
      <url>https://ckxkx.github.io/img/chen-portrait-small.webp</url>
      <title>chen</title>
      <link>https://ckxkx.github.io/</link>
    </image>
    <atom:link href="https://ckxkx.github.io/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>云不喜火锅后记</title>
      <link>https://ckxkx.github.io/2026/10/31/%E4%BA%91%E4%B8%8D%E5%96%9C%E7%81%AB%E9%94%85%E5%90%8E%E8%AE%B0/</link>
      <description>
        <![CDATA[<h1 id="重要的事要说三遍"><a href="#重要的事要说三遍" class="headerlink" title="重要的事要说三遍"></a>重要的事要说三遍</h1><h2 id="戒辣戒辣戒辣！！！"><a href="#戒辣戒辣戒辣！！！" class="he]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      <category domain="https://ckxkx.github.io/tags/%E4%BA%91%E4%B8%8D%E5%96%9C/">云不喜</category>
      <category domain="https://ckxkx.github.io/tags/%E7%81%AB%E9%94%85/">火锅</category>
      <category domain="https://ckxkx.github.io/tags/%E8%83%83%E7%97%9B/">胃痛</category>
      <pubDate>Sat, 31 Oct 2026 19:06:36 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="重要的事要说三遍"><a href="#重要的事要说三遍" class="headerlink" title="重要的事要说三遍"></a>重要的事要说三遍</h1><h2 id="戒辣戒辣戒辣！！！"><a href="#戒辣戒辣戒辣！！！" class="headerlink" title="戒辣戒辣戒辣！！！"></a>戒辣戒辣戒辣！！！</h2><h2 id="饮食习惯后记"><a href="#饮食习惯后记" class="headerlink" title="饮食习惯后记"></a>饮食习惯后记</h2><p>以后真的要改正饮食习惯了，少吃油腻的，辛辣的，刺激的合成食物，多吃天然食材，多吃肉！（真肉）🥩，多尝试吃清谈的食物，慢慢就成习惯了。<br>切忌不要冷热交替！胃真的受不了！！！巨难受😭😭😭</p><h2 id="拍的不错的美食图"><a href="#拍的不错的美食图" class="headerlink" title="拍的不错的美食图"></a>拍的不错的美食图</h2><h3 id="鲜猪肉"><a href="#鲜猪肉" class="headerlink" title="鲜猪肉"></a>鲜猪肉</h3><p><img src="/imgs/IMG_9056.jpeg"></p><h3 id="贡菜毛肚"><a href="#贡菜毛肚" class="headerlink" title="贡菜毛肚"></a>贡菜毛肚</h3><p><img src="/imgs/IMG_9058.jpeg"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用Taio修改苹果手机隐藏文件</title>
      <link>https://ckxkx.github.io/2026/10/31/%E4%BD%BF%E7%94%A8Taio%E4%BF%AE%E6%94%B9%E8%8B%B9%E6%9E%9C%E6%89%8B%E6%9C%BA%E9%9A%90%E8%97%8F%E6%96%87%E4%BB%B6/</link>
      <description>
        <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>由于 ios 的系统限制，我们无法直接查看&#x2F;修改文件夹中的隐藏文件，例如 obsidian 的.obsidian 文件夹。</p>]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%A7%91%E6%8A%80/">科技</category>
      <category domain="https://ckxkx.github.io/tags/%E8%8B%B9%E6%9E%9C%E6%89%8B%E6%9C%BA/">苹果手机</category>
      <category domain="https://ckxkx.github.io/tags/%E9%9A%90%E8%97%8F%E6%96%87%E4%BB%B6/">隐藏文件</category>
      <category domain="https://ckxkx.github.io/tags/Taio/">Taio</category>
      <pubDate>Sat, 31 Oct 2026 04:01:16 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>由于 ios 的系统限制，我们无法直接查看&#x2F;修改文件夹中的隐藏文件，例如 obsidian 的.obsidian 文件夹。</p><p>经过多个文件管理软件的测试，最终推荐 Taio。<br>Taio 的优缺点<br>优点：简单，免费<br>缺点：有一个坑！！！（下文细说）</p><h2 id="操作步骤"><a href="#操作步骤" class="headerlink" title="操作步骤"></a>操作步骤</h2><h3 id="1-从-appstore-找到这个-app，下载安装"><a href="#1-从-appstore-找到这个-app，下载安装" class="headerlink" title="1.从 appstore 找到这个 app，下载安装"></a>1.从 appstore 找到这个 app，下载安装</h3><p><img src="/imgs/f8874e88fdf698d2fc1b8406cb4df819.jpg" alt="334"></p><h3 id="2-打开以后，一定记得选-“添加的位置”，不要选我的-iPhone-！！！"><a href="#2-打开以后，一定记得选-“添加的位置”，不要选我的-iPhone-！！！" class="headerlink" title="2.打开以后，一定记得选 “添加的位置”，不要选我的 iPhone ！！！"></a>2.打开以后，一定记得选 “添加的位置”，不要选我的 iPhone ！！！</h3><p>选择<strong>添加的位置</strong>才是真正在原位置修改文件。<br>选择我的 iPhone 然后导入文件指的是：复制一份相同的文件到此处（这个坑给我搞麻了！🤡🤡）</p><p><img src="/imgs/852263ccb43e61919ff746256fe2365a.jpg" alt="329"></p><h3 id="3-添加完成后打开就能看到隐藏文件了（如果没显示-obsidian-就点击右上角把显示隐藏文件打开就可以了）"><a href="#3-添加完成后打开就能看到隐藏文件了（如果没显示-obsidian-就点击右上角把显示隐藏文件打开就可以了）" class="headerlink" title="3.添加完成后打开就能看到隐藏文件了（如果没显示.obsidian 就点击右上角把显示隐藏文件打开就可以了）"></a>3.添加完成后打开就能看到隐藏文件了（如果没显示.obsidian 就点击右上角把显示隐藏文件打开就可以了）</h3><p><img src="/imgs/89c2d7247e76f95d4446aeedf03d599b.jpg" alt="532"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>从源码角度理解Transformer</title>
      <link>https://ckxkx.github.io/2026/10/30/technology/%E4%BB%8E%E6%BA%90%E7%A0%81%E8%A7%92%E5%BA%A6%E7%90%86%E8%A7%A3Transformer/</link>
      <description>
        <![CDATA[<img src="/images/uploads/Transformer架构.webp" alt="Transformer架构.png" style="width:70%; max-width:100%;">

<h1 id="从源码角度理解Transformer"><a hr]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E5%AD%A6%E4%B9%A0/">学习</category>
      <category domain="https://ckxkx.github.io/tags/Transformer/">Transformer</category>
      <category domain="https://ckxkx.github.io/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/">大模型</category>
      <pubDate>Fri, 30 Oct 2026 11:16:50 GMT</pubDate>
      <content:encoded>
        <![CDATA[<img src="/images/uploads/Transformer架构.webp" alt="Transformer架构.png" style="width:70%; max-width:100%;"><h1 id="从源码角度理解Transformer"><a href="#从源码角度理解Transformer" class="headerlink" title="从源码角度理解Transformer"></a>从源码角度理解Transformer</h1><p>理解 Transformer，可以先沿着一个张量往下走：输入是什么形状，经过哪一步计算，哪些位置交换了信息，最后又变成什么形状。</p><p>本文以自己的 <code>Transformer.py</code> 修正版为线索，依次拆解五个类：<code>PositionalEncoding</code>、<code>MultiHeadAttention</code>、<code>PositionWiseFeedForward</code>、<code>EncoderLayer</code> 和 <code>DecoderLayer</code>。文中也会逐步解释缩放点积的数值计算。</p><p>这份源码实现了编码器和解码器的核心层。词嵌入、多层堆叠、词表输出层与训练循环还需要在外部组装。本文最后给出一个把这些核心层连起来的运行示例。</p><p><strong>按不熟悉的内容选择入口：</strong></p><table><thead><tr><th>想弄清的问题</th><th>阅读位置</th></tr></thead><tbody><tr><td><code>self</code>、<code>super()</code>、<code>forward()</code>、<code>dim</code>、切片是什么意思？</td><td><a href="#%E9%9B%B6%E3%80%81%E5%85%88%E8%AF%BB%E6%87%82%E4%BB%A3%E7%A0%81%E9%87%8C%E7%9A%84-Python-%E5%92%8C%E5%BC%A0%E9%87%8F%E8%AF%AD%E6%B3%95">基础语法与张量</a></td></tr><tr><td><code>arange</code>、<code>exp</code>、<code>sin/cos</code>、<code>register_buffer</code> 怎么用？</td><td><a href="#%E4%BA%8C%E3%80%81PositionalEncoding%EF%BC%9A%E5%91%8A%E8%AF%89%E6%A8%A1%E5%9E%8B%E2%80%9C%E6%88%91%E5%9C%A8%E5%93%AA%E9%87%8C%E2%80%9D">位置编码与具体数值</a></td></tr><tr><td><code>Linear</code>、<code>view</code>、<code>transpose</code>、<code>matmul</code>、<code>softmax</code> 怎么用？</td><td><a href="#%E4%B8%89%E3%80%81MultiHeadAttention%EF%BC%9A%E8%AE%A9%E4%B8%8D%E5%90%8C%E4%BD%8D%E7%BD%AE%E4%BA%A4%E6%8D%A2%E4%BF%A1%E6%81%AF">多头注意力</a></td></tr><tr><td><code>~mask</code>、<code>&amp;</code>、<code>masked_fill</code>、<code>all</code>、<code>keepdim</code> 是什么？</td><td><a href="#%E5%9B%9B%E3%80%81Mask%EF%BC%9A%E9%99%90%E5%AE%9A%E6%AF%8F%E4%B8%AA-Query-%E8%83%BD%E7%9C%8B%E5%93%AA%E4%BA%9B-Key">掩码逐步计算</a></td></tr><tr><td>为什么 <code>transpose</code> 后还要 <code>contiguous</code>？</td><td><a href="#%E4%BA%94%E3%80%81combine-heads%EF%BC%9A%E6%8A%8A%E5%A4%9A%E5%A4%B4%E7%BB%93%E6%9E%9C%E8%BF%98%E5%8E%9F">合头与内存布局</a></td></tr><tr><td><code>ReLU</code>、<code>Dropout</code>、<code>LayerNorm</code> 各做什么？</td><td><a href="#%E5%85%AD%E3%80%81PositionWiseFeedForward%EF%BC%9A%E9%80%90%E4%BD%8D%E7%BD%AE%E5%8A%A0%E5%B7%A5%E7%89%B9%E5%BE%81">前馈与激活</a>、<a href="#%E4%B8%83%E3%80%81EncoderLayer%EF%BC%9A%E6%8A%8A%E4%B8%A4%E4%B8%AA%E5%AD%90%E5%B1%82%E7%BB%84%E8%A3%85%E8%B5%B7%E6%9D%A5">归一化与残差</a></td></tr><tr><td><code>Embedding</code>、<code>reshape</code>、交叉熵、<code>backward</code> 如何连接？</td><td><a href="#%E4%B9%9D%E3%80%81%E6%9C%80%E5%B0%8F%E8%BF%90%E8%A1%8C%E7%A4%BA%E4%BE%8B%EF%BC%9A%E6%8A%8A%E6%BA%90%E7%A0%81%E4%B8%AD%E7%9A%84%E5%B1%82%E6%8E%A5%E8%B5%B7%E6%9D%A5">完整例子与函数拆解</a></td></tr></tbody></table><p>小例子的浮点输出按便于阅读的精度展示；实际打印有时会出现 <code>0.10000000149</code> 等尾数，这是浮点表示的正常现象。新增可独立运行的代码块已经在本机 PyTorch 环境执行核对。</p><h2 id="零、先读懂代码里的-Python-和张量语法"><a href="#零、先读懂代码里的-Python-和张量语法" class="headerlink" title="零、先读懂代码里的 Python 和张量语法"></a>零、先读懂代码里的 Python 和张量语法</h2><p>本文把“函数怎么用”和“为什么用在这里”放在一起解释。遇到一行较长的代码，可以按 <strong>调用对象 → 参数 → 返回值 → 形状变化</strong> 的顺序拆开读。</p><p>如果不熟悉 PyTorch，先阅读本节，再看位置编码中的数值例子、多头注意力中的分头例子，以及掩码中的逐步计算。每个以 <code>import torch</code> 开头的小例子都可以单独复制运行。</p><h3 id="1-import-torch-nn-as-nn：给模块起一个短名字"><a href="#1-import-torch-nn-as-nn：给模块起一个短名字" class="headerlink" title="1. import torch.nn as nn：给模块起一个短名字"></a>1. <code>import torch.nn as nn</code>：给模块起一个短名字</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><span class="hljs-keyword">import</span> math<br></code></pre></td></tr></table></figure><p><code>torch</code> 提供张量创建、矩阵乘法和自动求导等功能。<code>torch.nn</code> 提供线性层、Dropout 等神经网络组件；<code>as nn</code> 让我们可以写 <code>nn.Linear</code>，完整名字仍然是 <code>torch.nn.Linear</code>。<code>math</code> 是 Python 的数学模块，这里用到自然对数 <code>math.log()</code> 和平方根 <code>math.sqrt()</code>。</p><h3 id="2-nn-Module、-init-、self、super-各做什么？"><a href="#2-nn-Module、-init-、self、super-各做什么？" class="headerlink" title="2. nn.Module、__init__、self、super() 各做什么？"></a>2. <code>nn.Module</code>、<code>__init__</code>、<code>self</code>、<code>super()</code> 各做什么？</h3><p>先看一个比 Transformer 更小的模块：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">TinyLayer</span>(nn.Module):<br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, width</span>):<br>        <span class="hljs-built_in">super</span>().__init__()<br>        <span class="hljs-variable language_">self</span>.width = width<br>        <span class="hljs-variable language_">self</span>.linear = nn.Linear(width, width)<br><br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">forward</span>(<span class="hljs-params">self, x</span>):<br>        <span class="hljs-keyword">return</span> <span class="hljs-variable language_">self</span>.linear(x)<br><br>layer = TinyLayer(<span class="hljs-number">4</span>)             <span class="hljs-comment"># 创建模块，执行 __init__</span><br>x = torch.ones(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>)<br>y = layer(x)                    <span class="hljs-comment"># 调用模块，执行 forward 中的计算</span><br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(y.shape))           <span class="hljs-comment"># (2, 3, 4)</span><br></code></pre></td></tr></table></figure><p><code>class TinyLayer(nn.Module)</code> 表示这个类继承 PyTorch 的模块基类，因此具有参数管理、设备移动、保存状态等能力。</p><p><code>__init__</code> 是初始化方法。创建 <code>TinyLayer(4)</code> 时执行它，建立后续反复使用的线性层。<strong>层通常在初始化时创建，输入计算写在 <code>forward</code> 中</strong>；若每次 <code>forward</code> 都重新创建一个线性层，就会反复生成新的随机参数。</p><p><code>self</code> 表示当前这个实例。<code>self.width = width</code> 把配置存在实例上，之后其他方法也能读取；<code>self.linear = nn.Linear(...)</code> 则把一个子模块挂在当前模块上，PyTorch 会自动登记它的参数。</p><p><code>super().__init__()</code> 调用父类初始化，建立模块管理所需的内部状态。源码里的 <code>super(MultiHeadAttention, self).__init__()</code> 在这里表达相同目的。</p><p>日常使用 <code>layer(x)</code> 调用模块，PyTorch 会经由模块调用机制进入 <code>forward()</code>，也会处理已注册的钩子。自己通常不需要直接调用 <code>layer.forward(x)</code>。参见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.nn.Module.html">nn.Module 文档</a>。</p><h3 id="3-三维张量究竟是什么？"><a href="#3-三维张量究竟是什么？" class="headerlink" title="3. 三维张量究竟是什么？"></a>3. 三维张量究竟是什么？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>x = torch.zeros(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>)<br><span class="hljs-built_in">print</span>(x.ndim)             <span class="hljs-comment"># 3：有三个轴</span><br><span class="hljs-built_in">print</span>(x.shape)            <span class="hljs-comment"># torch.Size([2, 3, 4])</span><br><span class="hljs-built_in">print</span>(x.size())           <span class="hljs-comment"># torch.Size([2, 3, 4])</span><br><span class="hljs-built_in">print</span>(x.size(<span class="hljs-number">0</span>))          <span class="hljs-comment"># 2：第 0 轴的长度</span><br><span class="hljs-built_in">print</span>(x.size(<span class="hljs-number">1</span>))          <span class="hljs-comment"># 3：第 1 轴的长度</span><br><span class="hljs-built_in">print</span>(x.size(-<span class="hljs-number">1</span>))         <span class="hljs-comment"># 4：最后一轴的长度</span><br></code></pre></td></tr></table></figure><p>对本文的 <code>[B,L,D]</code>，可以想象为：一批有 2 句话，每句话有 3 个 token，每个 token 用 4 个数字表示。<code>x[0,1,2]</code> 是第 0 句话、第 1 个 token 的第 2 个特征值；下标从 0 开始。</p><p>注意两个容易混用的意思：<code>x.ndim == 3</code> 是“有三个轴”，<code>x.size(-1) == 4</code> 是“最后一个轴有四个元素”。</p><p><code>dim</code> 参数指定沿哪个轴操作；负数从末尾计数。对于 <code>[B,H,L,d_k]</code>：</p><table><thead><tr><th>轴下标</th><th>对应含义</th></tr></thead><tbody><tr><td><code>0</code> 或 <code>-4</code></td><td>batch</td></tr><tr><td><code>1</code> 或 <code>-3</code></td><td>注意力头</td></tr><tr><td><code>2</code> 或 <code>-2</code></td><td>序列位置</td></tr><tr><td><code>3</code> 或 <code>-1</code></td><td>每头特征</td></tr></tbody></table><p>因此，<code>transpose(-2,-1)</code> 里的 <code>-1</code> 是轴下标；而 <code>view(...,-1)</code> 里的 <code>-1</code> 表示自动推算长度。它们由不同函数解释，含义不同。</p><h3 id="4-切片里的-、0-2、None-怎么读？"><a href="#4-切片里的-、0-2、None-怎么读？" class="headerlink" title="4. 切片里的 :、0::2、None 怎么读？"></a>4. 切片里的 <code>:</code>、<code>0::2</code>、<code>None</code> 怎么读？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>a = torch.arange(<span class="hljs-number">6</span>)       <span class="hljs-comment"># tensor([0, 1, 2, 3, 4, 5])</span><br><span class="hljs-built_in">print</span>(a[:<span class="hljs-number">3</span>].tolist())     <span class="hljs-comment"># [0, 1, 2]：到下标 3 之前</span><br><span class="hljs-built_in">print</span>(a[<span class="hljs-number">1</span>:].tolist())     <span class="hljs-comment"># [1, 2, 3, 4, 5]：从下标 1 开始</span><br><span class="hljs-built_in">print</span>(a[<span class="hljs-number">0</span>::<span class="hljs-number">2</span>].tolist())   <span class="hljs-comment"># [0, 2, 4]：每隔 2 个取一个</span><br><span class="hljs-built_in">print</span>(a[<span class="hljs-number">1</span>::<span class="hljs-number">2</span>].tolist())   <span class="hljs-comment"># [1, 3, 5]</span><br><br>x = torch.zeros(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>)<br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(x[:, :<span class="hljs-number">2</span>].shape))     <span class="hljs-comment"># (2, 2, 4)</span><br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(x[<span class="hljs-number">0</span>].shape))         <span class="hljs-comment"># (3, 4)：整数下标取出一项，会减少一个轴</span><br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(x[<span class="hljs-number">0</span>:<span class="hljs-number">1</span>].shape))       <span class="hljs-comment"># (1, 3, 4)：切片保留这个轴</span><br></code></pre></td></tr></table></figure><p>切片的完整形式是 <code>开始:结束:步长</code>，结束位置不包含在结果中。逗号分隔不同轴；后面的轴没有写出时，相当于保留全部内容。</p><p>索引中的 <code>None</code> 会在当前位置插入一个长度为 1 的轴。例如 <code>[B,L]</code> 的 <code>valid[:,None,None,:]</code> 会变成 <code>[B,1,1,L]</code>，后面的掩码部分会给出实际数据。</p><h3 id="5-unsqueeze-dim-：插入长度为-1-的轴"><a href="#5-unsqueeze-dim-：插入长度为-1-的轴" class="headerlink" title="5. unsqueeze(dim)：插入长度为 1 的轴"></a>5. <code>unsqueeze(dim)</code>：插入长度为 1 的轴</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>v = torch.tensor([<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>])<br><span class="hljs-built_in">print</span>(v.unsqueeze(<span class="hljs-number">0</span>).tolist())  <span class="hljs-comment"># [[0, 1, 2]]，形状 [1, 3]</span><br><span class="hljs-built_in">print</span>(v.unsqueeze(<span class="hljs-number">1</span>).tolist())  <span class="hljs-comment"># [[0], [1], [2]]，形状 [3, 1]</span><br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(v.shape))          <span class="hljs-comment"># (3,)：原变量 v 的形状没有改变</span><br></code></pre></td></tr></table></figure><p><code>dim</code> 决定新轴插入的位置，原有的三个数字没有增加。对于一维 <code>v</code>，<code>v.unsqueeze(-1)</code> 也会得到 <code>[3,1]</code>。</p><p><code>unsqueeze()</code> 返回新视图，通常与原张量共享底层数据；它没有下划线结尾，不会直接把变量 <code>v</code> 的形状改掉。源码中的 <code>position = ...unsqueeze(1)</code> 是把返回结果重新赋给 <code>position</code>。详见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.unsqueeze.html">unsqueeze 文档</a>。</p><h3 id="6-广播：不同形状为什么还能逐元素相加、相乘？"><a href="#6-广播：不同形状为什么还能逐元素相加、相乘？" class="headerlink" title="6. 广播：不同形状为什么还能逐元素相加、相乘？"></a>6. 广播：不同形状为什么还能逐元素相加、相乘？</h3><p>逐元素运算会<strong>从最右边对齐各轴</strong>。每一对轴的长度必须相等，或者至少有一个为 1；缺失的前导轴视为 1。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>column = torch.tensor([[<span class="hljs-number">0.0</span>], [<span class="hljs-number">1.0</span>], [<span class="hljs-number">2.0</span>]])  <span class="hljs-comment"># [3, 1]</span><br>row = torch.tensor([<span class="hljs-number">1.0</span>, <span class="hljs-number">0.1</span>])               <span class="hljs-comment"># [2]，对齐时看作 [1, 2]</span><br><span class="hljs-built_in">print</span>((column * row).tolist())<br><span class="hljs-comment"># 约 [[0.0, 0.0], [1.0, 0.1], [2.0, 0.2]]，结果 [3, 2]</span><br></code></pre></td></tr></table></figure><p>这恰好对应位置编码中的 <code>position * div_term</code>：每个位置都乘上每一个频率。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">输入向量       [2, 6, 12]<br>位置编码       [1, 6, 12]<br>逐轴对齐       2/1 可扩展；6/6 相同；12/12 相同<br>相加结果       [2, 6, 12]<br></code></pre></td></tr></table></figure><p><code>[2,6,12]</code> 和 <code>[6]</code> 则不能这样相加，因为最右边的 <code>12</code> 与 <code>6</code> 不匹配。广播主要是计算时按规则复用数据，无需先手动复制多份张量；运算结果仍然需要存储空间。参见 <a href="https://docs.pytorch.org/docs/2.10/notes/broadcasting.html">广播规则</a>。</p><h3 id="7-其他-Python-写法速读"><a href="#7-其他-Python-写法速读" class="headerlink" title="7. 其他 Python 写法速读"></a>7. 其他 Python 写法速读</h3><table><thead><tr><th>源码写法</th><th>如何理解</th></tr></thead><tbody><tr><td><code>d_model: int</code></td><td>类型提示，告诉读者这里期待整数，本身不会自动完成运行时校验</td></tr><tr><td><code>dropout=0.1</code></td><td>默认参数，调用时不传就使用 <code>0.1</code></td></tr><tr><td><code>mask=None</code></td><td>默认没有提供掩码</td></tr><tr><td><code>if mask is not None:</code></td><td>只在确实传入掩码时执行；不要改成 <code>if mask:</code> 来判断多元素张量</td></tr><tr><td><code>d_model % num_heads</code></td><td>求余数，用于检查能否均分</td></tr><tr><td><code>d_model // num_heads</code></td><td>整数除法，例如 <code>12 // 3 == 4</code></td></tr><tr><td><code>raise ValueError(...)</code></td><td>输入不符合要求时主动报错，并停止本次调用</td></tr><tr><td><code>batch_size, seq_len, d_model = x.size()</code></td><td>按顺序把三个轴长度分别赋给三个变量</td></tr><tr><td><code>return x</code></td><td>把当前结果返回给调用者</td></tr></tbody></table><h2 id="一、先建立一张源码地图"><a href="#一、先建立一张源码地图" class="headerlink" title="一、先建立一张源码地图"></a>一、先建立一张源码地图</h2><table><thead><tr><th>类</th><th>主要职责</th><th>输入 → 输出</th></tr></thead><tbody><tr><td><code>PositionalEncoding</code></td><td>给词向量加入位置信息</td><td><code>[B, L, D] → [B, L, D]</code></td></tr><tr><td><code>MultiHeadAttention</code></td><td>根据 Q、K 的匹配程度融合 V</td><td><code>[B, Lq, D]</code>、<code>[B, Lk, D]</code>、<code>[B, Lk, D] → [B, Lq, D]</code></td></tr><tr><td><code>PositionWiseFeedForward</code></td><td>对每个位置进行非线性特征变换</td><td><code>[B, L, D] → [B, L, D]</code></td></tr><tr><td><code>EncoderLayer</code></td><td>自注意力 + 前馈网络 + 两次残差归一化</td><td><code>[B, Ls, D] → [B, Ls, D]</code></td></tr><tr><td><code>DecoderLayer</code></td><td>因果自注意力 + 交叉注意力 + 前馈网络</td><td>目标 <code>[B, Lt, D]</code>、源表示 <code>[B, Ls, D] → [B, Lt, D]</code></td></tr></tbody></table><p>统一约定：</p><ul><li><code>B</code>：批次大小。</li><li><code>L</code>：序列长度；<code>Ls</code>、<code>Lt</code> 分别表示源序列、目标序列长度。</li><li><code>D = d_model</code>：每个 token 的特征维度。</li><li><code>H = num_heads</code>：注意力头数。</li><li><code>d_k = D // H</code>：每个头的特征维度；这份实现中每头 V 的维度也等于 <code>d_k</code>。</li><li><code>d_ff</code>：前馈网络的中间维度。</li></ul><p>后文用 <code>B=2, Ls=6, Lt=4, D=12, H=3, d_k=4</code> 举例。所有核心模块默认 batch 在第一维。</p><pre><code class=" mermaid">flowchart TD    Src[&quot;源 token IDs&quot;] --&gt; SrcEmb[&quot;词嵌入 + 位置编码&quot;]    SrcEmb --&gt; Enc[&quot;EncoderLayer：自注意力 → 前馈网络&quot;]    Enc --&gt; Memory[&quot;encoder_output：源序列表示&quot;]    Tgt[&quot;右移后的目标 token IDs&quot;] --&gt; TgtEmb[&quot;词嵌入 + 位置编码&quot;]    TgtEmb --&gt; Self[&quot;DecoderLayer：因果自注意力&quot;]    Self --&gt; Cross[&quot;交叉注意力：Q 来自目标，K/V 来自源&quot;]    Memory --&gt; Cross    Cross --&gt; FF[&quot;前馈网络&quot;]    FF --&gt; Head[&quot;词表线性层 → logits&quot;]</code></pre><p>图中的嵌入和词表层属于外部组装；源码中的每个注意力、前馈子层都带有残差连接和 LayerNorm。</p><h2 id="二、PositionalEncoding：告诉模型“我在哪里”"><a href="#二、PositionalEncoding：告诉模型“我在哪里”" class="headerlink" title="二、PositionalEncoding：告诉模型“我在哪里”"></a>二、PositionalEncoding：告诉模型“我在哪里”</h2><h3 id="1-为什么输入词向量以后还要加位置编码？"><a href="#1-为什么输入词向量以后还要加位置编码？" class="headerlink" title="1. 为什么输入词向量以后还要加位置编码？"></a>1. 为什么输入词向量以后还要加位置编码？</h3><p>自注意力本身根据向量内容计算匹配程度。如果不加入位置相关信息，模型缺少显式区分词语顺序的线索。</p><p>源码使用固定的正弦、余弦位置编码。其形式为：</p><p>$$<br>PE(pos,2i)&#x3D;\sin\left(\frac{pos}{10000^{2i&#x2F;D}}\right)<br>$$</p><p>$$<br>PE(pos,2i+1)&#x3D;\cos\left(\frac{pos}{10000^{2i&#x2F;D}}\right)<br>$$</p><p>不同特征维度采用不同频率，为每个位置提供一组有规律的数值。公式来源见原论文的<a href="https://arxiv.org/html/1706.03762v7#S3.SS5">位置编码部分</a>。</p><h3 id="2-对照构造函数读公式"><a href="#2-对照构造函数读公式" class="headerlink" title="2. 对照构造函数读公式"></a>2. 对照构造函数读公式</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs python">position = torch.arange(max_len).unsqueeze(<span class="hljs-number">1</span>)<br>div_term = torch.exp(<br>    torch.arange(<span class="hljs-number">0</span>, d_model, <span class="hljs-number">2</span>)<br>    * (-math.log(<span class="hljs-number">10000.0</span>) / d_model)<br>)<br>pe = torch.zeros(max_len, d_model)<br>pe[:, <span class="hljs-number">0</span>::<span class="hljs-number">2</span>] = torch.sin(position * div_term)<br>pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>] = torch.cos(position * div_term[:pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>].size(<span class="hljs-number">1</span>)])<br><span class="hljs-variable language_">self</span>.register_buffer(<span class="hljs-string">&quot;pe&quot;</span>, pe.unsqueeze(<span class="hljs-number">0</span>))<br></code></pre></td></tr></table></figure><p><code>position</code> 的形状是 <code>[max_len, 1]</code>。<code>div_term</code> 为偶数下标维度生成频率系数，广播相乘后，一次算出所有位置、所有频率的相位。</p><p>这里的指数写法与公式里的除法等价：</p><p>$$<br>\exp\left(-\frac{2i\log 10000}{D}\right)&#x3D;\frac{1}{10000^{2i&#x2F;D}}<br>$$</p><p><code>0::2</code> 对应下标 <code>0, 2, 4, ...</code>；<code>1::2</code> 对应 <code>1, 3, 5, ...</code>。当 <code>D</code> 为奇数时，奇数下标的列数会少一列，所以余弦分支需要截取对应数量的频率。</p><p><code>unsqueeze(0)</code> 把位置编码变为 <code>[1, max_len, D]</code>，方便对一个 batch 中的所有样本共享位置编码。</p><h3 id="2-1-把位置编码这几行算成具体数字"><a href="#2-1-把位置编码这几行算成具体数字" class="headerlink" title="2.1 把位置编码这几行算成具体数字"></a>2.1 把位置编码这几行算成具体数字</h3><p>把 <code>max_len</code> 缩小到 3、<code>d_model</code> 缩小到 4，就可以直接观察每一步：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> math<br><span class="hljs-keyword">import</span> torch<br><br>max_len, d_model = <span class="hljs-number">3</span>, <span class="hljs-number">4</span><br>position = torch.arange(max_len).unsqueeze(<span class="hljs-number">1</span>)<br>even_dims = torch.arange(<span class="hljs-number">0</span>, d_model, <span class="hljs-number">2</span>)<br>div_term = torch.exp(even_dims * (-math.log(<span class="hljs-number">10000.0</span>) / d_model))<br>angles = position * div_term<br>pe = torch.zeros(max_len, d_model)<br>pe[:, <span class="hljs-number">0</span>::<span class="hljs-number">2</span>] = torch.sin(angles)<br>pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>] = torch.cos(angles)<br><br><span class="hljs-built_in">print</span>(position.tolist())       <span class="hljs-comment"># [[0], [1], [2]]</span><br><span class="hljs-built_in">print</span>(even_dims.tolist())      <span class="hljs-comment"># [0, 2]</span><br><span class="hljs-built_in">print</span>(div_term.tolist())       <span class="hljs-comment"># 约 [1.0, 0.01]</span><br><span class="hljs-built_in">print</span>(angles.tolist())         <span class="hljs-comment"># 约 [[0, 0], [1, 0.01], [2, 0.02]]</span><br><span class="hljs-built_in">print</span>(pe)<br><span class="hljs-comment"># 约为：</span><br><span class="hljs-comment"># tensor([[0.0000, 1.0000, 0.0000, 1.0000],</span><br><span class="hljs-comment">#         [0.8415, 0.5403, 0.0100, 0.9999],</span><br><span class="hljs-comment">#         [0.9093,-0.4161, 0.0200, 0.9998]])</span><br></code></pre></td></tr></table></figure><p>逐句对应函数的用法：</p><table><thead><tr><th>表达式</th><th>参数与返回结果</th><th>在这里的用途</th></tr></thead><tbody><tr><td><code>torch.arange(3)</code></td><td>从 0 开始，到 3 之前，返回 <code>[0,1,2]</code></td><td>生成位置编号</td></tr><tr><td><code>torch.arange(0,4,2)</code></td><td>从 0 开始、步长 2、到 4 之前，返回 <code>[0,2]</code></td><td>生成偶数特征下标</td></tr><tr><td><code>math.log(10000.0)</code></td><td>Python 浮点数的自然对数，约 9.21034</td><td>构造频率指数中的常数</td></tr><tr><td><code>torch.exp(t)</code></td><td>对张量每个元素求 <code>e</code> 的该元素次方，形状不变</td><td>把负指数转换成频率系数</td></tr><tr><td><code>torch.zeros(3,4)</code></td><td>创建三行四列的零张量</td><td>准备保存最终位置编码</td></tr><tr><td><code>torch.sin(t)</code>、<code>torch.cos(t)</code></td><td>逐元素计算正弦、余弦，输入单位为弧度</td><td>从相位得到编码值</td></tr><tr><td><code>pe[:,0::2] = ...</code></td><td>修改 pe 的第 0、2 列，所有行都参与</td><td>写入正弦结果</td></tr><tr><td><code>pe[:,1::2] = ...</code></td><td>修改第 1、3 列</td><td>写入余弦结果</td></tr></tbody></table><p>构造函数中的赋值会实际修改 <code>pe</code>。与普通 <code>pe + something</code> 返回计算结果不同，<code>pe[...] = ...</code> 是把数据写入指定切片。</p><p>再看最难读的奇数维处理：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>] = torch.cos(position * div_term[:pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>].size(<span class="hljs-number">1</span>)])<br></code></pre></td></tr></table></figure><p>把右侧拆开，等价于下面这几步：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python">odd_columns = pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>]          <span class="hljs-comment"># 取出需要填写 cos 的那些列</span><br>odd_count = odd_columns.size(<span class="hljs-number">1</span>)    <span class="hljs-comment"># 这些列一共有几列</span><br>cos_frequencies = div_term[:odd_count]<br>cos_angles = position * cos_frequencies<br>pe[:, <span class="hljs-number">1</span>::<span class="hljs-number">2</span>] = torch.cos(cos_angles)<br></code></pre></td></tr></table></figure><p>例如 <code>d_model=5</code> 时，正弦负责第 <code>0,2,4</code> 列，有三个频率；余弦只负责第 <code>1,3</code> 列，只需要前两个频率。截断是为了使赋值左右两侧列数相同。</p><h3 id="3-buffer-与参数有什么区别？"><a href="#3-buffer-与参数有什么区别？" class="headerlink" title="3. buffer 与参数有什么区别？"></a>3. buffer 与参数有什么区别？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-variable language_">self</span>.register_buffer(<span class="hljs-string">&quot;pe&quot;</span>, pe.unsqueeze(<span class="hljs-number">0</span>))<br></code></pre></td></tr></table></figure><p>固定位置编码无需由优化器更新，但需要随模型移动设备，并随模型保存。注册成 buffer 正好表达这种状态：<code>pe</code> 可以通过 <code>self.pe</code> 访问，默认进入 <code>state_dict()</code>，但不属于可训练参数。参见 <a href="https://docs.pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.register_buffer">PyTorch 的 register_buffer 文档</a>。</p><p>原代码只写了注册 buffer 的注释，没有实际执行这行语句，因此调用 <code>forward()</code> 会出现找不到 <code>self.pe</code> 的错误。</p><h3 id="4-forward-中的广播"><a href="#4-forward-中的广播" class="headerlink" title="4. forward 中的广播"></a>4. forward 中的广播</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs python">x = x + <span class="hljs-variable language_">self</span>.pe[:, :x.size(<span class="hljs-number">1</span>)].to(dtype=x.dtype)<br><span class="hljs-keyword">return</span> <span class="hljs-variable language_">self</span>.dropout(x)<br></code></pre></td></tr></table></figure><p>形状变化是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">输入 x                 [2, 6, 12]<br>切片后的位置编码       [1, 6, 12]<br>沿 batch 维广播后相加  [2, 6, 12]<br></code></pre></td></tr></table></figure><p>序列长度取自 <code>x.size(1)</code>。修正版会检查输入维度以及是否超过 <code>max_len</code>；超长输入需要增加构造时的 <code>max_len</code>。位置编码的 dtype 与输入对齐，设备则通过先把模块和输入移动到同一设备来保持一致。</p><h3 id="4-1-把位置编码的-forward-完整展开"><a href="#4-1-把位置编码的-forward-完整展开" class="headerlink" title="4.1 把位置编码的 forward 完整展开"></a>4.1 把位置编码的 forward 完整展开</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">if</span> x.ndim != <span class="hljs-number">3</span> <span class="hljs-keyword">or</span> x.size(-<span class="hljs-number">1</span>) != <span class="hljs-variable language_">self</span>.pe.size(-<span class="hljs-number">1</span>):<br>    <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">&quot;x must have shape [B, L, d_model]&quot;</span>)<br><span class="hljs-keyword">if</span> x.size(<span class="hljs-number">1</span>) &gt; <span class="hljs-variable language_">self</span>.pe.size(<span class="hljs-number">1</span>):<br>    <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">&quot;sequence length exceeds max_len&quot;</span>)<br><br>seq_len = x.size(<span class="hljs-number">1</span>)                       <span class="hljs-comment"># 本次输入有多少个 token</span><br>position_part = <span class="hljs-variable language_">self</span>.pe[:, :seq_len]      <span class="hljs-comment"># 取前 seq_len 个位置</span><br>position_part = position_part.to(dtype=x.dtype)<br>x_with_position = x + position_part      <span class="hljs-comment"># 沿 batch 维广播相加</span><br>output = <span class="hljs-variable language_">self</span>.dropout(x_with_position)   <span class="hljs-comment"># 由训练/评估模式决定是否丢弃元素</span><br><span class="hljs-keyword">return</span> output<br></code></pre></td></tr></table></figure><p>这里展示的是方法内部代码，<code>self</code> 和 <code>x</code> 来自模块调用。第一条检查使用 Python 的 <code>or</code>：只要左侧“不是三维”已经成立，就不会继续计算右侧；这样的短路判断也避免了对错误维数输入继续按预期结构访问。</p><p><code>x.dtype</code> 表示元素类型，例如 <code>torch.float32</code>；<code>x.device</code> 表示数据位于 CPU 还是某块 GPU。<code>tensor.to(dtype=...)</code> 转换元素类型，<code>tensor.to(device=...)</code> 移动设备，也可以同时指定。对 Tensor 调用 <code>.to()</code> 应使用返回值；对 Module 调用 <code>.to()</code> 会修改模块中参数和 buffer 的设备&#x2F;类型，并返回模块本身。</p><p><code>self.dropout = nn.Dropout(...)</code> 是创建一个可反复调用的组件，<code>self.dropout(x)</code> 才是把本次输入送入该组件。前馈网络一节会给出 Dropout 的具体输出规则。</p><h2 id="三、MultiHeadAttention：让不同位置交换信息"><a href="#三、MultiHeadAttention：让不同位置交换信息" class="headerlink" title="三、MultiHeadAttention：让不同位置交换信息"></a>三、MultiHeadAttention：让不同位置交换信息</h2><p>整个模块可以按五个动作阅读：<strong>投影 → 分头 → 注意力 → 合头 → 输出投影</strong>。</p><h3 id="1-为什么有四个线性层？"><a href="#1-为什么有四个线性层？" class="headerlink" title="1. 为什么有四个线性层？"></a>1. 为什么有四个线性层？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-variable language_">self</span>.W_q = nn.Linear(d_model, d_model)<br><span class="hljs-variable language_">self</span>.W_k = nn.Linear(d_model, d_model)<br><span class="hljs-variable language_">self</span>.W_v = nn.Linear(d_model, d_model)<br><span class="hljs-variable language_">self</span>.W_out = nn.Linear(d_model, d_model)<br></code></pre></td></tr></table></figure><p>前三层把输入变换到不同的特征空间：Q 表达当前位置想寻找什么，K 提供匹配线索，V 提供实际要融合的内容。<code>W_out</code> 对拼接后的各个头进行可学习的特征混合。</p><p>在源码中，<code>forward(Q, K, V)</code> 的形参是<strong>投影前的输入</strong>。经过 <code>W_q/W_k/W_v</code> 后，同名变量才成为实际用于注意力计算的查询、键和值。</p><p>以自注意力为例：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">attn_output = <span class="hljs-variable language_">self</span>.self_attn(x, x, x, mask)<br></code></pre></td></tr></table></figure><p>三个输入来自同一个 <code>x</code>，但投影层参数各不相同，所以 Q、K、V 通常仍然不同。PyTorch 的 <code>nn.Linear</code> 默认还带偏置，常见公式 <code>Q=XW_Q</code> 在这里应理解为省略了偏置的简写。</p><h3 id="1-1-nn-Linear-in-features-out-features-到底算了什么？"><a href="#1-1-nn-Linear-in-features-out-features-到底算了什么？" class="headerlink" title="1.1 nn.Linear(in_features, out_features) 到底算了什么？"></a>1.1 <code>nn.Linear(in_features, out_features)</code> 到底算了什么？</h3><p><code>nn.Linear(3,2)</code> 表示：每个输入向量含 3 个特征，输出向量含 2 个特征。默认有可训练的 <code>weight</code> 和 <code>bias</code>；权重形状为 <code>[out_features,in_features]</code>，实际计算是：</p><p>$$<br>y&#x3D;xW^\top+b<br>$$</p><p>为了避免随机权重掩盖计算过程，下面手动设置权重，只用于演示：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><br>linear = nn.Linear(<span class="hljs-number">3</span>, <span class="hljs-number">2</span>, bias=<span class="hljs-literal">False</span>)<br><span class="hljs-keyword">with</span> torch.no_grad():<br>    linear.weight.copy_(torch.tensor([<br>        [<span class="hljs-number">1.0</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>],<br>        [<span class="hljs-number">0.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">0.0</span>],<br>    ]))<br><br>x = torch.tensor([[<span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">3.0</span>]])<br><span class="hljs-built_in">print</span>(linear(x).tolist())            <span class="hljs-comment"># [[4.0, 4.0]]</span><br><span class="hljs-comment"># 第一个输出 = 1*1 + 2*0 + 3*1 = 4</span><br><span class="hljs-comment"># 第二个输出 = 1*0 + 2*2 + 3*0 = 4</span><br><br>batch = torch.ones(<span class="hljs-number">2</span>, <span class="hljs-number">5</span>, <span class="hljs-number">3</span>)<br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(linear(batch).shape))    <span class="hljs-comment"># (2, 5, 2)</span><br></code></pre></td></tr></table></figure><p><code>with torch.no_grad():</code> 临时关闭这段操作的梯度记录，便于给参数写入演示值；<code>copy_()</code> 把数值复制到已有权重张量中，末尾的下划线表示原地修改。</p><p>对 <code>[2,5,3]</code>，线性层独立处理每一个三维向量，前两个轴保持不变。源码中的 <code>nn.Linear(D,D)</code> 虽然输入、输出维度相同，内部仍然进行可学习的数值变换，并非原样返回。详见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.nn.Linear.html">Linear 文档</a>。</p><h3 id="2-split-heads：拆分的是特征维度"><a href="#2-split-heads：拆分的是特征维度" class="headerlink" title="2. split_heads：拆分的是特征维度"></a>2. split_heads：拆分的是特征维度</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">split_heads</span>(<span class="hljs-params">self, x</span>):<br>    batch_size, seq_len, d_model = x.size()<br>    <span class="hljs-keyword">return</span> x.view(<br>        batch_size, seq_len, <span class="hljs-variable language_">self</span>.num_heads, <span class="hljs-variable language_">self</span>.d_k<br>    ).transpose(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)<br></code></pre></td></tr></table></figure><p>逐步看形状：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">[B, L, D]       = [2, 6, 12]<br>view           → [2, 6, 3, 4]<br>transpose(1,2) → [2, 3, 6, 4]<br>                 B  H  L  d_k<br></code></pre></td></tr></table></figure><p>每个头仍然能访问完整序列；它处理的是投影后的一部分特征维度。<code>D</code> 必须能被 <code>H</code> 整除，否则就无法按这份实现均分。</p><p>不同头可以学习不同的关注模式，但代码没有规定“第一个头必须学习语法、第二个头必须学习语义”，这种分工不能预先保证。</p><h3 id="2-1-view-与-transpose-的区别，用数据看更清楚"><a href="#2-1-view-与-transpose-的区别，用数据看更清楚" class="headerlink" title="2.1 view() 与 transpose() 的区别，用数据看更清楚"></a>2.1 <code>view()</code> 与 <code>transpose()</code> 的区别，用数据看更清楚</h3><p><code>view()</code> 按兼容的内存布局重新解释形状，元素总数必须一致；<code>transpose(a,b)</code> 交换两个轴。它们承担不同的步骤，不能仅因为最终形状一样就互相替换。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br><span class="hljs-comment"># 1 个样本，2 个 token，每个 token 有 4 个特征；拆为 2 个头。</span><br>x = torch.tensor([[<br>    [<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>],<br>    [<span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">6</span>, <span class="hljs-number">7</span>],<br>]])                                      <span class="hljs-comment"># [1, 2, 4]</span><br><br>grouped = x.view(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>)              <span class="hljs-comment"># [B, L, H, d_k]</span><br>heads = grouped.transpose(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)           <span class="hljs-comment"># [B, H, L, d_k]</span><br><span class="hljs-built_in">print</span>(heads[<span class="hljs-number">0</span>, <span class="hljs-number">0</span>].tolist())               <span class="hljs-comment"># 第 0 头：[[0, 1], [4, 5]]</span><br><span class="hljs-built_in">print</span>(heads[<span class="hljs-number">0</span>, <span class="hljs-number">1</span>].tolist())               <span class="hljs-comment"># 第 1 头：[[2, 3], [6, 7]]</span><br><br>wrong = x.view(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>)                <span class="hljs-comment"># 只 view，没有交换 L 与 H</span><br><span class="hljs-built_in">print</span>(wrong[<span class="hljs-number">0</span>, <span class="hljs-number">0</span>].tolist())               <span class="hljs-comment"># [[0, 1], [2, 3]]，数据归属不同</span><br></code></pre></td></tr></table></figure><p>在 <code>grouped[B,L,H,d_k]</code> 中，先选择 token，再选择这个 token 的某个头；在 <code>heads[B,H,L,d_k]</code> 中，先选头，再访问该头看到的所有 token。</p><p>这个例子特意让 <code>L=H=2</code>：两种排列的形状完全相同，但 <code>heads[0,0]</code> 与 <code>wrong[0,0]</code> 数值不同。因此检查形状时，还要核对每个轴代表什么。</p><p><code>view(1,2,2,2)</code> 中各长度的乘积为 8，与原来的 <code>1×2×4</code> 相等；改成 <code>view(1,2,3,2)</code> 就无法容纳同样多的元素。<code>view(1,2,-1)</code> 可让 PyTorch 根据总数推算最后一轴为 4，但只能有一个待推算的 <code>-1</code>。参见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.Tensor.view.html">view 文档</a> 与 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.transpose.html">transpose 文档</a>。</p><h3 id="3-缩放点积：从匹配分数到信息汇总"><a href="#3-缩放点积：从匹配分数到信息汇总" class="headerlink" title="3. 缩放点积：从匹配分数到信息汇总"></a>3. 缩放点积：从匹配分数到信息汇总</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python">attn_scores = torch.matmul(<br>    Q, K.transpose(-<span class="hljs-number">2</span>, -<span class="hljs-number">1</span>)<br>) / math.sqrt(<span class="hljs-variable language_">self</span>.d_k)<br>attn_probs = torch.softmax(attn_scores, dim=-<span class="hljs-number">1</span>)<br>output = torch.matmul(attn_probs, V)<br></code></pre></td></tr></table></figure><p>暂时忽略掩码，公式是：</p><p>$$<br>\operatorname{Attention}(Q,K,V)<br>&#x3D;\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V<br>$$</p><p>对交叉注意力，假设目标长度为 4、源长度为 6：</p><table><thead><tr><th>步骤</th><th>张量形状</th><th>意义</th></tr></thead><tbody><tr><td>Q</td><td><code>[2, 3, 4, 4]</code></td><td>每个目标位置、每个头的查询</td></tr><tr><td>K</td><td><code>[2, 3, 6, 4]</code></td><td>每个源位置、每个头的键</td></tr><tr><td>K 转置</td><td><code>[2, 3, 4, 6]</code></td><td>交换最后两维以便矩阵乘法</td></tr><tr><td>QKᵀ</td><td><code>[2, 3, 4, 6]</code></td><td>每个目标位置与全部源位置的匹配分数</td></tr><tr><td>Softmax</td><td><code>[2, 3, 4, 6]</code></td><td>在最后的 6 个源位置之间分配权重</td></tr><tr><td>权重乘 V</td><td><code>[2, 3, 4, 4]</code></td><td>汇总每个头的信息</td></tr></tbody></table><p><code>transpose(-2, -1)</code> 只交换最后两个维度，batch 和头维保持原样。<code>dim=-1</code> 则保证每个 Query 分别在自己的所有 Key 之间归一化。</p><p>为什么除以 <code>sqrt(d_k)</code>？在各分量独立、均值为 0、方差为 1 的简化假设下，点积的方差会随 <code>d_k</code> 增大，除以其平方根有助于控制得分尺度，避免 Softmax 过早趋于极端。注意力公式和缩放动机见<a href="https://arxiv.org/html/1706.03762v7#S3.SS2">原论文的注意力部分</a>。</p><p>原代码使用 <code>torch.bmm()</code>，但分头后 Q&#x2F;K 已经是四维张量。<code>bmm</code> 要求输入为三维，因此这里改成支持前导批量维度的 <code>matmul</code>。也可以先把 <code>B</code> 和 <code>H</code> 合并再使用 <code>bmm</code>，但那需要额外的形状转换。参见 <a href="https://docs.pytorch.org/docs/2.9/generated/torch.bmm.html">bmm 文档</a> 和 <a href="https://docs.pytorch.org/docs/stable/generated/torch.matmul">matmul 文档</a>。</p><h3 id="3-1-torch-matmul-与-分别在乘什么？"><a href="#3-1-torch-matmul-与-分别在乘什么？" class="headerlink" title="3.1 torch.matmul() 与 * 分别在乘什么？"></a>3.1 <code>torch.matmul()</code> 与 <code>*</code> 分别在乘什么？</h3><p>先看二维情况：矩阵乘法把左矩阵的一行与右矩阵的一列对应相乘，再求和。<code>[m,n] @ [n,p]</code> 得到 <code>[m,p]</code>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>a = torch.tensor([[<span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>], [<span class="hljs-number">3.0</span>, <span class="hljs-number">4.0</span>]])<br>b = torch.tensor([[<span class="hljs-number">10.0</span>, <span class="hljs-number">20.0</span>], [<span class="hljs-number">30.0</span>, <span class="hljs-number">40.0</span>]])<br><span class="hljs-built_in">print</span>((a * b).tolist())          <span class="hljs-comment"># 逐元素相乘：[[10, 40], [90, 160]]</span><br><span class="hljs-built_in">print</span>(torch.matmul(a, b).tolist())<br><span class="hljs-comment"># 矩阵乘法：[[70, 100], [150, 220]]</span><br><span class="hljs-comment"># 左上角 70 = 1*10 + 2*30</span><br></code></pre></td></tr></table></figure><p><code>a @ b</code> 是矩阵乘法的另一种写法；它与上例的 <code>torch.matmul(a,b)</code> 表达相同运算。</p><p>对四维的 <code>[B,H,Lq,d_k] @ [B,H,d_k,Lk]</code>，<code>matmul</code> 把最后两轴视为矩阵，前面的 <code>[B,H]</code> 视为批量索引。它分别计算每个样本、每个头的乘积；这里不会把不同头彼此相乘。</p><p><code>Q * K</code> 只做按位置相乘，无法生成“每个 Query 对所有 Key”的完整匹配表。</p><h3 id="3-2-torch-softmax-scores-dim-1-：为什么-dim-很重要？"><a href="#3-2-torch-softmax-scores-dim-1-：为什么-dim-很重要？" class="headerlink" title="3.2 torch.softmax(scores, dim=-1)：为什么 dim 很重要？"></a>3.2 <code>torch.softmax(scores, dim=-1)</code>：为什么 dim 很重要？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>scores = torch.tensor([[<span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">3.0</span>],<br>                       [<span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">3.0</span>]])<br>by_row = torch.softmax(scores, dim=-<span class="hljs-number">1</span>)<br>by_column = torch.softmax(scores, dim=<span class="hljs-number">0</span>)<br><span class="hljs-built_in">print</span>(by_row)<br><span class="hljs-comment"># tensor([[0.0900, 0.2447, 0.6652],</span><br><span class="hljs-comment">#         [0.0900, 0.2447, 0.6652]])</span><br><span class="hljs-built_in">print</span>(by_row.<span class="hljs-built_in">sum</span>(dim=-<span class="hljs-number">1</span>).tolist())       <span class="hljs-comment"># [1.0, 1.0]，浮点误差下近似成立</span><br><span class="hljs-built_in">print</span>(by_column)<br><span class="hljs-comment"># tensor([[0.5000, 0.5000, 0.5000],</span><br><span class="hljs-comment">#         [0.5000, 0.5000, 0.5000]])</span><br></code></pre></td></tr></table></figure><p><code>dim=-1</code> 沿每一行的三个得分归一化；<code>dim=0</code> 沿每一列的两个得分归一化。虽然输入完全相同，选择不同轴会得到不同权重。</p><p>对注意力得分 <code>[B,H,Lq,Lk]</code>，最后一轴是 Key 位置，所以要对它做 Softmax。每个 Query 得到自己的 <code>Lk</code> 个权重，随后这些权重与 V 相乘，完成加权求和。</p><h3 id="4-把多头注意力的-forward-拆成没有嵌套的写法"><a href="#4-把多头注意力的-forward-拆成没有嵌套的写法" class="headerlink" title="4. 把多头注意力的 forward 拆成没有嵌套的写法"></a>4. 把多头注意力的 forward 拆成没有嵌套的写法</h3><p>原代码的 <code>self.split_heads(self.W_q(Q))</code> 从内向外执行。展开后是：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs python">q_projected = <span class="hljs-variable language_">self</span>.W_q(Q)                    <span class="hljs-comment"># [B,Lq,D]</span><br>k_projected = <span class="hljs-variable language_">self</span>.W_k(K)                    <span class="hljs-comment"># [B,Lk,D]</span><br>v_projected = <span class="hljs-variable language_">self</span>.W_v(V)                    <span class="hljs-comment"># [B,Lk,D]</span><br><br>q_heads = <span class="hljs-variable language_">self</span>.split_heads(q_projected)      <span class="hljs-comment"># [B,H,Lq,d_k]</span><br>k_heads = <span class="hljs-variable language_">self</span>.split_heads(k_projected)      <span class="hljs-comment"># [B,H,Lk,d_k]</span><br>v_heads = <span class="hljs-variable language_">self</span>.split_heads(v_projected)      <span class="hljs-comment"># [B,H,Lk,d_k]</span><br><br>head_output = <span class="hljs-variable language_">self</span>.scaled_dot_product_attention(<br>    q_heads, k_heads, v_heads, mask<br>)                                           <span class="hljs-comment"># [B,H,Lq,d_k]</span><br>merged = <span class="hljs-variable language_">self</span>.combine_heads(head_output)     <span class="hljs-comment"># [B,Lq,D]</span><br>output = <span class="hljs-variable language_">self</span>.W_out(merged)                  <span class="hljs-comment"># [B,Lq,D]</span><br><span class="hljs-keyword">return</span> output<br></code></pre></td></tr></table></figure><p>这是 <code>forward</code> 方法内部的等价展开，不需要重复执行两套代码。<code>split_heads</code>、<code>combine_heads</code> 是自己定义的普通实例方法；<code>self.W_q(...)</code>、<code>self.W_out(...)</code> 调用的是注册在实例上的神经网络子模块。</p><h2 id="四、Mask：限定每个-Query-能看哪些-Key"><a href="#四、Mask：限定每个-Query-能看哪些-Key" class="headerlink" title="四、Mask：限定每个 Query 能看哪些 Key"></a>四、Mask：限定每个 Query 能看哪些 Key</h2><h3 id="1-先统一这份代码的约定"><a href="#1-先统一这份代码的约定" class="headerlink" title="1. 先统一这份代码的约定"></a>1. 先统一这份代码的约定</h3><p>本实现使用<strong>允许关注的掩码</strong>：<code>True/1</code> 表示可以关注，<code>False/0</code> 表示屏蔽。</p><p>这份实现不接收 <code>0/-inf</code> 形式的加性偏置掩码。不同 PyTorch 接口的布尔掩码语义也可能不同，迁移调用时应先核对对应文档。</p><p>为了适配 <code>[B, H, Lq, Lk]</code> 的注意力分数，修正版支持：</p><table><thead><tr><th>传入形状</th><th>处理方式 &#x2F; 用途</th></tr></thead><tbody><tr><td><code>[Lq, Lk]</code></td><td>扩成 <code>[1, 1, Lq, Lk]</code>，所有样本、所有头共用</td></tr><tr><td><code>[B, Lq, Lk]</code></td><td>扩成 <code>[B, 1, Lq, Lk]</code>，每个样本单独指定</td></tr><tr><td><code>[B, 1, 1, Lk]</code></td><td>所有 Query、所有头共用该样本的 Key 有效性</td></tr><tr><td>其他四维形状</td><td>需能广播到 <code>[B, H, Lq, Lk]</code></td></tr></tbody></table><p>二维张量固定解释为 <code>[Lq, Lk]</code>。如果手头是 <code>[B, Lk]</code> 的 padding 标记，应先写成 <code>valid[:, None, None, :]</code>，再传给模块。</p><h3 id="2-Padding-Mask：屏蔽补齐位置"><a href="#2-Padding-Mask：屏蔽补齐位置" class="headerlink" title="2. Padding Mask：屏蔽补齐位置"></a>2. Padding Mask：屏蔽补齐位置</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">src_mask = (src_ids != pad_id)[:, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :]<br></code></pre></td></tr></table></figure><p>假设一个样本的源输入是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">token:    A  B  C  PAD  PAD  PAD<br>有效性:   1  1  1   0    0    0<br></code></pre></td></tr></table></figure><p>屏蔽的是注意力矩阵的 <strong>Key 列</strong>，使各个 Query 不从 PAD 位置获取信息。这个 mask 可以同时用于编码器自注意力和解码器交叉注意力。</p><p>这并不保证 PAD 对应的 Query 行输出为零。后续前馈网络、残差和带偏置的投影仍可能产生数值；训练时还需要在损失中忽略 PAD 标签。</p><h3 id="3-Causal-Mask：屏蔽未来位置"><a href="#3-Causal-Mask：屏蔽未来位置" class="headerlink" title="3. Causal Mask：屏蔽未来位置"></a>3. Causal Mask：屏蔽未来位置</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">causal_mask = torch.ones(Lt, Lt, dtype=torch.<span class="hljs-built_in">bool</span>).tril()<br></code></pre></td></tr></table></figure><p>当 <code>Lt=4</code> 时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs text">           Key 位置<br>            0 1 2 3<br>Query 0     1 0 0 0<br>Query 1     1 1 0 0<br>Query 2     1 1 1 0<br>Query 3     1 1 1 1<br></code></pre></td></tr></table></figure><p>第 <code>i</code> 个位置只能读取自己和更早位置。训练时，目标输入已经右移：例如用 <code>[BOS, 我, 爱]</code> 预测 <code>[我, 爱, 你]</code>，所以允许看到当前位置的输入仍然不会暴露当前位置要预测的标签。</p><p>目标序列包含 PAD 时，可以把两种限制合并：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">tgt_mask = (<br>    (tgt_input != pad_id)[:, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :]<br>    &amp; causal_mask[<span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :, :]<br>)<br></code></pre></td></tr></table></figure><p><code>DecoderLayer</code> 接收外部传入的 <code>tgt_mask</code>，不会自行生成因果掩码。若传入 <code>None</code>，它就会执行不受因果约束的自注意力；用于自回归训练时必须显式传入。</p><h3 id="4-为什么还要处理“整行都被屏蔽”？"><a href="#4-为什么还要处理“整行都被屏蔽”？" class="headerlink" title="4. 为什么还要处理“整行都被屏蔽”？"></a>4. 为什么还要处理“整行都被屏蔽”？</h3><p>一般屏蔽逻辑是把相应得分设成负无穷，使其 Softmax 权重为零：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">attn_scores = attn_scores.masked_fill(~mask, <span class="hljs-built_in">float</span>(<span class="hljs-string">&quot;-inf&quot;</span>))<br></code></pre></td></tr></table></figure><p>但一行全部变成 <code>-inf</code> 时，普通 Softmax 会产生 NaN；若全部替换成相同的有限负数，又会得到均匀权重，让本应被忽略的 Value 混入输出。</p><p>修正版显式定义这个边界行为：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">all_masked = (~mask).<span class="hljs-built_in">all</span>(dim=-<span class="hljs-number">1</span>, keepdim=<span class="hljs-literal">True</span>)<br>attn_scores = attn_scores.masked_fill(all_masked, <span class="hljs-number">0.0</span>)<br>attn_probs = torch.softmax(attn_scores, dim=-<span class="hljs-number">1</span>)<br>attn_probs = attn_probs.masked_fill(~mask, <span class="hljs-number">0.0</span>)<br></code></pre></td></tr></table></figure><p>先让空行的 Softmax 输入有限，再把被屏蔽位置的权重清零。这样该行的加权汇总为零，反向传播也避免了这类 NaN。普通有效行的权重和为 1；整行屏蔽时则为 0。</p><p>这里保证的是 <code>scaled_dot_product_attention()</code> 的汇总结果为零。完整 <code>MultiHeadAttention</code> 后面还有带偏置的 <code>W_out</code>，因此最终输出不一定为零。</p><p>使用 <code>-inf</code> 还避免了原代码中 <code>-1e9</code> 无法以 FP16 有限数表示的问题，但它不能消除所有低精度数值风险，例如 QK 点积自身的溢出。</p><h3 id="5-、-、masked-fill-、all-分别怎么用？"><a href="#5-、-、masked-fill-、all-分别怎么用？" class="headerlink" title="5. ~、&amp;、masked_fill()、all() 分别怎么用？"></a>5. <code>~</code>、<code>&amp;</code>、<code>masked_fill()</code>、<code>all()</code> 分别怎么用？</h3><p>先用两行得分演示：第一行允许关注两个位置，第二行全部屏蔽。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>scores = torch.tensor([[<span class="hljs-number">2.0</span>, <span class="hljs-number">1.0</span>, <span class="hljs-number">0.0</span>],<br>                       [<span class="hljs-number">3.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">1.0</span>]])<br>mask = torch.tensor([[<span class="hljs-literal">True</span>, <span class="hljs-literal">False</span>, <span class="hljs-literal">True</span>],<br>                     [<span class="hljs-literal">False</span>, <span class="hljs-literal">False</span>, <span class="hljs-literal">False</span>]])<br><br>blocked = ~mask<br><span class="hljs-built_in">print</span>(blocked.tolist())<br><span class="hljs-comment"># [[False, True, False], [True, True, True]]</span><br><br>masked_scores = scores.masked_fill(blocked, <span class="hljs-built_in">float</span>(<span class="hljs-string">&quot;-inf&quot;</span>))<br><span class="hljs-built_in">print</span>(masked_scores.tolist())<br><span class="hljs-comment"># [[2.0, -inf, 0.0], [-inf, -inf, -inf]]</span><br><br>all_masked = blocked.<span class="hljs-built_in">all</span>(dim=-<span class="hljs-number">1</span>, keepdim=<span class="hljs-literal">True</span>)<br><span class="hljs-built_in">print</span>(all_masked.tolist())                <span class="hljs-comment"># [[False], [True]]</span><br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(all_masked.shape))            <span class="hljs-comment"># (2, 1)</span><br><br>safe_scores = masked_scores.masked_fill(all_masked, <span class="hljs-number">0.0</span>)<br><span class="hljs-built_in">print</span>(safe_scores.tolist())<br><span class="hljs-comment"># [[2.0, -inf, 0.0], [0.0, 0.0, 0.0]]</span><br><br>probs = torch.softmax(safe_scores, dim=-<span class="hljs-number">1</span>)<br>probs = probs.masked_fill(blocked, <span class="hljs-number">0.0</span>)<br><span class="hljs-built_in">print</span>(probs)<br><span class="hljs-comment"># tensor([[0.8808, 0.0000, 0.1192],</span><br><span class="hljs-comment">#         [0.0000, 0.0000, 0.0000]])</span><br></code></pre></td></tr></table></figure><p><strong><code>~mask</code>：逐元素逻辑取反。</strong> 对 bool 张量，<code>True</code> 变 <code>False</code>，<code>False</code> 变 <code>True</code>。源码先把 mask 转成 <code>torch.bool</code> 再取反；若对整数张量直接用 <code>~</code>，执行的是整数按位取反，不能按布尔示例理解。</p><p><strong><code>tensor.masked_fill(condition,value)</code>：把 condition 为 True 的位置填成 value。</strong> 因为原 mask 的 True 表示允许关注，需要传入 <code>~mask</code> 才能定位要屏蔽的位置。它返回新结果，所以源码把结果重新赋给 <code>attn_scores</code>。带下划线的 <code>masked_fill_()</code> 才表示原地修改。</p><p><code>float(&quot;-inf&quot;)</code> 是 Python 中表示负无穷的浮点值。Softmax 的指数步骤中，负无穷对应的指数值为零。</p><p><strong><code>.all(dim=-1)</code>：检查最后一轴是否全部为 True。</strong> 上例中第一行有允许的位置，返回 False；第二行全部被屏蔽，返回 True。<code>keepdim=True</code> 把被汇总的轴保留为长度 1，结果是 <code>[2,1]</code>，能广播回 <code>[2,3]</code>，从而整行填零。四维时同理：<code>[B,H,Lq,Lk] → [B,H,Lq,1]</code>。</p><p><strong><code>a &amp; b</code>：布尔张量的逐元素“并且”。</strong> 目标 padding mask 和因果 mask 都允许的位置才保留。Python 的 <code>and</code> 用于整体真假判断，不适合拿来替代多元素张量的 <code>&amp;</code>。例如：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>a = torch.tensor([<span class="hljs-literal">True</span>, <span class="hljs-literal">True</span>, <span class="hljs-literal">False</span>])<br>b = torch.tensor([<span class="hljs-literal">True</span>, <span class="hljs-literal">False</span>, <span class="hljs-literal">True</span>])<br><span class="hljs-built_in">print</span>((a &amp; b).tolist())                  <span class="hljs-comment"># [True, False, False]</span><br></code></pre></td></tr></table></figure><p>相关规则可核对 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.Tensor.masked_fill.html">masked_fill</a> 和 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.all.html">all 的 dim&#x2F;keepdim 参数</a>。</p><h3 id="6-用真实形状理解-None-None"><a href="#6-用真实形状理解-None-None" class="headerlink" title="6. 用真实形状理解 [:, None, None, :]"></a>6. 用真实形状理解 <code>[:, None, None, :]</code></h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>ids = torch.tensor([[<span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">0</span>],<br>                    [<span class="hljs-number">6</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>]])                 <span class="hljs-comment"># [B,Lk] = [2,3]</span><br>valid = ids != <span class="hljs-number">0</span><br><span class="hljs-built_in">print</span>(valid.tolist())<br><span class="hljs-comment"># [[True, True, False], [True, False, False]]</span><br><br>mask = valid[:, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :]                   <span class="hljs-comment"># [B,1,1,Lk]</span><br>same_mask = valid.unsqueeze(<span class="hljs-number">1</span>).unsqueeze(<span class="hljs-number">1</span>)<br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(mask.shape))                        <span class="hljs-comment"># (2,1,1,3)</span><br><span class="hljs-built_in">print</span>(torch.equal(mask, same_mask))              <span class="hljs-comment"># True</span><br></code></pre></td></tr></table></figure><p>这里的 <code>!=</code> 对每个 token ID 分别比较，返回同样形状的布尔张量。新加的两个长度为 1 的轴，对应“共享给所有注意力头”和“共享给所有 Query”。<code>torch.equal(a,b)</code> 检查两个张量的形状和元素值是否完全一致。</p><p>源码中的 <code>mask.to(device=attn_scores.device,dtype=torch.bool)</code> 一次完成两件事：把掩码移到得分所在设备，并统一成布尔类型。转换后任何非零数都会变为 True；因此应传入约定的 0&#x2F;1 或 bool 掩码，不能把 <code>-inf</code> 当作屏蔽标记传入这个接口。</p><h2 id="五、combine-heads：把多头结果还原"><a href="#五、combine-heads：把多头结果还原" class="headerlink" title="五、combine_heads：把多头结果还原"></a>五、combine_heads：把多头结果还原</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">combine_heads</span>(<span class="hljs-params">self, x</span>):<br>    batch_size, num_heads, seq_len, d_k = x.size()<br>    <span class="hljs-keyword">return</span> x.transpose(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>).contiguous().view(<br>        batch_size, seq_len, <span class="hljs-variable language_">self</span>.d_model<br>    )<br></code></pre></td></tr></table></figure><p>以长度为 4 的目标序列为例：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">注意力输出         [2, 3, 4, 4] = [B, H, L, d_k]<br>transpose(1,2)  → [2, 4, 3, 4] = [B, L, H, d_k]<br>contiguous()    → 保持形状，整理内存布局<br>view            → [2, 4, 12]   = [B, L, D]<br></code></pre></td></tr></table></figure><p>先把头维移到序列维后面，再拼接各个头的特征。转置后张量通常不再连续，<code>contiguous()</code> 让后续 <code>view()</code> 能按预期重排。</p><p>原代码用三个变量解包四维形状，会在此处报错。修正后，再执行：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">output = <span class="hljs-variable language_">self</span>.W_out(<span class="hljs-variable language_">self</span>.combine_heads(attn_output))<br></code></pre></td></tr></table></figure><p>多头注意力最终保持 Query 的序列长度，并回到 <code>d_model</code> 维，为残差相加做好准备。</p><h3 id="1-为什么转置后还要-contiguous-？"><a href="#1-为什么转置后还要-contiguous-？" class="headerlink" title="1. 为什么转置后还要 .contiguous()？"></a>1. 为什么转置后还要 <code>.contiguous()</code>？</h3><p>张量的“逻辑形状”和底层数据如何存放是两件事。转置通常共享原来的数据，只改变访问这些数据的步长；此时按行读取的逻辑顺序可能不再对应连续内存。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>x = torch.arange(<span class="hljs-number">6</span>).view(<span class="hljs-number">2</span>, <span class="hljs-number">3</span>)<br><span class="hljs-built_in">print</span>(x.tolist())                         <span class="hljs-comment"># [[0,1,2], [3,4,5]]</span><br>t = x.transpose(<span class="hljs-number">0</span>, <span class="hljs-number">1</span>)<br><span class="hljs-built_in">print</span>(t.tolist())                         <span class="hljs-comment"># [[0,3], [1,4], [2,5]]</span><br><span class="hljs-built_in">print</span>(t.is_contiguous())                  <span class="hljs-comment"># False</span><br><br>c = t.contiguous()<br><span class="hljs-built_in">print</span>(c.is_contiguous())                  <span class="hljs-comment"># True</span><br><span class="hljs-built_in">print</span>(c.view(-<span class="hljs-number">1</span>).tolist())                 <span class="hljs-comment"># [0,3,1,4,2,5]</span><br></code></pre></td></tr></table></figure><p>直接对本例的 <code>t</code> 调用 <code>view(-1)</code> 会报错，因为当前步长无法按要求解释为一维；<code>contiguous()</code> 按转置后的逻辑顺序整理出连续布局，再 <code>view(-1)</code> 就能展开。</p><p>它不是把矩阵“转回去”：<code>c</code> 的形状仍然是 <code>[3,2]</code>，元素位置仍然与 <code>t</code> 相同。若输入本来就连续，<code>contiguous()</code> 可以直接返回它。参见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.Tensor.contiguous.html">contiguous 文档</a>。</p><p><code>reshape()</code> 也是改变形状：能复用视图时复用，需要时会复制，因此 <code>t.reshape(-1)</code> 也能得到上述一维结果。不过，合头前的 <code>transpose</code> 仍然需要保留，因为它决定各个头的特征如何归属到对应 token。</p><h3 id="2-把合头的数字接回前面的分头例子"><a href="#2-把合头的数字接回前面的分头例子" class="headerlink" title="2. 把合头的数字接回前面的分头例子"></a>2. 把合头的数字接回前面的分头例子</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>x = torch.tensor([[[<span class="hljs-number">0</span>,<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>], [<span class="hljs-number">4</span>,<span class="hljs-number">5</span>,<span class="hljs-number">6</span>,<span class="hljs-number">7</span>]]])<br>heads = x.view(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>).transpose(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)<br>merged = heads.transpose(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>).contiguous().view(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">4</span>)<br><span class="hljs-built_in">print</span>(merged.tolist())                    <span class="hljs-comment"># [[[0,1,2,3], [4,5,6,7]]]</span><br><span class="hljs-built_in">print</span>(torch.equal(merged, x))              <span class="hljs-comment"># True</span><br></code></pre></td></tr></table></figure><p>这个往返示例说明的是形状操作能还原数据顺序。实际注意力会在分头之后更新数值，所以真实模型合头后的内容已经融合了上下文。</p><h2 id="六、PositionWiseFeedForward：逐位置加工特征"><a href="#六、PositionWiseFeedForward：逐位置加工特征" class="headerlink" title="六、PositionWiseFeedForward：逐位置加工特征"></a>六、PositionWiseFeedForward：逐位置加工特征</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">x = <span class="hljs-variable language_">self</span>.linear1(x)<br>x = <span class="hljs-variable language_">self</span>.relu(x)<br>x = <span class="hljs-variable language_">self</span>.dropout(x)<br>x = <span class="hljs-variable language_">self</span>.linear2(x)<br></code></pre></td></tr></table></figure><p>当 <code>d_ff=24</code> 时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">[2, 6, 12] → Linear → [2, 6, 24]<br>           → ReLU   → [2, 6, 24]<br>           → Dropout→ [2, 6, 24]<br>           → Linear → [2, 6, 12]<br></code></pre></td></tr></table></figure><p><code>nn.Linear</code> 作用于最后一个维度，所以所有 token 使用同一组线性层，各自处理自己的特征向量。这个模块内部不进行跨位置的信息交换。</p><p>可以把两类模块的分工理解为：注意力负责让位置之间交换信息，前馈网络负责对每个位置已经获得的信息做非线性加工。</p><p>忽略 Dropout 后，前馈网络可写成：</p><p>$$<br>\operatorname{FFN}(x)&#x3D;\max(0,xW_1+b_1)W_2+b_2<br>$$</p><p>先扩展再压回原维度，既增加了中间表示能力，又保持子层接口的 <code>D</code> 维形状。</p><h3 id="1-nn-ReLU-：把负值变成零"><a href="#1-nn-ReLU-：把负值变成零" class="headerlink" title="1. nn.ReLU()：把负值变成零"></a>1. <code>nn.ReLU()</code>：把负值变成零</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><br>relu = nn.ReLU()<br>x = torch.tensor([-<span class="hljs-number">2.0</span>, -<span class="hljs-number">0.5</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>, <span class="hljs-number">3.0</span>])<br><span class="hljs-built_in">print</span>(relu(x).tolist())                  <span class="hljs-comment"># [0.0,0.0,0.0,1.0,3.0]</span><br></code></pre></td></tr></table></figure><p>它对每个元素执行 <code>max(0,x)</code>，形状保持不变。<code>self.relu = nn.ReLU()</code> 是创建激活函数组件，<code>self.relu(x)</code> 是执行它。两个线性层之间加入非线性，才能表达单个线性变换无法表达的映射；如果只连续堆叠两层仿射变换，整体仍可合并为一层仿射变换。参见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.nn.ReLU.html">ReLU 文档</a>。</p><h3 id="2-nn-Dropout-p-：训练时随机把部分元素置零"><a href="#2-nn-Dropout-p-：训练时随机把部分元素置零" class="headerlink" title="2. nn.Dropout(p)：训练时随机把部分元素置零"></a>2. <code>nn.Dropout(p)</code>：训练时随机把部分元素置零</h3><p><code>p</code> 表示每个元素被置零的概率。训练时，保留下来的元素还会乘以 <code>1/(1-p)</code>，保持数值的期望；评估时按原值输出。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><br>drop = nn.Dropout(p=<span class="hljs-number">0.5</span>)<br>x = torch.ones(<span class="hljs-number">10</span>)<br><br>drop.train()<br>y = drop(x)<br><span class="hljs-built_in">print</span>(y)                                <span class="hljs-comment"># 每项为 0 或 2，具体位置随机</span><br><br>drop.<span class="hljs-built_in">eval</span>()<br><span class="hljs-built_in">print</span>(drop(x).tolist())                  <span class="hljs-comment"># 十个 1.0</span><br></code></pre></td></tr></table></figure><p>源码的 <code>dropout=0.1</code> 对应约 10% 的置零概率；每次实际置零数量会波动，并不保证精确丢掉 10%。Dropout 不会删除 token 或缩短张量，<code>[B,L,D]</code> 的形状保持不变。</p><p><code>model.train()</code> 与 <code>model.eval()</code> 会递归切换已注册子模块的模式；新建模块默认处于训练模式。<code>eval()</code> 调整 Dropout 等模块的行为，但<strong>不关闭自动求导</strong>；推理时通常同时使用评估模式和 <code>torch.no_grad()</code>。规则见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.nn.Dropout.html">Dropout 文档</a>。</p><h3 id="3-前馈模块里每一行如何连接？"><a href="#3-前馈模块里每一行如何连接？" class="headerlink" title="3. 前馈模块里每一行如何连接？"></a>3. 前馈模块里每一行如何连接？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-variable language_">self</span>.linear1 = nn.Linear(d_model, d_ff)   <span class="hljs-comment"># 创建 D → d_ff 的可训练映射</span><br><span class="hljs-variable language_">self</span>.dropout = nn.Dropout(p=dropout)     <span class="hljs-comment"># 创建丢弃规则，没有可训练权重</span><br><span class="hljs-variable language_">self</span>.linear2 = nn.Linear(d_ff, d_model)   <span class="hljs-comment"># 创建 d_ff → D 的可训练映射</span><br><span class="hljs-variable language_">self</span>.relu = nn.ReLU()                   <span class="hljs-comment"># 创建非线性规则，没有可训练权重</span><br></code></pre></td></tr></table></figure><p>这些语句在 <code>__init__</code> 中执行一次，真正的运行顺序由 <code>forward</code> 决定。即使 <code>self.relu</code> 写在初始化的最后一行，前向计算仍然会按 <code>linear1 → relu → dropout → linear2</code> 执行。</p><p><code>x = self.linear1(x)</code> 只是让局部变量名 <code>x</code> 指向这一步的新结果，不代表用同样的数值覆盖模型参数。参数存在线性层内部，由后续优化器更新。</p><h2 id="七、EncoderLayer：把两个子层组装起来"><a href="#七、EncoderLayer：把两个子层组装起来" class="headerlink" title="七、EncoderLayer：把两个子层组装起来"></a>七、EncoderLayer：把两个子层组装起来</h2><h3 id="1-先在构造函数中连接依赖"><a href="#1-先在构造函数中连接依赖" class="headerlink" title="1. 先在构造函数中连接依赖"></a>1. 先在构造函数中连接依赖</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-variable language_">self</span>.self_attn = MultiHeadAttention(d_model, num_heads)<br><span class="hljs-variable language_">self</span>.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout)<br><span class="hljs-variable language_">self</span>.norm1 = nn.LayerNorm(d_model)<br><span class="hljs-variable language_">self</span>.norm2 = nn.LayerNorm(d_model)<br></code></pre></td></tr></table></figure><p>原代码创建注意力和前馈模块时没有传入必需参数，实例化 <code>EncoderLayer</code> 就会抛出 <code>TypeError</code>。修正后，编码器层把自己的维度配置传给两个子模块。</p><h3 id="2-顺着-forward-读一遍"><a href="#2-顺着-forward-读一遍" class="headerlink" title="2. 顺着 forward 读一遍"></a>2. 顺着 forward 读一遍</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python">attn_output = <span class="hljs-variable language_">self</span>.self_attn(x, x, x, mask)<br>x = <span class="hljs-variable language_">self</span>.norm1(x + <span class="hljs-variable language_">self</span>.dropout(attn_output))<br><br>ff_output = <span class="hljs-variable language_">self</span>.feed_forward(x)<br>x = <span class="hljs-variable language_">self</span>.norm2(x + <span class="hljs-variable language_">self</span>.dropout(ff_output))<br><span class="hljs-keyword">return</span> x<br></code></pre></td></tr></table></figure><p>第一步，自注意力让源序列中的每个位置根据允许访问的其他位置更新信息。</p><p>第二步，<code>x + ...</code> 保留输入表示，并叠加子层计算出的更新。这就是残差连接，也为梯度提供了直接经过加法节点传播的路径。</p><p>第三步，<code>LayerNorm(d_model)</code> 对每个 token 的最后一个特征维进行归一化，再应用可学习的缩放和偏移。它不会把整句话或整个 batch 混在一起统计。参见 <a href="https://docs.pytorch.org/docs/2.9/generated/torch.nn.LayerNorm.html">LayerNorm 官方说明</a>。</p><p>之后再经过前馈网络、另一次残差相加与归一化，输出形状仍为 <code>[B, Ls, D]</code>。</p><p>这份代码采用 <strong>Post-LN</strong>：</p><p>$$<br>x_{next}&#x3D;\operatorname{LayerNorm}(x+\operatorname{Dropout}(\operatorname{Sublayer}(x)))<br>$$</p><p>归一化位于残差相加之后。阅读其他实现时可能遇到先归一化再进入子层的 Pre-LN，需要按实际调用顺序区分。</p><h3 id="3-nn-LayerNorm-d-model-的参数和计算"><a href="#3-nn-LayerNorm-d-model-的参数和计算" class="headerlink" title="3. nn.LayerNorm(d_model) 的参数和计算"></a>3. <code>nn.LayerNorm(d_model)</code> 的参数和计算</h3><p><code>nn.LayerNorm(4)</code> 要求输入最后一轴长度为 4，会分别对每个四维特征向量计算均值、方差：</p><p>$$<br>y&#x3D;\gamma\odot\frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}+\beta<br>$$</p><p><code>epsilon</code> 防止分母为零。默认可训练的 <code>gamma</code> 和 <code>beta</code> 分别初始化为 1 和 0，模型可以学习归一化后的缩放和偏移。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><br>norm = nn.LayerNorm(<span class="hljs-number">2</span>)<br>x = torch.tensor([[[<span class="hljs-number">1.0</span>, <span class="hljs-number">3.0</span>],<br>                   [<span class="hljs-number">10.0</span>, <span class="hljs-number">14.0</span>]]])       <span class="hljs-comment"># [B,L,D] = [1,2,2]</span><br>y = norm(x)<br><span class="hljs-built_in">print</span>(y)<br><span class="hljs-comment"># 约 tensor([[[-1., 1.], [-1., 1.]]])</span><br></code></pre></td></tr></table></figure><p>第一个 token <code>[1,3]</code> 的均值是 2、方差是 1，忽略很小的 epsilon 后得到 <code>[-1,1]</code>。第二个 token <code>[10,14]</code> 自己计算均值 12、方差 4，也得到 <code>[-1,1]</code>。方差按元素数 D 作分母，与默认使用 D-1 的某些样本方差写法不同。</p><p>两个 token 的统计量分别计算，batch 之间也分别计算。训练后由于 <code>gamma</code>、<code>beta</code> 可能变化，最终输出不必始终保持零均值、单位方差。</p><h3 id="4-把嵌套的残差语句按执行顺序拆开"><a href="#4-把嵌套的残差语句按执行顺序拆开" class="headerlink" title="4. 把嵌套的残差语句按执行顺序拆开"></a>4. 把嵌套的残差语句按执行顺序拆开</h3><p>源码中的这一行包含三次操作：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">x = <span class="hljs-variable language_">self</span>.norm1(x + <span class="hljs-variable language_">self</span>.dropout(attn_output))<br></code></pre></td></tr></table></figure><p>可以展开为下面的等价代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python">update = <span class="hljs-variable language_">self</span>.dropout(attn_output)   <span class="hljs-comment"># ① 处理注意力给出的更新</span><br>residual_sum = x + update            <span class="hljs-comment"># ② 输入与更新逐元素相加</span><br>x = <span class="hljs-variable language_">self</span>.norm1(residual_sum)         <span class="hljs-comment"># ③ 对相加结果做 LayerNorm</span><br></code></pre></td></tr></table></figure><p>三者在当前实现中都是 <code>[B,L,D]</code>，因此可以逐元素相加。残差相加既没有沿序列求和，也没有把两个向量拼成 <code>2D</code> 维；每个位置、每个特征都保留一个相加后的值。</p><p>随后 <code>ff_output = self.feed_forward(x)</code> 接收到的是<strong>已经过第一次残差和归一化的 x</strong>。它不是重新读取原始词嵌入。</p><p><code>norm1</code> 和 <code>norm2</code> 是两个独立的 LayerNorm 实例，各自拥有自己的缩放、偏移参数。<code>self.dropout</code> 虽然同一个实例被调用多次，每次训练态调用都会执行随机采样，并不因此共享固定的置零位置。</p><h2 id="八、DecoderLayer：为什么需要两种注意力？"><a href="#八、DecoderLayer：为什么需要两种注意力？" class="headerlink" title="八、DecoderLayer：为什么需要两种注意力？"></a>八、DecoderLayer：为什么需要两种注意力？</h2><p>解码器依次经过三个子层，每个子层后面都有残差连接和 LayerNorm。</p><h3 id="1-掩码自注意力：读取目标端上下文"><a href="#1-掩码自注意力：读取目标端上下文" class="headerlink" title="1. 掩码自注意力：读取目标端上下文"></a>1. 掩码自注意力：读取目标端上下文</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs python">attn_output = <span class="hljs-variable language_">self</span>.self_attn(x, x, x, tgt_mask)<br>x = <span class="hljs-variable language_">self</span>.norm1(x + <span class="hljs-variable language_">self</span>.dropout(attn_output))<br></code></pre></td></tr></table></figure><p>Q、K、V 的输入都来自目标端当前表示。因果掩码限定当前位置能读取哪些目标输入。</p><p>形状是 <code>[B, Lt, D] → [B, Lt, D]</code>，中间注意力矩阵为 <code>[B, H, Lt, Lt]</code>。</p><h3 id="2-交叉注意力：根据目标端需要读取源信息"><a href="#2-交叉注意力：根据目标端需要读取源信息" class="headerlink" title="2. 交叉注意力：根据目标端需要读取源信息"></a>2. 交叉注意力：根据目标端需要读取源信息</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">cross_attn_output = <span class="hljs-variable language_">self</span>.cross_attn(<br>    x, encoder_output, encoder_output, src_mask<br>)<br>x = <span class="hljs-variable language_">self</span>.norm2(x + <span class="hljs-variable language_">self</span>.dropout(cross_attn_output))<br></code></pre></td></tr></table></figure><p>这里参数来源发生了变化：</p><table><thead><tr><th>角色</th><th>投影前的输入</th><th>职责</th></tr></thead><tbody><tr><td>Q</td><td>目标端当前表示 <code>x</code></td><td>当前目标位置需要什么信息</td></tr><tr><td>K</td><td><code>encoder_output</code></td><td>源位置提供哪些匹配线索</td></tr><tr><td>V</td><td><code>encoder_output</code></td><td>源位置提供哪些信息内容</td></tr></tbody></table><p>K 和 V 输入相同，但经过各自独立的线性层。目标长度和源长度可以不同：注意力矩阵是 <code>[B, H, Lt, Ls]</code>，最终输出仍然是 <code>[B, Lt, D]</code>。</p><p>源码中的 <code>src_mask</code> 应当能适配交叉注意力。通常传入 <code>[B, 1, 1, Ls]</code> 的源 padding mask，便可同时广播到编码器的 <code>[B,H,Ls,Ls]</code> 和解码器的 <code>[B,H,Lt,Ls]</code>。</p><h3 id="3-前馈网络：加工融合后的目标表示"><a href="#3-前馈网络：加工融合后的目标表示" class="headerlink" title="3. 前馈网络：加工融合后的目标表示"></a>3. 前馈网络：加工融合后的目标表示</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python">ff_output = <span class="hljs-variable language_">self</span>.feed_forward(x)<br>x = <span class="hljs-variable language_">self</span>.norm3(x + <span class="hljs-variable language_">self</span>.dropout(ff_output))<br><span class="hljs-keyword">return</span> x<br></code></pre></td></tr></table></figure><p>至此，每个目标位置已经结合了允许访问的目标上下文与源序列信息，但输出仍是隐藏向量。还需要接上词表线性层，才能获得预测各个 token 的 logits。</p><h3 id="4-一个-DecoderLayer-调用怎样对应到形参？"><a href="#4-一个-DecoderLayer-调用怎样对应到形参？" class="headerlink" title="4. 一个 DecoderLayer 调用怎样对应到形参？"></a>4. 一个 DecoderLayer 调用怎样对应到形参？</h3><p>外部调用：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">hidden = decoder(tgt_x, memory, src_mask, tgt_mask)<br></code></pre></td></tr></table></figure><p>方法定义：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">forward</span>(<span class="hljs-params">self, x, encoder_output, src_mask=<span class="hljs-literal">None</span>, tgt_mask=<span class="hljs-literal">None</span></span>):<br>    ...<br></code></pre></td></tr></table></figure><p>PyTorch 会把当前 decoder 实例作为 <code>self</code>，其余位置参数依次对应 <code>x=tgt_x</code>、<code>encoder_output=memory</code>。<code>src_mask</code> 用于交叉注意力，<code>tgt_mask</code> 用于目标端自注意力。</p><p>初学时可以使用关键字参数让调用更易读：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python">hidden = decoder(<br>    x=tgt_x,<br>    encoder_output=memory,<br>    src_mask=src_mask,<br>    tgt_mask=tgt_mask,<br>)<br></code></pre></td></tr></table></figure><p><code>encoder_output</code> 在这里提供数据来源，梯度仍然可以通过交叉注意力传回编码器。正常训练无需把它 <code>detach()</code>；<code>detach()</code> 会切断该张量到前面计算过程的梯度连接。</p><p><code>self.self_attn</code> 和 <code>self.cross_attn</code> 是两个分别创建的注意力模块，有各自独立的 Q&#x2F;K&#x2F;V&#x2F;输出投影参数。即使都使用 <code>MultiHeadAttention</code> 这个类，参数也不会自动共享。</p><h2 id="九、最小运行示例：把源码中的层接起来"><a href="#九、最小运行示例：把源码中的层接起来" class="headerlink" title="九、最小运行示例：把源码中的层接起来"></a>九、最小运行示例：把源码中的层接起来</h2><p>下面示例可以放在 <code>Transformer.py</code> 同目录的另一个 Python 文件中运行。它组装一层编码器、一层解码器以及嵌入和输出层，展示训练时一次前向计算与反向传播。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> math<br><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><span class="hljs-keyword">from</span> Transformer <span class="hljs-keyword">import</span> PositionalEncoding, EncoderLayer, DecoderLayer<br><br>torch.manual_seed(<span class="hljs-number">42</span>)<br>device = torch.device(<span class="hljs-string">&quot;cpu&quot;</span>)<br>pad_id, vocab_size = <span class="hljs-number">0</span>, <span class="hljs-number">32</span><br>d_model, num_heads, d_ff = <span class="hljs-number">12</span>, <span class="hljs-number">3</span>, <span class="hljs-number">24</span><br><br>src_embedding = nn.Embedding(vocab_size, d_model, padding_idx=pad_id).to(device)<br>tgt_embedding = nn.Embedding(vocab_size, d_model, padding_idx=pad_id).to(device)<br>position = PositionalEncoding(d_model, dropout=<span class="hljs-number">0.0</span>, max_len=<span class="hljs-number">32</span>).to(device)<br>encoder = EncoderLayer(d_model, num_heads, d_ff, dropout=<span class="hljs-number">0.0</span>).to(device)<br>decoder = DecoderLayer(d_model, num_heads, d_ff, dropout=<span class="hljs-number">0.0</span>).to(device)<br>lm_head = nn.Linear(d_model, vocab_size).to(device)<br><br><span class="hljs-comment"># 0=PAD，1=BOS，2=EOS；其余数字代表词表中的其他 token。</span><br>src_ids = torch.tensor([<br>    [<span class="hljs-number">4</span>, <span class="hljs-number">5</span>, <span class="hljs-number">6</span>, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>],<br>    [<span class="hljs-number">7</span>, <span class="hljs-number">8</span>, <span class="hljs-number">9</span>, <span class="hljs-number">10</span>, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>],<br>], device=device)<br>tgt_full = torch.tensor([<br>    [<span class="hljs-number">1</span>, <span class="hljs-number">11</span>, <span class="hljs-number">12</span>, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>],<br>    [<span class="hljs-number">1</span>, <span class="hljs-number">13</span>, <span class="hljs-number">14</span>, <span class="hljs-number">15</span>, <span class="hljs-number">2</span>],<br>], device=device)<br><br><span class="hljs-comment"># 右移输入和标签：输入前缀，预测下一个 token。</span><br>tgt_input = tgt_full[:, :-<span class="hljs-number">1</span>]  <span class="hljs-comment"># [2, 4]</span><br>labels = tgt_full[:, <span class="hljs-number">1</span>:]      <span class="hljs-comment"># [2, 4]</span><br><br>src_mask = (src_ids != pad_id)[:, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :]  <span class="hljs-comment"># [2, 1, 1, 6]</span><br>Lt = tgt_input.size(<span class="hljs-number">1</span>)<br>causal = torch.ones(Lt, Lt, dtype=torch.<span class="hljs-built_in">bool</span>, device=device).tril()<br>tgt_mask = (<br>    (tgt_input != pad_id)[:, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :]<br>    &amp; causal[<span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, :, :]<br>)  <span class="hljs-comment"># [2, 1, 4, 4]</span><br><br>src_x = position(src_embedding(src_ids) * math.sqrt(d_model))<br>tgt_x = position(tgt_embedding(tgt_input) * math.sqrt(d_model))<br>memory = encoder(src_x, src_mask)                      <span class="hljs-comment"># [2, 6, 12]</span><br>hidden = decoder(tgt_x, memory, src_mask, tgt_mask)     <span class="hljs-comment"># [2, 4, 12]</span><br>logits = lm_head(hidden)                              <span class="hljs-comment"># [2, 4, 32]</span><br><br>loss = nn.CrossEntropyLoss(ignore_index=pad_id)(<br>    logits.reshape(-<span class="hljs-number">1</span>, vocab_size),<br>    labels.reshape(-<span class="hljs-number">1</span>),<br>)<br>loss.backward()<br><br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;memory:&quot;</span>, <span class="hljs-built_in">tuple</span>(memory.shape))<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;hidden:&quot;</span>, <span class="hljs-built_in">tuple</span>(hidden.shape))<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;logits:&quot;</span>, <span class="hljs-built_in">tuple</span>(logits.shape))<br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;loss is finite:&quot;</span>, <span class="hljs-built_in">bool</span>(torch.isfinite(loss)))<br></code></pre></td></tr></table></figure><p>这里 <code>CrossEntropyLoss</code> 直接接收 logits，损失前不应再手动 Softmax。示例只做一次前后向计算，没有优化器更新，所以不能据此判断模型已经学会翻译或生成。</p><p>训练时可以通过右移输入与因果掩码并行计算各个位置；生成时则需要从 BOS 开始，每次根据当前前缀预测下一个 token，把它追加到输入，再继续计算，直到 EOS 或长度上限。</p><h3 id="1-nn-Embedding-vocab-size-d-model-padding-idx-：按-ID-查向量"><a href="#1-nn-Embedding-vocab-size-d-model-padding-idx-：按-ID-查向量" class="headerlink" title="1. nn.Embedding(vocab_size,d_model,padding_idx)：按 ID 查向量"></a>1. <code>nn.Embedding(vocab_size,d_model,padding_idx)</code>：按 ID 查向量</h3><p>Embedding 内部是一张 <code>[vocab_size,d_model]</code> 的可训练表。输入的每个整数 ID 取出表中的一行，所以 <code>[B,L]</code> 的 ID 会得到 <code>[B,L,D]</code> 的向量。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><span class="hljs-keyword">import</span> torch.nn <span class="hljs-keyword">as</span> nn<br><br>embedding = nn.Embedding(<span class="hljs-number">5</span>, <span class="hljs-number">3</span>, padding_idx=<span class="hljs-number">0</span>)<br>ids = torch.tensor([[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>]])          <span class="hljs-comment"># 整数 ID，形状 [1,3]</span><br>vectors = embedding(ids)<br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(embedding.weight.shape))    <span class="hljs-comment"># (5,3)</span><br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(vectors.shape))             <span class="hljs-comment"># (1,3,3)</span><br><span class="hljs-built_in">print</span>(vectors[<span class="hljs-number">0</span>, <span class="hljs-number">2</span>].tolist())            <span class="hljs-comment"># 新建模块的 PAD 行默认 [0,0,0]</span><br></code></pre></td></tr></table></figure><p><code>padding_idx=0</code> 指定第 0 行为填充向量：它默认初始化为零，且通过该嵌入查找获得的梯度不会更新这一行。加入位置编码后，PAD 对应位置的向量仍然可以变成非零，因此还需要注意力 mask 和损失中的 PAD 忽略规则。参见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.nn.Embedding.html">Embedding 文档</a>。</p><p><code>src_embedding(src_ids) * math.sqrt(d_model)</code> 先查词向量再乘一个标量，最后交给 <code>position(...)</code> 加位置编码。缩放词嵌入与注意力中除以 <code>sqrt(d_k)</code> 发生在不同位置，处理的也不是同一个张量。</p><h3 id="2-1-与-1-：输入、标签如何对齐？"><a href="#2-1-与-1-：输入、标签如何对齐？" class="headerlink" title="2. [:, :-1] 与 [:, 1:]：输入、标签如何对齐？"></a>2. <code>[:, :-1]</code> 与 <code>[:, 1:]</code>：输入、标签如何对齐？</h3><p>假设一行目标序列为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">tgt_full       [BOS, A, B, EOS, PAD]<br>tgt_full[:,:-1] [BOS, A, B, EOS]    输入：去掉最后一项<br>tgt_full[:,1:]  [A,   B, EOS, PAD]  标签：去掉第一项<br></code></pre></td></tr></table></figure><p><code>:-1</code> 的结束位置是最后一个元素，因此会排除它；这里的 <code>-1</code> 表示最后一个元素的下标，与前面讲过的轴下标、自动推算长度还要按语境区分。</p><p>每个输入位置都预测它后面一项：看到 BOS 预测 A，看到 <code>[BOS,A]</code> 预测 B。最后位置对应 PAD 标签，会由损失函数忽略。</p><h3 id="3-reshape-1-vocab-size-：把每个位置当成一个分类样本"><a href="#3-reshape-1-vocab-size-：把每个位置当成一个分类样本" class="headerlink" title="3. reshape(-1,vocab_size)：把每个位置当成一个分类样本"></a>3. <code>reshape(-1,vocab_size)</code>：把每个位置当成一个分类样本</h3><p>原来的 <code>logits[B,Lt,V]</code> 为每个样本、每个位置保存 V 个词表得分。交叉熵这里采用 <code>[N,V]</code> 的调用方式，因此把前两个轴合并，其中 <code>N=B×Lt</code>。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>logits = torch.zeros(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>, <span class="hljs-number">32</span>)<br>labels = torch.tensor([[<span class="hljs-number">11</span>, <span class="hljs-number">12</span>, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>], [<span class="hljs-number">13</span>, <span class="hljs-number">14</span>, <span class="hljs-number">15</span>, <span class="hljs-number">2</span>]])<br><span class="hljs-built_in">print</span>(<span class="hljs-built_in">tuple</span>(logits.reshape(-<span class="hljs-number">1</span>, <span class="hljs-number">32</span>).shape))   <span class="hljs-comment"># (8,32)</span><br><span class="hljs-built_in">print</span>(labels.reshape(-<span class="hljs-number">1</span>).tolist())          <span class="hljs-comment"># [11,12,2,0,13,14,15,2]</span><br></code></pre></td></tr></table></figure><p>两个张量按相同的样本、位置顺序展开，保证每行词表分数仍然对应正确的标签。<code>-1</code> 让 PyTorch 自动算出这里的 N 为 8。</p><h3 id="4-nn-CrossEntropyLoss-：为什么连续出现两对括号？"><a href="#4-nn-CrossEntropyLoss-：为什么连续出现两对括号？" class="headerlink" title="4. nn.CrossEntropyLoss(...)(...)：为什么连续出现两对括号？"></a>4. <code>nn.CrossEntropyLoss(...)(...)</code>：为什么连续出现两对括号？</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">loss = nn.CrossEntropyLoss(ignore_index=pad_id)(<br>    logits.reshape(-<span class="hljs-number">1</span>, vocab_size),<br>    labels.reshape(-<span class="hljs-number">1</span>),<br>)<br></code></pre></td></tr></table></figure><p>第一对括号创建一个损失模块，第二对括号调用它。可以拆开写成：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">criterion = nn.CrossEntropyLoss(ignore_index=pad_id)<br>flat_logits = logits.reshape(-<span class="hljs-number">1</span>, vocab_size)<br>flat_labels = labels.reshape(-<span class="hljs-number">1</span>)<br>loss = criterion(flat_logits, flat_labels)<br></code></pre></td></tr></table></figure><p><code>flat_logits</code> 是浮点得分，<code>flat_labels</code> 是各位置正确 token 的整数 ID。损失越小，通常表示模型给正确标签分配的概率越高。该接口内部完成稳定的对数概率计算，传入前不用自己 Softmax。</p><p><code>ignore_index=pad_id</code> 表示标签为 PAD 的位置不计入损失；默认求均值时，也不计入有效位置数量。本示例有正常有效标签；若一个 batch 所有标签都被忽略，平均损失需要另行处理。参见 <a href="https://docs.pytorch.org/docs/2.10/generated/torch.nn.CrossEntropyLoss.html">CrossEntropyLoss 文档</a>。</p><h3 id="5-loss-backward-：算梯度；参数更新由优化器执行"><a href="#5-loss-backward-：算梯度；参数更新由优化器执行" class="headerlink" title="5. loss.backward()：算梯度；参数更新由优化器执行"></a>5. <code>loss.backward()</code>：算梯度；参数更新由优化器执行</h3><p>前向计算会建立计算图；<code>loss.backward()</code> 沿图反向应用链式法则，把参数梯度写入各参数的 <code>.grad</code>。它本身不执行优化器更新。</p><p>下面用一个标量把区别看清楚：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> torch<br><br>w = torch.tensor(<span class="hljs-number">2.0</span>, requires_grad=<span class="hljs-literal">True</span>)<br>loss = (w - <span class="hljs-number">5.0</span>) ** <span class="hljs-number">2</span><br>loss.backward()<br><span class="hljs-built_in">print</span>(w.item())         <span class="hljs-comment"># 2.0：backward 后数值仍然为 2</span><br><span class="hljs-built_in">print</span>(w.grad.item())    <span class="hljs-comment"># -6.0：导数 2*(2-5)</span><br><br>optimizer = torch.optim.SGD([w], lr=<span class="hljs-number">0.1</span>)<br>optimizer.step()<br><span class="hljs-built_in">print</span>(w.item())         <span class="hljs-comment"># 约 2.6，即 2 - 0.1*(-6)</span><br>optimizer.zero_grad()   <span class="hljs-comment"># 清除本轮梯度，避免下轮意外累积</span><br></code></pre></td></tr></table></figure><p><code>requires_grad=True</code> 表示需要为这个张量记录梯度，模型的可训练参数通常已经启用了它。<code>.item()</code> 从只有一个元素的张量中取出 Python 数值，适合记录标量；多元素张量不能直接调用它得到一个整体数值。</p><p>实际训练通常是“清梯度 → 前向 → 损失 → backward → optimizer.step”。前面的完整示例只演示到 backward；若要训练整套模型，优化器必须包含嵌入、编码器、解码器和输出层等所有需要训练的参数。</p><p><code>torch.manual_seed(42)</code> 设置随机数种子，便于重复观察示例；它不能单独保证不同设备、版本下所有运算逐位相同。<code>torch.isfinite(loss)</code> 检查是否既不是 NaN 也不是正负无穷，单元素结果可转成 Python 的 <code>bool</code>。</p><h3 id="6-parameters-、state-dict-、train-、eval-放在一起理解"><a href="#6-parameters-、state-dict-、train-、eval-放在一起理解" class="headerlink" title="6. parameters()、state_dict()、train()、eval() 放在一起理解"></a>6. <code>parameters()</code>、<code>state_dict()</code>、<code>train()</code>、<code>eval()</code> 放在一起理解</h3><table><thead><tr><th>接口</th><th>作用</th><th>常见使用位置</th></tr></thead><tbody><tr><td><code>model.parameters()</code></td><td>迭代模型及注册子模块的参数</td><td>交给优化器</td></tr><tr><td><code>model.state_dict()</code></td><td>获取参数与默认持久 buffer 的状态映射</td><td>保存模型状态；固定位置编码 pe 也在其中</td></tr><tr><td><code>model.to(device)</code></td><td>移动参数和 buffer</td><td>前向计算前，与输入设备保持一致</td></tr><tr><td><code>model.train()</code></td><td>设置训练模式</td><td>训练阶段启用 Dropout 等行为</td></tr><tr><td><code>model.eval()</code></td><td>设置评估模式</td><td>验证和生成阶段关闭 Dropout 等行为</td></tr><tr><td><code>with torch.no_grad():</code></td><td>临时关闭梯度记录</td><td>推理或手动给参数写入演示数值</td></tr></tbody></table><p>多个子层被赋给 <code>self.xxx</code> 后，父模块才能递归管理它们。这也是自定义模型继承 <code>nn.Module</code>、先执行 <code>super().__init__()</code>、在初始化中创建子层的重要原因。</p><h2 id="十、这次检查修正了什么？"><a href="#十、这次检查修正了什么？" class="headerlink" title="十、这次检查修正了什么？"></a>十、这次检查修正了什么？</h2><table><thead><tr><th>原代码问题</th><th>影响</th><th>修正</th></tr></thead><tbody><tr><td>只写了 buffer 注册注释</td><td><code>self.pe</code> 不存在</td><td>注册 <code>[1, max_len, D]</code> 的位置编码</td></tr><tr><td>奇数 <code>d_model</code> 的 cos 赋值列数不匹配</td><td>初始化位置编码失败</td><td>截取对应数量的频率系数</td></tr><tr><td>四维 Q&#x2F;K 使用 <code>torch.bmm</code></td><td>多头注意力运行报错</td><td>改为 <code>torch.matmul</code></td></tr><tr><td>合头函数按三维解包</td><td>变量解包报错</td><td>正确读取 <code>[B,H,L,d_k]</code></td></tr><tr><td>编码器、解码器创建子模块时缺少参数</td><td>层无法实例化</td><td>传入维度、头数和 Dropout 配置</td></tr><tr><td>三维批次掩码没有补头维</td><td>广播报错或按错误维度广播</td><td>明确约定并转换 2D&#x2F;3D&#x2F;4D 掩码</td></tr><tr><td>使用 <code>-1e9</code> 屏蔽</td><td>FP16 表示范围不足</td><td>使用 <code>-inf</code> 并处理全屏蔽行</td></tr><tr><td>整行全屏蔽后没有额外处理</td><td>可能产生错误汇总或 NaN</td><td>把空行注意力权重显式清零</td></tr></tbody></table><p>另外增加了头数、维度和位置编码长度的基本检查，便于在输入配置错误时直接定位原因。</p><h3 id="修正版的实际验证"><a href="#修正版的实际验证" class="headerlink" title="修正版的实际验证"></a>修正版的实际验证</h3><p>2026-10-29 使用本机 <code>study</code> 环境中的 PyTorch <code>2.10.0+cu126</code> 完成了 7 组验证：</p><ul><li>位置编码的奇偶维度、位置 0 的已知数值、buffer 注册和长度限制。</li><li>分头与合头互为逆操作。</li><li>无掩码以及二维、三维、四维掩码下，注意力输出与 PyTorch <code>scaled_dot_product_attention</code> 对照一致。</li><li>FP32、FP16、BF16 下，全屏蔽行汇总为零且梯度有限。</li><li>源、目标长度不同时，编码器和解码器输出形状正确，所有参数均获得有限梯度。</li><li>改动目标端未来位置不影响较早位置的输出；改动被屏蔽的源 PAD 位置不影响目标端输出。</li><li>GPU 上的位置编码随模型移动，CPU 掩码可适配 GPU 注意力计算。</li></ul><p>上节完整示例也已运行，得到 <code>memory=(2,6,12)</code>、<code>hidden=(2,4,12)</code>、<code>logits=(2,4,32)</code>，损失有限且反向传播成功。这些检查验证了核心计算与修复项；模型的学习效果还需要通过实际数据训练和评估确认。</p><h2 id="十一、核心层之外还需要什么？"><a href="#十一、核心层之外还需要什么？" class="headerlink" title="十一、核心层之外还需要什么？"></a>十一、核心层之外还需要什么？</h2><p>若要把这份教学代码扩展为完整的序列到序列模型，还需要：</p><ol><li><strong>词表和分词器</strong>：把文本变成 token IDs，并定义 PAD、BOS、EOS。</li><li><strong>源端与目标端 Embedding</strong>：把 IDs 变成 <code>d_model</code> 维向量，再加入位置编码。</li><li><strong>多层编码器、解码器</strong>：可用 <code>nn.ModuleList</code> 注册多层，并在 <code>forward</code> 中逐层调用。通常每层应有独立参数。</li><li><strong>词表输出层</strong>：把 <code>[B,Lt,D]</code> 映射为 <code>[B,Lt,vocab_size]</code> 的 logits。</li><li><strong>训练流程</strong>：右移目标输入、构造掩码、忽略 PAD 标签、更新参数并评估。</li><li><strong>生成循环</strong>：切换到评估模式，从已有前缀逐 token 扩展输出。</li></ol><p>当前注意力模块没有对注意力权重单独应用 Dropout；位置编码、前馈网络和残差支路有 Dropout。它适合理解计算过程，若希望逐项复现某个参考实现，还需对齐这些配置。</p><h2 id="十二、读懂源码时应抓住的主线"><a href="#十二、读懂源码时应抓住的主线" class="headerlink" title="十二、读懂源码时应抓住的主线"></a>十二、读懂源码时应抓住的主线</h2><p>这份实现的接口围绕 <code>[B,L,D]</code> 展开：位置编码保留形状，注意力临时拆出头维进行跨位置的信息汇总，前馈网络在每个位置变换特征，残差和归一化则把各个子层连接起来。</p><p>编码器的自注意力从源序列内部获取信息；解码器先读取受因果掩码约束的目标上下文，再通过交叉注意力读取编码器结果。最后再把目标端隐藏向量投影到词表，才得到用于预测下一个 token 的分数。</p><p>以后阅读新的 Transformer 实现，可以反复核对四件事：<strong>Q&#x2F;K&#x2F;V 分别来自哪里、张量当前是什么形状、mask 中什么值代表允许、残差和归一化发生在什么顺序</strong>。把这四条线跟清楚，大部分核心计算就能与源码逐句对应。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>苏大机试第三批-回忆版</title>
      <link>https://ckxkx.github.io/2026/10/30/%E8%8B%8F%E5%A4%A7%E6%9C%BA%E8%AF%95%E7%AC%AC%E4%B8%89%E6%89%B9-%E5%9B%9E%E5%BF%86%E7%89%88/</link>
      <description>
        <![CDATA[<h2 id="大致题型"><a href="#大致题型" class="headerlink" title="大致题型"></a>大致题型</h2><p><strong>6 个程序设计题 +2 个数据结构题 +2 个算法题</strong></p>
<p>具体的形式是给你一个]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E4%BF%9D%E7%A0%94/">保研</category>
      <category domain="https://ckxkx.github.io/tags/%E4%BF%9D%E7%A0%94/">保研</category>
      <category domain="https://ckxkx.github.io/tags/%E8%8B%8F%E5%A4%A7%E6%9C%BA%E8%AF%95/">苏大机试</category>
      <category domain="https://ckxkx.github.io/tags/%E6%9C%BA%E8%AF%95/">机试</category>
      <pubDate>Fri, 30 Oct 2026 04:25:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="大致题型"><a href="#大致题型" class="headerlink" title="大致题型"></a>大致题型</h2><p><strong>6 个程序设计题 +2 个数据结构题 +2 个算法题</strong></p><p>具体的形式是给你一个 pdf 文件，里面有题目，然后给你一个 sample.cpp 文件，里面把 10 个题的函数声明好，把主体空出来让你填，十个函数一个 main 函数（<strong>！main 函数中是空的，所以想要测试函数逻辑对不对，需要自己写测试用例</strong>）（func1  … func10）26 年机试第一批和第二批都有原题（力扣热题 100，但是第三批没有原题🤷🤷🤷）</p><p>例如：<br>string func2(int n){<br>return “”；<br>}</p><h3 id="1-学生成绩评级问题（-if-else-问题）"><a href="#1-学生成绩评级问题（-if-else-问题）" class="headerlink" title="1.学生成绩评级问题（ if -else 问题）"></a>1.学生成绩评级问题（ if -else 问题）</h3><p>题目给定四个参数，违纪 violation ，奖学金 award  ，成绩 score ，出勤 attend ，计算学生的评级（A+ -&gt; D），评级规则大概是：第一看违纪，如果有违纪直接 D，否则第二看奖学金（一等奖二等奖三等奖无），选定奖学金后判断 成绩和出勤情况，比如：score&gt;&#x3D;80 &amp;&amp; attend &gt;&#x3D;18，输出 A+，以此类推。</p><h3 id="2-忠实数判断：一个数是-7-的幂或者-7-的幂之和"><a href="#2-忠实数判断：一个数是-7-的幂或者-7-的幂之和" class="headerlink" title="2.忠实数判断：一个数是 7 的幂或者 7 的幂之和"></a>2.忠实数判断：一个数是 7 的幂或者 7 的幂之和</h3><p>比如 57&#x3D;1+7+49，所以 57 是 7 的幂，输出 Yes，比如 3 不是 7 的幂，输出 NO。</p><h3 id="3-给定一个步数-k-和一对坐标-x-y-，计算-k-步之后，遍历过的二维数组中的值的总和（bfs）"><a href="#3-给定一个步数-k-和一对坐标-x-y-，计算-k-步之后，遍历过的二维数组中的值的总和（bfs）" class="headerlink" title="3.给定一个步数 k 和一对坐标 (x, y)，计算 k 步之后，遍历过的二维数组中的值的总和（bfs）"></a>3.给定一个步数 k 和一对坐标 (x, y)，计算 k 步之后，遍历过的二维数组中的值的总和（bfs）</h3><p>题目给定一个二维数组，每一个步长需要计算 <strong>上下左右（包括本身位置）</strong> 五个位置的值，但是不能重复计算。</p><h3 id="4-回文串的回文子串（以测试用例解释）"><a href="#4-回文串的回文子串（以测试用例解释）" class="headerlink" title="4.回文串的回文子串（以测试用例解释）"></a>4.回文串的回文子串（以测试用例解释）</h3><p>给定一个字符串 a1bcbbcb，其可以划分为 a1bcb 和 bcb 两个子串，</p><h3 id="5-找圆心（其实没太读懂题意）"><a href="#5-找圆心（其实没太读懂题意）" class="headerlink" title="5.找圆心（其实没太读懂题意）"></a>5.找圆心（其实没太读懂题意）</h3><p>题目给定一个坐标，给定一个半径，以该半径画圆，能覆盖最多点数的圆心坐标在哪里？（最优圆心）<br>样例 (2，-3），半径 r&#x3D;1 ：输出（5，5）</p><h3 id="6-题目给定一个二维网格，其中-代表可行，x-代表障碍物，在行走过程中，如果遇到障碍物或者边界就右转（右转也算一步），给定一个步数-K，计算-K-步之后，有过多少个不重复的格子。（bfs）"><a href="#6-题目给定一个二维网格，其中-代表可行，x-代表障碍物，在行走过程中，如果遇到障碍物或者边界就右转（右转也算一步），给定一个步数-K，计算-K-步之后，有过多少个不重复的格子。（bfs）" class="headerlink" title="6.题目给定一个二维网格，其中 . 代表可行，x 代表障碍物，在行走过程中，如果遇到障碍物或者边界就右转（右转也算一步），给定一个步数 K，计算 K 步之后，有过多少个不重复的格子。（bfs）"></a>6.题目给定一个二维网格，其中 . 代表可行，x 代表障碍物，在行走过程中，如果遇到障碍物或者边界就右转（右转也算一步），给定一个步数 K，计算 K 步之后，有过多少个不重复的格子。（bfs）</h3><p>注：7 8 题是数据结构题</p><h3 id="7-给定一个数-K，找到第-K-个节点的值-v1-和倒数第-K-个节点-v2-的值，找出在这两个值-v1v2-之间的值的个数。"><a href="#7-给定一个数-K，找到第-K-个节点的值-v1-和倒数第-K-个节点-v2-的值，找出在这两个值-v1v2-之间的值的个数。" class="headerlink" title="7.给定一个数 K，找到第 K 个节点的值 v1 和倒数第 K 个节点 v2 的值，找出在这两个值 v1v2 之间的值的个数。"></a>7.给定一个数 K，找到第 K 个节点的值 v1 和倒数第 K 个节点 v2 的值，找出在这两个值 v1v2 之间的值的个数。</h3><blockquote><ul><li>注意一个点：它提供给你的数据结构不是 LeetCode 上的，是类似大一 c++ 课程上的数据结构 LinkedList() 好好复习这些用法<br>（题目很好理解，但是难的是会用他给你的链表结构，不能更改原有结构，只能添加函数）</li></ul></blockquote><h3 id="8-给定两个节点，求两个节点高度差的绝对值"><a href="#8-给定两个节点，求两个节点高度差的绝对值" class="headerlink" title="8.给定两个节点，求两个节点高度差的绝对值"></a>8.给定两个节点，求两个节点高度差的绝对值</h3><p>注：算法题</p><h3 id="9-零件流水线加工问题：给定数组与数值-k，在保持数组元素相对顺序不变的前提下，将数组拆分为-k-份。计算每段子数组最大值乘以子数组长度的总和，求出该总和的最小值。"><a href="#9-零件流水线加工问题：给定数组与数值-k，在保持数组元素相对顺序不变的前提下，将数组拆分为-k-份。计算每段子数组最大值乘以子数组长度的总和，求出该总和的最小值。" class="headerlink" title="9.零件流水线加工问题：给定数组与数值 k，在保持数组元素相对顺序不变的前提下，将数组拆分为 k 份。计算每段子数组最大值乘以子数组长度的总和，求出该总和的最小值。"></a>9.零件流水线加工问题：给定数组与数值 k，在保持数组元素相对顺序不变的前提下，将数组拆分为 k 份。计算每段子数组最大值乘以子数组长度的总和，求出该总和的最小值。</h3><h3 id="10-网格图里从-S-走到-T，每个格子有传送带，走的方向和传送带方向不一致则产生-1-代价，问最小代价"><a href="#10-网格图里从-S-走到-T，每个格子有传送带，走的方向和传送带方向不一致则产生-1-代价，问最小代价" class="headerlink" title="10. 网格图里从 S 走到 T，每个格子有传送带，走的方向和传送带方向不一致则产生 1 代价，问最小代价"></a>10. 网格图里从 S 走到 T，每个格子有传送带，走的方向和传送带方向不一致则产生 1 代价，问最小代价</h3>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 的第一篇笔记</title>
      <link>https://ckxkx.github.io/2026/10/30/welcome/</link>
      <description>
        <![CDATA[<p>欢迎来到 <strong>chen</strong>。这里是朱会琛的个人网站，用来记录学习、技术与生活中的想法。</p>
<p>这是一个新的开始。希望在这里留下有价值的笔记，也记录一路上的探索与成长。</p>]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      <category domain="https://ckxkx.github.io/tags/%E5%BC%80%E5%A7%8B/">开始</category>
      <pubDate>Thu, 29 Oct 2026 16:36:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>欢迎来到 <strong>chen</strong>。这里是朱会琛的个人网站，用来记录学习、技术与生活中的想法。</p><p>这是一个新的开始。希望在这里留下有价值的笔记，也记录一路上的探索与成长。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>本科期间获奖经历</title>
      <link>https://ckxkx.github.io/2026/08/24/baoyan-awards/</link>
      <description>
        <![CDATA[<p>本科期间学科竞赛获奖整理：AIC 国赛一等奖、大唐杯、睿抗、蓝桥杯等国家级与省级证书一览。</p>
<h1 id="本科期间获奖经历"><a href="#本科期间获奖经历" class="headerlink" title="本科期间获奖经历"></a>本科期间获奖经历<]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E4%BF%9D%E7%A0%94/">保研</category>
      <category domain="https://ckxkx.github.io/tags/%E4%BF%9D%E7%A0%94/">保研</category>
      <pubDate>Mon, 24 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>本科期间学科竞赛获奖整理：AIC 国赛一等奖、大唐杯、睿抗、蓝桥杯等国家级与省级证书一览。</p><h1 id="本科期间获奖经历"><a href="#本科期间获奖经历" class="headerlink" title="本科期间获奖经历"></a>本科期间获奖经历</h1><p>这篇帖子按国家级和省级整理我本科期间的主要获奖，也当作给自己的一份阶段性总结。</p><h2 id="国家级奖项"><a href="#国家级奖项" class="headerlink" title="国家级奖项"></a>国家级奖项</h2><h3 id="2025-AIC-大赛-·-全国一等奖"><a href="#2025-AIC-大赛-·-全国一等奖" class="headerlink" title="2025 AIC 大赛 · 全国一等奖"></a>2025 AIC 大赛 · 全国一等奖</h3><p>人工智能方向的国家级赛事。从选题到答辩前后准备了一个多月，最终拿到全国一等奖，也是我本科阶段分量最重的一张证书。</p><p><img src="/media/2025%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD%E5%A4%A7%E8%B5%9B%E5%9B%BD%E5%AE%B6%E4%B8%80%E7%AD%89%E5%A5%96.webp" alt="2025 AIC 大赛 全国一等奖"></p><h3 id="大唐杯-全国总决赛-·-三等奖"><a href="#大唐杯-全国总决赛-·-三等奖" class="headerlink" title="大唐杯 全国总决赛 · 三等奖"></a>大唐杯 全国总决赛 · 三等奖</h3><p>大唐杯是信息通信领域的老牌赛事，我参加了其 5G 技术及应用赛道。省赛一等奖晋级全国总决赛，最终斩获全国三等奖。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E5%9B%BD%E5%AE%B6%E4%B8%89%E7%AD%89%E5%A5%96.webp" alt="大唐杯 全国总决赛三等奖"></p><h3 id="睿抗机器人开发者大赛-·-国家级奖项"><a href="#睿抗机器人开发者大赛-·-国家级奖项" class="headerlink" title="睿抗机器人开发者大赛 · 国家级奖项"></a>睿抗机器人开发者大赛 · 国家级奖项</h3><p>与队友一起完成的机器人视觉识别项目，从省赛一路打到国赛，最终获得国家级奖项。这也是我第一次把课堂上学到的计算机视觉知识完整落地到比赛中。</p><p><img src="/media/%E7%9D%BF%E6%8A%97%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%9B%BD%E5%AE%B6%E7%BA%A7%E5%A5%96.webp" alt="睿抗机器人开发者大赛 国家级"></p><h3 id="中国大学生计算机设计大赛-·-国家级三等奖"><a href="#中国大学生计算机设计大赛-·-国家级三等奖" class="headerlink" title="中国大学生计算机设计大赛 · 国家级三等奖"></a>中国大学生计算机设计大赛 · 国家级三等奖</h3><p>作品围绕 AI 应用展开，从校赛、省赛到国赛三轮打磨，最终获得国家级三等奖。</p><p><img src="/media/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%AE%BE%E8%AE%A1%E5%A4%A7%E8%B5%9B%E5%9B%BD%E5%AE%B6%E4%B8%89%E7%AD%89%E5%A5%96.webp" alt="中国大学生计算机设计大赛 国家级三等奖"></p><h2 id="省级奖项"><a href="#省级奖项" class="headerlink" title="省级奖项"></a>省级奖项</h2><h3 id="2025-AIC-大赛-·-省赛一等奖"><a href="#2025-AIC-大赛-·-省赛一等奖" class="headerlink" title="2025 AIC 大赛 · 省赛一等奖"></a>2025 AIC 大赛 · 省赛一等奖</h3><p>国赛的前一站，省赛阶段就以一等奖的成绩拿到了全国总决赛的入场券。</p><p><img src="/media/2025%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD%E5%A4%A7%E8%B5%9B%E7%9C%81%E4%B8%80%E7%AD%89%E5%A5%96.webp" alt="2025 AIC 大赛 省赛一等奖"></p><h3 id="大唐杯-省赛一等奖"><a href="#大唐杯-省赛一等奖" class="headerlink" title="大唐杯 省赛一等奖"></a>大唐杯 省赛一等奖</h3><p>通信赛道的省赛一等奖，也是晋级全国总决赛的敲门砖。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E7%9C%81%E4%B8%80%E7%AD%89%E5%A5%96.webp" alt="大唐杯 省赛一等奖"></p><h3 id="中国大学生服务外包创新创业大赛-·-省级一等奖"><a href="#中国大学生服务外包创新创业大赛-·-省级一等奖" class="headerlink" title="中国大学生服务外包创新创业大赛 · 省级一等奖"></a>中国大学生服务外包创新创业大赛 · 省级一等奖</h3><p>与团队一起完成的软件服务类项目，获得省级一等奖。</p><p><img src="/media/%E6%9C%8D%E5%8A%A1%E5%A4%96%E5%8C%85%E5%A4%A7%E8%B5%9B%E7%9C%81%E4%B8%80%E7%AD%89%E5%A5%96.webp" alt="中国大学生服务外包创新创业大赛 省级一等奖"></p><h3 id="睿抗机器人开发者大赛-·-省级奖项"><a href="#睿抗机器人开发者大赛-·-省级奖项" class="headerlink" title="睿抗机器人开发者大赛 · 省级奖项"></a>睿抗机器人开发者大赛 · 省级奖项</h3><p>机器人方向的省级奖项，也是国赛的前一站。</p><p><img src="/media/%E7%9D%BF%E6%8A%97%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%9C%81%E7%BA%A7%E5%A5%96.webp" alt="睿抗机器人开发者大赛 省级"></p><h3 id="全国大学生统计建模大赛-·-省赛"><a href="#全国大学生统计建模大赛-·-省赛" class="headerlink" title="全国大学生统计建模大赛 · 省赛"></a>全国大学生统计建模大赛 · 省赛</h3><p>用统计建模的方法做了一个实际问题分析，获得了省赛奖项。</p><p><img src="/media/%E7%BB%9F%E8%AE%A1%E5%BB%BA%E6%A8%A1%E5%A4%A7%E8%B5%9B%E7%9C%81%E7%BA%A7%E5%A5%96.webp" alt="全国大学生统计建模大赛 省赛"></p><h3 id="蓝桥杯-·-Java-组省二等奖"><a href="#蓝桥杯-·-Java-组省二等奖" class="headerlink" title="蓝桥杯 · Java 组省二等奖"></a>蓝桥杯 · Java 组省二等奖</h3><p>蓝桥杯是程序设计的经典赛事，在 Java 语言组拿到了省二等奖。</p><p><img src="/media/%E8%93%9D%E6%A1%A5%E6%9D%AFJava%E7%9C%81%E4%BA%8C%E7%AD%89%E5%A5%96.webp" alt="蓝桥杯 Java 组 省二等奖"></p><h3 id="中国大学生计算机设计大赛-·-省级奖项"><a href="#中国大学生计算机设计大赛-·-省级奖项" class="headerlink" title="中国大学生计算机设计大赛 · 省级奖项"></a>中国大学生计算机设计大赛 · 省级奖项</h3><p>计设大赛的省级阶段，与国赛作品一脉相承。</p><p><img src="/media/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%AE%BE%E8%AE%A1%E5%A4%A7%E8%B5%9B%E7%9C%81%E7%BA%A7%E5%A5%96.webp" alt="中国大学生计算机设计大赛 省级"></p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>回头看这些证书，数量不是最重要的，重要的是每一次参赛都在逼自己走出舒适区：第一次写完整的技术文档，第一次在评委面前答辩，第一次和队友为一个 bug 熬到凌晨。</p><p>如果有同学也在准备保研，我的建议是：早点关注自己专业相关的学科竞赛，把课堂知识用到真实题目里，收获的往往比证书本身更多。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>大唐杯证书查询步骤</title>
      <link>https://ckxkx.github.io/2026/08/23/datang-certificate-query/</link>
      <description>
        <![CDATA[<p>大唐杯（大学生新一代信息通信科技大赛）证书在线查询完整步骤：官网登录、我的证书、跳转查询平台、输入证书编号查询。</p>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>参加”大唐杯]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E5%AD%A6%E4%B9%A0/">学习</category>
      <category domain="https://ckxkx.github.io/tags/%E4%BF%9D%E7%A0%94/">保研</category>
      <pubDate>Sun, 23 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>大唐杯（大学生新一代信息通信科技大赛）证书在线查询完整步骤：官网登录、我的证书、跳转查询平台、输入证书编号查询。</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>参加”大唐杯”全国大学生新一代信息通信技术大赛后，证书需要在线查询。本文记录完整的证书查询步骤，按顺序操作即可。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E8%B5%9B%E4%BA%8B.webp" alt="大唐杯赛事"></p><h2 id="第一步：打开大赛官网"><a href="#第一步：打开大赛官网" class="headerlink" title="第一步：打开大赛官网"></a>第一步：打开大赛官网</h2><p>在浏览器中打开网址：<strong><a href="https://www.ksup.cn/">https://www.ksup.cn</a></strong></p><p>这是大学生新一代信息通信科技大赛（大唐杯）的官方网站，所有证书查询操作都从这里开始。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E5%AE%98%E7%BD%91%E9%A6%96%E9%A1%B5.webp" alt="大赛官网首页"></p><h2 id="第二步：登录账号"><a href="#第二步：登录账号" class="headerlink" title="第二步：登录账号"></a>第二步：登录账号</h2><p>进入官网后，点击登录。<strong>账号为报名时提供的手机号</strong>，输入手机号和密码完成登录。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E7%BD%91%E7%AB%99.webp" alt="官网首页"></p><h2 id="第三步：进入”我的证书”"><a href="#第三步：进入”我的证书”" class="headerlink" title="第三步：进入”我的证书”"></a>第三步：进入”我的证书”</h2><p>登录成功后，进入<strong>个人中心 → 我的证书</strong>。</p><p>在这里可以看到参赛获得的证书列表，每张证书右侧有”查询地址”入口。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E6%88%91%E7%9A%84%E8%AF%81%E4%B9%A6.png" alt="我的证书"></p><h2 id="第四步：跳转查询平台并查询"><a href="#第四步：跳转查询平台并查询" class="headerlink" title="第四步：跳转查询平台并查询"></a>第四步：跳转查询平台并查询</h2><p>点击”查询地址”，页面会跳转至<strong>证书查询平台</strong>：</p><blockquote><p><a href="https://cx.miitec.cn/certificateSearch?type=4">https://cx.miitec.cn/certificateSearch?type=4</a></p></blockquote><p>在查询平台中，<strong>复制证书编号</strong>（如 <code>mc12b025132</code>）粘贴到输入框，点击”查询”按钮，即可查看证书信息。</p><p><img src="/media/%E5%A4%A7%E5%94%90%E6%9D%AF%E8%AF%81%E4%B9%A6%E6%9F%A5%E8%AF%A2%E5%B9%B3%E5%8F%B0.webp" alt="证书查询平台"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整个流程非常简单：</p><ol><li><p>打开官网 <strong>ksup.cn</strong></p></li><li><p>用报名时手机号<strong>登录</strong></p></li><li><p>进入<strong>个人中心 - 我的证书</strong></p></li><li><p>点击<strong>查询地址</strong>跳转平台，输入<strong>证书编号</strong>查询</p></li></ol><p>证书编号和报名手机号务必提前准备好，以免遗漏。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>解决 Ubuntu 没有网络的真神命令</title>
      <link>https://ckxkx.github.io/2026/08/23/ubuntu-network-nmcli-restart/</link>
      <description>
        <![CDATA[<p>Ubuntu 突然断网时，一条 nmcli 命令重启网络管理器，快速恢复网络连接。</p>
<h2 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h2><p>Ubuntu 系统使用一段时间后突然没有网]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%A7%91%E6%8A%80/">科技</category>
      <category domain="https://ckxkx.github.io/tags/Ubuntu/">Ubuntu</category>
      <category domain="https://ckxkx.github.io/tags/Linux/">Linux</category>
      <category domain="https://ckxkx.github.io/tags/%E7%BD%91%E7%BB%9C/">网络</category>
      <pubDate>Sun, 23 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Ubuntu 突然断网时，一条 nmcli 命令重启网络管理器，快速恢复网络连接。</p><h2 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h2><p>Ubuntu 系统使用一段时间后突然没有网络（有线 &#x2F; Wi-Fi 都无法连接），ping 不通外网，浏览器打不开页面，但系统本身运行正常。这种情况经常发生在：</p><ul><li><p>休眠 &#x2F; 挂起唤醒后</p></li><li><p>网络服务异常</p></li><li><p>切换了网络环境</p></li><li><p>虚拟机中网络状态异常</p></li></ul><h2 id="真神命令"><a href="#真神命令" class="headerlink" title="真神命令"></a>真神命令</h2><p>在终端中执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> nmcli networking off<br><span class="hljs-built_in">sudo</span> nmcli networking on<br></code></pre></td></tr></table></figure><p>先关闭网络管理，再重新开启，让 NetworkManager 重新初始化所有网络接口。</p><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p><code>nmcli</code> 是 NetworkManager 的命令行工具，<code>networking off/on</code> 会整体重启网络管理服务，相当于对网络栈做一次「软重置」。比单独重启某个接口（如 <code>ip link set dev eth0 down/up</code>）更彻底，能修复大部分「莫名断网」问题。</p><h2 id="其他排查手段"><a href="#其他排查手段" class="headerlink" title="其他排查手段"></a>其他排查手段</h2><p>如果上面命令无效，可以按顺序尝试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 重启 NetworkManager 服务</span><br><span class="hljs-built_in">sudo</span> systemctl restart NetworkManager<br><br><span class="hljs-comment"># 查看网卡状态</span><br>ip a<br><br><span class="hljs-comment"># 释放并重新获取 DHCP 地址（有线）</span><br><span class="hljs-built_in">sudo</span> dhclient -r<br><span class="hljs-built_in">sudo</span> dhclient<br></code></pre></td></tr></table></figure><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ul><li><p>命令需要 <code>sudo</code> 权限</p></li><li><p>如果配置了静态 IP，重启后需确认配置仍然生效</p></li><li><p>无线网络下，重启后可能需要重新连接 Wi-Fi</p></li><li><p>在远程服务器上操作时注意：网络重启瞬间 SSH 会短暂断开，随后恢复</p></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>删除 clone 项目中的 Git 链接</title>
      <link>https://ckxkx.github.io/2026/08/23/git-remove-clone-link/</link>
      <description>
        <![CDATA[<p>一条命令把从 GitHub 等平台 clone 的项目变成普通文件夹，移除 .git 版本控制目录。</p>
<h2 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h2><p>当你从 GitHub 或]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%A7%91%E6%8A%80/">科技</category>
      <category domain="https://ckxkx.github.io/tags/Git/">Git</category>
      <category domain="https://ckxkx.github.io/tags/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/">开发工具</category>
      <pubDate>Sun, 23 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>一条命令把从 GitHub 等平台 clone 的项目变成普通文件夹，移除 .git 版本控制目录。</p><h2 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h2><p>当你从 GitHub 或其他平台 <code>git clone</code> 一个项目后，如果只想保留源码文件、摆脱与远程仓库的关联（比如把它当作普通文件夹使用、或不想误 push），需要删除项目内的 <code>.git</code> 目录。</p><h2 id="命令"><a href="#命令" class="headerlink" title="命令"></a>命令</h2><p>在项目根目录执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">find . -name <span class="hljs-string">&quot;.git&quot;</span> | xargs <span class="hljs-built_in">rm</span> -rf<br></code></pre></td></tr></table></figure><p>这条命令会递归查找当前目录下所有名为 <code>.git</code> 的目录（包括子项目、依赖中的 git 仓库），并全部删除。</p><h2 id="说明与注意"><a href="#说明与注意" class="headerlink" title="说明与注意"></a>说明与注意</h2><ul><li><p><code>find . -name &quot;.git&quot;</code>：从当前目录开始递归查找 <code>.git</code> 目录</p></li><li><p><code>xargs rm -rf</code>：把找到的路径传给 <code>rm -rf</code> 强制删除</p></li><li><p>⚠️ <strong>此操作不可恢复</strong>：删除后项目将失去所有 Git 历史记录，无法回退到任意提交</p></li><li><p>建议先确认路径再执行：<code>find . -name &quot;.git&quot;</code> 单独运行，检查列出的目录都是你想删除的</p></li><li><p>如果只需要顶层 <code>.git</code>，也可以直接 <code>rm -rf .git</code></p></li></ul><h2 id="删除后"><a href="#删除后" class="headerlink" title="删除后"></a>删除后</h2><ul><li><p>项目变成普通文件夹，可自由移动、复制</p></li><li><p>失去 <code>git log</code>、<code>git diff</code> 等历史功能</p></li><li><p>如需重新建立版本管理，可以 <code>git init</code> 初始化一个新仓库</p></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>刷新 Windows 系统环境变量命令</title>
      <link>https://ckxkx.github.io/2026/08/23/windows-refresh-env-vars/</link>
      <description>
        <![CDATA[<p>修改环境变量后，在 PowerShell 里一条命令立即刷新 PATH，无需重启终端或电脑。</p>
<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>在 Windows 上安装了新软]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%A7%91%E6%8A%80/">科技</category>
      <category domain="https://ckxkx.github.io/tags/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/">环境变量</category>
      <category domain="https://ckxkx.github.io/tags/Windows/">Windows</category>
      <pubDate>Sun, 23 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>修改环境变量后，在 PowerShell 里一条命令立即刷新 PATH，无需重启终端或电脑。</p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>在 Windows 上安装了新软件（如 Python、Node、Go），或手动修改了系统环境变量后，<strong>已经打开的终端</strong>往往不会立即生效——因为 PATH 是在终端启动时读取并缓存到进程里的。通常需要重开终端，甚至重启电脑才能生效。</p><h2 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h2><p>在 PowerShell 中执行以下命令，立即将系统（Machine）和用户（User）的 PATH 合并并应用到当前会话：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs powershell"><span class="hljs-variable">$env:Path</span> = [<span class="hljs-type">System.Environment</span>]::GetEnvironmentVariable(<span class="hljs-string">&quot;Path&quot;</span>,<span class="hljs-string">&quot;Machine&quot;</span>) + <span class="hljs-string">&quot;;&quot;</span> + [<span class="hljs-type">System.Environment</span>]::GetEnvironmentVariable(<span class="hljs-string">&quot;Path&quot;</span>,<span class="hljs-string">&quot;User&quot;</span>)<br></code></pre></td></tr></table></figure><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><ul><li><p><code>GetEnvironmentVariable(&quot;Path&quot;, &quot;Machine&quot;)</code>：读取<strong>系统级</strong> PATH（所有用户生效）</p></li><li><p><code>GetEnvironmentVariable(&quot;Path&quot;, &quot;User&quot;)</code>：读取<strong>用户级</strong> PATH（当前用户生效）</p></li><li><p>把两者合并后写回当前会话的 <code>$env:Path</code>，终端立刻拥有最新的搜索路径</p></li></ul><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><p>刷新后运行 <code>echo $env:Path</code> 查看是否包含新路径，或直接执行刚安装的命令（如 <code>python --version</code>）确认生效。</p><h2 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h2><ul><li><p>只对<strong>当前 PowerShell 会话</strong>生效，新开的终端会自动读取最新值</p></li><li><p>若需要每次打开终端自动刷新，可以把命令加入 PowerShell 的 <code>$PROFILE</code></p></li><li><p>命令行 CMD 中可改用：<code>refreshenv</code>（需安装 Chocolatey 环境增强）或重开窗口</p></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Apple 图书无法从云端同步的解决办法</title>
      <link>https://ckxkx.github.io/2026/08/23/apple-books-cloud-sync/</link>
      <description>
        <![CDATA[<p>Apple 图书（Books）从 iCloud 下载书籍失败，多半是触发了苹果的保护流量机制，连接 Wi-Fi 即可解决。</p>
<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>在]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%A7%91%E6%8A%80/">科技</category>
      <category domain="https://ckxkx.github.io/tags/iCloud/">iCloud</category>
      <category domain="https://ckxkx.github.io/tags/Apple/">Apple</category>
      <pubDate>Sun, 23 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Apple 图书（Books）从 iCloud 下载书籍失败，多半是触发了苹果的保护流量机制，连接 Wi-Fi 即可解决。</p><h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>在 iPhone &#x2F; iPad 的「图书」App 中，点击云端书籍想要下载到本地，却一直卡住、转圈、或提示无法下载，即使当前网络看起来正常。</p><h2 id="原因"><a href="#原因" class="headerlink" title="原因"></a>原因</h2><p><strong>触发了苹果的保护流量机制。</strong></p><p>苹果会对异常流量（包括蜂窝网络下的高频下载、部分代理环境下的连接）进行风控保护，导致 iCloud 下载请求被临时拦截。此时图书下载自然失败或卡住。</p><h2 id="解决方法（目前）"><a href="#解决方法（目前）" class="headerlink" title="解决方法（目前）"></a>解决方法（目前）</h2><p><strong>连接 Wi-Fi 再进行下载。</strong></p><p>切换到 Wi-Fi 网络后，重新点击云端书籍的下载按钮，通常即可正常下载。</p><h2 id="补充建议"><a href="#补充建议" class="headerlink" title="补充建议"></a>补充建议</h2><ul><li><p>如果 Wi-Fi 下仍然失败，尝试「设置 → 通用 → 还原 → 还原网络设置」，或重启设备</p></li><li><p>检查「设置 → App Store &#x2F; iCloud」中图书的下载权限是否被关闭</p></li><li><p>若使用了代理工具，关闭代理再试，避免流量特征被风控误判</p></li><li><p>蜂窝网络下可以开启「设置 → 图书 → 使用蜂窝数据」，但下载大文件仍建议在 Wi-Fi 下进行</p></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>歪比巴卜影院</title>
      <link>https://ckxkx.github.io/2026/08/18/website-3f21707e-f545-45f5-9a56-09ef331c66c0/</link>
      <description>
        <![CDATA[<p>资源非常全面，播放器功能完善，有弹幕，作者非常良心~</p>
<p><a href="https://v.wbbb1.com/">访问原始内容</a></p>]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E5%BD%B1%E8%A7%86/">影视</category>
      <pubDate>Tue, 18 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>资源非常全面，播放器功能完善，有弹幕，作者非常良心~</p><p><a href="https://v.wbbb1.com/">访问原始内容</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>爱看机器人影视</title>
      <link>https://ckxkx.github.io/2026/08/18/website-27c9b99e-3710-46d7-aa19-da583de18b60/</link>
      <description>
        <![CDATA[<p>简单好用无广线路多</p>
<h3 id="优点："><a href="#优点：" class="headerlink" title="优点："></a>优点：</h3><p>资源均采用爬虫从各平台爬取下来，古今中外影视基本都有，线路很多</p>
<h3 id="缺点：">]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E5%BD%B1%E8%A7%86/">影视</category>
      <category domain="https://ckxkx.github.io/tags/%E5%BD%B1%E8%A7%86/">影视</category>
      <pubDate>Tue, 18 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>简单好用无广线路多</p><h3 id="优点："><a href="#优点：" class="headerlink" title="优点："></a>优点：</h3><p>资源均采用爬虫从各平台爬取下来，古今中外影视基本都有，线路很多</p><h3 id="缺点："><a href="#缺点：" class="headerlink" title="缺点："></a>缺点：</h3><p>晚高峰线路人很多，可能会比较卡，白天非常流畅</p><p><a href="https://">https://www.ikanbot.com/</a></p><p><img src="/media/%E7%88%B1%E7%9C%8B%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%BD%B1%E8%A7%86%E6%88%AA%E5%9B%BE.webp" alt="image.png"></p><p><a href="https://www.ikanbot.com/">访问原始内容</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Termux 入门教程：架设手机 Server 下载文件</title>
      <link>https://ckxkx.github.io/2026/08/18/website-f8862cb5-7a7d-498f-9037-2bc9b3248642/</link>
      <description>
        <![CDATA[<p>Termux 美化</p>
<h3 id="玩转termux美化"><a href="#玩转termux美化" class="headerlink" title="玩转termux美化"></a>玩转termux美化</h3><p>Termux 入门教程：架设手机 Serv]]>
      </description>
      <author>朱会琛</author>
      <category domain="https://ckxkx.github.io/categories/%E7%A7%91%E6%8A%80/">科技</category>
      <pubDate>Tue, 18 Aug 2026 04:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Termux 美化</p><h3 id="玩转termux美化"><a href="#玩转termux美化" class="headerlink" title="玩转termux美化"></a>玩转termux美化</h3><p>Termux 入门教程：架设手机 Server 下载文件<br>阮一峰大神出品，必是精品</p><p><a href="https://www.ruanyifeng.com/blog/2019/07/termux-tutorial.html">访问原始内容</a></p>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
