<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://hac.he.cn/</id>
  <title type="text">HAC&apos;s Blog</title>
  <subtitle type="text">Main site</subtitle>
  <updated>2026-10-02T00:00:00.000Z</updated>
  <author><name>HAC</name></author>
  <link rel="alternate" href="https://hac.he.cn/"/>
  <link rel="self" href="https://hac.he.cn/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://hac.he.cn/posts/agent-04/</id>
      <title type="text">pi-subagents</title>
      <published>2026-10-02T00:00:00.000Z</published>
      <updated>2026-10-02T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/agent-04/"/>
      <summary type="text">记录 pi-subagents 扩展的内容与实践</summary>
      <content type="html"><![CDATA[<p><code>pi-subagents</code> 本质上是一个 面向 Pi 的多 agent 编排扩展。它把一个 Pi 会话变成 父 agent（parent agent），从而可以将聚焦的细分工作委派给额外的 Pi 会话或外部编码 agent CLI。</p>
<p>简明结构：</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>你</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span>Pi 主会话</span></div></div><div><div><div>5</div></div><div><span>(parent / orchestrator)</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>├── scout ───────► 探索代码库</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>├── worker ──────► 应用代码修改</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>├── reviewer ────► 审阅代码修改</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>└── oracle ──────► 挑战 plan</span></div></div><div><div><div>14</div></div><div><span><span>       </span></span><span>│</span></div></div><div><div><div>15</div></div><div><span><span>       </span></span><span>▼</span></div></div><div><div><div>16</div></div><div><span>results returned to parent</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<section><h1>1. 使用<a href="#1-使用"><span>#</span></a></h1><p>内置的 subagents（部分拥有别名）：</p>

<table><thead><tr><th>Agent</th><th>用途</th></tr></thead><tbody><tr><td><code>scout</code></td><td>代码库侦察：快速定位入口、关键文件、接口、数据流、依赖和风险，整理供后续 agent 使用的上下文，默认输出 <code>context.md</code>.</td></tr><tr><td><code>researcher</code></td><td>网络与文档研究：搜索、读取原始来源、比较证据，产出带引用的研究简报，默认输出 <code>research.md</code>.</td></tr><tr><td><code>evidence-auditor</code></td><td>独立证据审计：检查已有研究中的重要结论是否真的得到来源支持，指出矛盾、过时来源和证据缺口</td></tr><tr><td><code>worker</code> / <code>developer</code> / <code>coder</code> / <code>implementer</code> / <code>develop</code></td><td>代码实现：按任务或已批准的方案修改文件、运行检查和测试；遇到未批准的架构或产品决策时向主 agent 请示</td></tr><tr><td><code>reviewer</code></td><td>审查与验证：审查代码 diff、计划、解决方案、代码库状态，以及 PR/issue 的完成情况，给出有证据的问题和结论</td></tr><tr><td><code>oracle</code> / <code>advisor</code></td><td>第二意见：结合已有对话与约束，检查隐藏假设、决策矛盾和方向偏移，挑战当前方案</td></tr><tr><td><code>delegate</code></td><td>通用委派：行为接近主 agent,没有固定的默认预读文件，直接完成一个边界明确的任务</td></tr></tbody></table><p>可调用的外部 CLI subagents：</p>

<table><thead><tr><th>Agent</th><th>用途</th></tr></thead><tbody><tr><td><code>claude-code</code></td><td>只分析传入的交接材料，无工具，不可编辑文件，适合对已提供的方案或材料给出意见</td></tr><tr><td><code>claude-code-writer</code></td><td>修改文件；开放 <code>Read</code>、<code>Write</code>、<code>Edit</code>、<code>Glob</code>、<code>Grep</code>，不开放 <code>Bash</code></td></tr><tr><td><code>codex-exec</code></td><td>在 <code>read-only</code> 沙箱里做只读分析</td></tr><tr><td><code>codex-exec-writer</code></td><td>在 <code>workspace-write</code> 沙箱里执行工作区修改</td></tr><tr><td><code>cursor-agent</code></td><td><code>ask</code> 模式做只读分析</td></tr><tr><td><code>cursor-agent-writer</code></td><td>启动沙箱的非交互模式执行工作区修改</td></tr></tbody></table><p>在调用外部 CLI subagents 前，需要先安装并认证相应 CLI. 另外，内置的 subagents 没有默认绑定模型，可以通过设置单独覆盖。</p><p>使用示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>让 scout 梳理这个项目的支付流程，列出关键文件和风险。</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>让 worker 按这个计划实现修改，然后让 reviewer 独立审查。</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>请 oracle 挑战这个架构方案，重点检查是否违反我们之前确定的约束。</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>让 researcher 调研这两个库，再让 evidence-auditor 核验关键结论。</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>使用 codex-exec 对当前改动做只读分析。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>1.1 创建自定义 subagents<a href="#11-创建自定义-subagents"><span>#</span></a></h2><p>分为两种情况：</p><ul>
<li>创建新 subagent：写一个 markdown agent 文件</li>
<li>修改内置 subagent：少量修改可以用 <code>settings.json</code> 覆盖设置，大幅修改则复制同名的 agent 文件</li>
</ul><p>配置文件位置：</p>

<table><thead><tr><th>用途</th><th>用户级：跨项目使用</th><th>项目级：仅当前项目</th></tr></thead><tbody><tr><td>自定义 agent 定义</td><td><code>~/.pi/agent/agents/&lt;名称&gt;.md</code></td><td><code>&lt;项目根目录&gt;/.pi/agents/&lt;名称&gt;.md</code></td></tr><tr><td>覆盖内置或自定义 agent 的字段</td><td><code>~/.pi/agent/settings.json</code></td><td><code>&lt;项目根目录&gt;/.pi/settings.json</code></td></tr></tbody></table><p>同名 agent 定义的优先级是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>项目级 &gt; 用户级 &gt; 安装包提供的定义 &gt; 内置定义</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>设置中的覆盖字段也遵循<strong>项目级优先于用户级</strong>。</p><div><div><div></div><div>Warning</div></div><div><p><code>subagents.agentOverrides</code> 应写进 Pi 的 <code>settings.json</code>，不是扩展的 <code>~/.pi/agent/extensions/subagent/config.json</code>。</p></div></div><section><h3>1.1.1 创建自定义 subagent 示例<a href="#111-创建自定义-subagent-示例"><span>#</span></a></h3><p>以创建一个<strong>只读安全审查员</strong>为例。</p><p>创建文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>~/.pi/agent/agents/security-reviewer.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内容如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>name</span><span>: </span><span>security-reviewer</span></div></div><div><div><div>3</div></div><div><span>description</span><span>: </span><span>只读安全审查员，检查认证、授权、输入校验及敏感信息泄漏</span></div></div><div><div><div>4</div></div><div><span>aliases</span><span>: </span><span>security-auditor</span></div></div><div><div><div>5</div></div><div><span>advertise</span><span>: </span><span>true</span></div></div><div><div><div>6</div></div><div><span>model</span><span>: </span><span>inherit</span></div></div><div><div><div>7</div></div><div><span>thinking</span><span>: </span><span>high</span></div></div><div><div><div>8</div></div><div><span>tools</span><span>: </span><span>read, grep, find, ls, contact_supervisor</span></div></div><div><div><div>9</div></div><div><span>systemPromptMode</span><span>: </span><span>replace</span></div></div><div><div><div>10</div></div><div><span>inheritProjectContext</span><span>: </span><span>true</span></div></div><div><div><div>11</div></div><div><span>inheritGlobalContext</span><span>: </span><span>false</span></div></div><div><div><div>12</div></div><div><span>inheritSkills</span><span>: </span><span>false</span></div></div><div><div><div>13</div></div><div><span>defaultContext</span><span>: </span><span>fresh</span></div></div><div><div><div>14</div></div><div><span>acceptanceRole</span><span>: </span><span>read-only</span></div></div><div><div><div>15</div></div><div><span>async</span><span>: </span><span>true</span></div></div><div><div><div>16</div></div><div><span>timeoutMs</span><span>: </span><span>900000</span></div></div><div><div><div>17</div></div><div><span>---</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>你是一个只读安全审查员。</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>任务：</span></div></div><div><div><div>22</div></div><div><span><span>-</span><span> 审查指定文件或主 agent 提供的 diff。</span></span></div></div><div><div><div>23</div></div><div><span><span>-</span><span> 重点检查认证绕过、权限校验缺失、输入校验和敏感信息泄漏。</span></span></div></div><div><div><div>24</div></div><div><span><span>-</span><span> 从实际代码中验证问题，不凭猜测报告漏洞。</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>边界：</span></div></div><div><div><div>27</div></div><div><span><span>-</span><span> 不修改文件，不运行 shell 命令。</span></span></div></div><div><div><div>28</div></div><div><span><span>-</span><span> 如果缺少必要材料，报告缺口。</span></span></div></div><div><div><div>29</div></div><div><span><span>-</span><span> 如需主 agent 做决定，使用 contact_supervisor 请求确认。</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>输出：</span></div></div><div><div><div>32</div></div><div><span><span>1.</span><span> 审查范围。</span></span></div></div><div><div><div>33</div></div><div><span><span>2.</span><span> 已确认问题：严重程度、文件路径、行号、证据与修复建议。</span></span></div></div><div><div><div>34</div></div><div><span><span>3.</span><span> 未验证事项与剩余风险。</span></span></div></div><div><div><div>35</div></div><div><span><span>4.</span><span> 若无已确认问题，明确说明。</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>字段说明：</p>

<table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>name</code></td><td>调用时使用的正式名称。这里是 <code>security-reviewer</code>；同名定义会发生覆盖。</td></tr><tr><td><code>description</code></td><td>角色简介，帮助主 agent 判断它适合什么任务。</td></tr><tr><td><code>aliases</code></td><td>可选别名。这里也可以用 <code>security-auditor</code> 选择该角色。</td></tr><tr><td><code>advertise</code></td><td><code>true</code> 表示在 <code>subagent</code> 工具激活时，将名称和简介加入主 agent 的角色目录，不代表会自动启动或自动委派。</td></tr><tr><td><code>model</code></td><td>默认模型。<code>inherit</code> 表示使用当前父会话模型；也可填已配置的 <code>provider/model-id</code>。</td></tr><tr><td><code>thinking</code></td><td>推理强度。示例为 <code>high</code>；实际是否支持取决于模型及提供方。</td></tr><tr><td><code>tools</code></td><td>子会话的<strong>严格工具白名单</strong>。此例没有 <code>bash</code>、<code>edit</code>、<code>write</code>。</td></tr><tr><td><code>systemPromptMode</code></td><td><code>replace</code>：使用该 agent 的独立提示词；<code>append</code>：把正文追加到 Pi 的基础提示词。默认是 <code>replace</code>。</td></tr><tr><td><code>inheritProjectContext</code></td><td>是否保留项目指令，例如仓库的 <code>AGENTS.md</code>、<code>CLAUDE.md</code>。</td></tr><tr><td><code>inheritGlobalContext</code></td><td>是否保留用户全局上下文。只有 <code>inheritProjectContext: true</code> 时才有效，默认是 <code>false</code>。</td></tr><tr><td><code>inheritSkills</code></td><td>是否让子会话看到 Pi 发现的 skills 目录。也可以用 <code>skills</code> 单独选择需要的技能。</td></tr><tr><td><code>defaultContext</code></td><td><code>fresh</code>：默认从新上下文开始；<code>fork</code>：默认继承父会话对话上下文。与是否继承项目指令是两回事。</td></tr><tr><td><code>acceptanceRole</code></td><td>用于自动验收推断的角色标记：<code>read-only</code> 或 <code>writer</code>。<strong>本身不授予或撤销工具权限。</strong></td></tr><tr><td><code>async</code></td><td>单 agent 调用默认是否后台执行。显式调用参数可以覆盖，强制异步配置除外。</td></tr><tr><td><code>timeoutMs</code></td><td>默认运行时限，单位毫秒。<code>900000</code> 是 15 分钟。</td></tr></tbody></table><p>如果不需要这些控制项，最小定义也可以很简单：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>name</span><span>: </span><span>security-reviewer</span></div></div><div><div><div>3</div></div><div><span>description</span><span>: </span><span>只读安全审查员</span></div></div><div><div><div>4</div></div><div><span>tools</span><span>: </span><span>read, grep, find, ls</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>审查指定代码中的安全问题，只报告有证据的问题，不修改文件。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保存后执行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/reload</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后告诉 Pi：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请使用 security-reviewer 审查 src/auth/ 下的认证和权限逻辑，只报告有证据的问题。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也可以让 Pi 帮你确认加载结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>列出当前可用的 subagents，并显示 security-reviewer 的配置。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.1.2 修改内置 subagent：方式 A，覆盖部分字段<a href="#112-修改内置-subagent方式-a覆盖部分字段"><span>#</span></a></h3><p><strong>适合：改模型、推理强度、工具权限、上下文策略、描述等少量配置。</strong></p><p>例如，将内置 <code>reviewer</code> 调整为高推理强度、默认使用新上下文，并修改简介。</p><p>编辑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>~/.pi/agent/settings.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>合并以下内容：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"subagents"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"agentOverrides"</span><span>: {</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"reviewer"</span><span>: {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>"description"</span><span>: </span><span>"独立只读审查员，重点检查正确性、回归风险和测试覆盖"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>"model"</span><span>: </span><span>"inherit"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"thinking"</span><span>: </span><span>"high"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>"defaultContext"</span><span>: </span><span>"fresh"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>"inheritProjectContext"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>"inheritGlobalContext"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"inheritSkills"</span><span>: </span><span>false</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里：</p><ul>
<li><code>subagents</code>：扩展的设置命名空间。</li>
<li><code>agentOverrides</code>：按 agent 正式名称配置覆盖项。</li>
<li><code>reviewer</code>：要修改的角色。</li>
<li>内层字段替换该角色对应的默认值；<strong>没写的字段继续保留原定义</strong>，包括原系统提示词和工具白名单。</li>
</ul><p>如果仅希望某个项目采用这个配置，写到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;项目根目录&gt;/.pi/settings.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>已有 <code>settings.json</code> 时，请合并字段，不要用示例覆盖整个文件。</strong> 修改后执行 <code>/reload</code>。</p><p>还可以禁用某个角色：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"subagents"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"agentOverrides"</span><span>: {</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"reviewer"</span><span>: {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>"disabled"</span><span>: </span><span>true</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Info</div></div><div><p>设置覆盖不仅适用于内置角色，也能覆盖同名自定义角色。因此，如果你的 Markdown 配置没有按预期生效，应检查用户和项目的 <code>agentOverrides</code>。</p></div></div></section><section><h3>1.1.3 修改内置 subagent：方式 B，创建同名完整定义<a href="#113-修改内置-subagent方式-b创建同名完整定义"><span>#</span></a></h3><p><strong>适合：大幅改变角色职责、重写系统提示词或重新设计工具集。</strong></p><p>以 <code>reviewer</code> 为例，可以先把随包定义复制到用户目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>~/.pi/agent/agents</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>cp</span><span> </span><span>~/.pi/agent/npm/node_modules/pi-subagents/agents/reviewer.md</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>   </span><span>~/.pi/agent/agents/reviewer.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后编辑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>~/.pi/agent/agents/reviewer.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保留：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>name</span><span>: </span><span>reviewer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>并按需求修改配置与正文。执行 <code>/reload</code> 后，你的同名定义会覆盖随包的 <code>reviewer</code>。</p><p>也可以直接请 Pi 使用扩展的管理操作：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请将内置 reviewer eject 到用户级目录，供我编辑；不要启动审查任务。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其对应工具操作是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>subagent</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>action</span><span>:</span><span> </span><span>"eject"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>agent</span><span>:</span><span> </span><span>"reviewer"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>agentScope</span><span>:</span><span> </span><span>"user"</span></div></div><div><div><div>5</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>重要区别：同名文件是完整替换，不是字段合并。</strong> 自定义文件中省略的字段不会从原内置定义继承。</p><p>恢复默认时，可以删除同名自定义文件，并清除相应覆盖项；也可以请 Pi 执行 <code>reset</code>。如果项目级仍有同名定义或覆盖项，用户级恢复后也仍会受项目级影响。</p></section></section><section><h2>1.2 工作流<a href="#12-工作流"><span>#</span></a></h2><p>两类现成入口：</p><ul>
<li>6 个斜杠命令工作流模板</li>
<li>2 个命名工作流资源：通过 subagent 工具调用，偏程序化集成。</li>
</ul><p>此外，还支持自行组合串行、并行、审查循环等流程。</p><section><h3>1.2.1 <code>/parallel-review</code>：多角度并行审查<a href="#121-parallel-review多角度并行审查"><span>#</span></a></h3><p>让多个 <code>reviewer</code> 从不同角度独立检查当前改动，再由主 agent 汇总。</p><p>典型流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>确定目标与审查角度</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>并行 reviewer：正确性 / 测试 / 可维护性等</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>主 agent 汇总：立即修复 / 可选改进 / 忽略或延期</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>默认使用 fresh 上下文，不依赖主会话的推理历史。</li>
<li>倾向使用 3 个有明确分工的 reviewer，但角度和数量不是固定死的。</li>
<li>默认只审查，不修改文件。</li>
<li>添加 <code>autofix</code>，会在汇总后修复“值得现在处理”的问题，并验证结果，不自动执行可选改进。</li>
</ul><p>示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/parallel-review 审查当前 diff，重点关注权限校验和回归风险</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/parallel-review autofix 审查 src/auth/ 的当前改动</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2.2 <code>/review-loop</code>：实现、审查、修复循环<a href="#122-review-loop实现审查修复循环"><span>#</span></a></h3><p>典型流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>worker 实现（如果尚未实现）</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>并行 reviewer 审查</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>主 agent 筛选问题</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>worker 修复 → 必要时再次审查</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>默认最多 3 轮审查，可以指定其他上限。</li>
<li>如果目标是已有 diff，直接从审查开始。</li>
<li>不会为了可选润色一直循环。</li>
<li>遇到未批准的产品、范围或架构决定，会暂停询问。</li>
<li>默认同一工作区一次只有一个 writer。</li>
</ul><p>示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/review-loop 为登录接口增加速率限制，最多审查 2 轮</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/review-loop 审查并修复当前 diff，最多 3 轮，不做范围外重构</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的“干净”并非必须消灭所有建议，而是没有尚需处理的阻塞问题或重要修复；可选项可以记录并延期。</p></section><section><h3>1.2.3 <code>/parallel-research</code>：并行调研与代码侦察<a href="#123-parallel-research并行调研与代码侦察"><span>#</span></a></h3><p>组合 <code>researcher</code> 和 <code>scout</code>，同时调查外部证据、本地实现和实践取舍。</p><p>典型流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>researcher：官方资料与外部证据 ─┐</span></div></div><div><div><div>2</div></div><div><span>scout：本地代码、约束与接入点 ──┼→ 主 agent 综合结论</span></div></div><div><div><div>3</div></div><div><span>researcher/scout：实践取舍 ─────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>默认角度是：</p><ol>
<li>外部证据。</li>
<li>本地代码上下文。</li>
<li>实践取舍与风险。</li>
</ol><p>通常使用 2–3 个子 agent，默认只研究，不实现。</p><p>示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/parallel-research 结合当前项目，比较 PostgreSQL 和 SQLite 的适用性</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>条件： 使用 <code>researcher</code> 的部分要求子会话加载 <code>pi-web-access</code> 及所需网络工具。</p></section><section><h3>1.2.4 <code>/gather-context-and-clarify</code>：先收集上下文，再澄清需求<a href="#124-gather-context-and-clarify先收集上下文再澄清需求"><span>#</span></a></h3><p>适合需求较模糊，但你希望 agent 先看代码，再问有针对性的问题。</p><p>典型流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>scout 查看代码</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>＋</span></div></div><div><div><div>3</div></div><div><span>必要时 researcher 查外部资料</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>汇总已知信息与缺口</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>向用户提出关键澄清问题</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/gather-context-and-clarify 我想给这个项目增加多租户支持，先看现有实现再问我问题</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Warning</div></div><div><p>该模板明确要求使用 <code>interview</code> 工具提问，但模板本身不提供这个工具。如果当前会话没有 <code>interview</code>，需要补齐工具，或者明确让主 agent 改为普通对话提问。</p></div></div></section><section><h3>1.2.5 <code>/parallel-cleanup</code>：并行清理审查<a href="#125-parallel-cleanup并行清理审查"><span>#</span></a></h3><p>这是聚焦代码与文字质量的专项审查，不是完整的正确性审查。</p><p>默认启动两个只读 reviewer：</p>

<table><thead><tr><th>审查分工</th><th>重点</th></tr></thead><tbody><tr><td>Deslop pass</td><td>AI 式冗余注释、占位文字、掩盖错误的防御代码、宽泛类型转换、无价值包装、生成腔文案等。</td></tr><tr><td>Verbosity pass</td><td>重复逻辑、啰嗦分支、无价值临时变量、重复测试、重复说明与样板代码等。</td></tr></tbody></table><ul>
<li>如果有 <code>deslop</code> 或 <code>verbosity-cleaner</code> skill，就使用它们。</li>
<li>没有这些 skill，也有模板内置指导，不是必需依赖。</li>
<li>默认只报告；添加 <code>autofix</code> 后应用筛选出的必要修复。</li>
<li>强调必须更短，同时更清晰且不改变行为。</li>
</ul><p>示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/parallel-cleanup 检查当前 diff 中的冗余代码和注释</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/parallel-cleanup autofix 仅清理本次改动，不做大规模重构</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2.6 <code>/council</code>：有边界的顾问讨论<a href="#126-council有边界的顾问讨论"><span>#</span></a></h3><p>适合重要架构选择、存在明显取舍的技术决策，或需要多个独立观点的问题。</p><p>典型流程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>主 agent 明确问题、范围和非目标</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>2–3 名顾问给出意见</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>主 agent 筛选并传递质疑，组织有限轮交叉检验</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>主 agent 输出最终决策备忘录</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>默认最多 2 个讨论 pass，可指定 2 或 3。</li>
<li>通常选择 2–3 名顾问，最多 4 名。</li>
<li>优先使用已配置的 <code>council-*</code> 自定义角色；没有足够角色时，通常使用 <code>oracle</code> 和 <code>reviewer</code> 补齐。</li>
<li>顾问不直接互聊，也不默认共享完整彼此对话。</li>
<li>主 agent 始终负责综合与最终决策。</li>
<li><code>council-*</code> 是可自行添加的角色配置，不是随包默认提供的一组 agent。</li>
</ul><p><strong>示例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/council 是否应该把当前单体拆成微服务？ --max-passes 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/council 比较两个迁移方案 --advisors oracle,reviewer --max-passes 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2.7 2 个命名工作流资源<a href="#127-2-个命名工作流资源"><span>#</span></a></h3><p>这两个不是上述斜杠模板，而是扩展提供的可按名字解析的工作流资源。</p>

<table><thead><tr><th>名称</th><th>功能</th><th>参数</th></tr></thead><tbody><tr><td><strong><code>review</code></strong></td><td>启动一个 <code>reviewer</code>，返回其审查输出。不是多 reviewer 并行审查。</td><td><code>args.task</code>：非空任务字符串</td></tr><tr><td><strong><code>run-ci</code></strong></td><td>执行受支持的非交互验证命令，收集有界日志和执行结果。</td><td><code>args.command</code>、可选 <code>args.timeoutMs</code></td></tr></tbody></table><p>对应工具调用示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>subagent</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>workflow</span><span>:</span><span> </span><span>"review"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>args</span><span>:</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>task</span><span>:</span><span> </span><span>"审查当前 diff，报告有证据的正确性问题"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>subagent</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>workflow</span><span>:</span><span> </span><span>"run-ci"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>cwd</span><span>:</span><span> </span><span>"/path/to/project"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>args</span><span>:</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>command</span><span>:</span><span> </span><span>"npm run typecheck"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>timeoutMs</span><span>:</span><span> </span><span>120000</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong><code>run-ci</code> 当前仅支持：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>npm test</span></div></div><div><div><div>2</div></div><div><span>npm run typecheck</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2.8 创建自定义工作流<a href="#128-创建自定义工作流"><span>#</span></a></h3><p>有两种主要方式：</p><ul>
<li><strong>Markdown 提示模板</strong>：适合自然语言流程、简单串行步骤、斜杠命令。</li>
<li><strong>JavaScript 工作流脚本</strong>：适合明确控制串行、并行、条件分支和结果汇总。</li>
</ul><p>文件位置：</p>

<table><thead><tr><th>内容</th><th>用户级，跨项目使用</th><th>项目级</th></tr></thead><tbody><tr><td>工作流提示模板</td><td><code>~/.pi/agent/prompts/&lt;名称&gt;.md</code></td><td><code>&lt;项目&gt;/.pi/prompts/&lt;名称&gt;.md</code></td></tr><tr><td>JavaScript 工作流</td><td>自选路径，例如 <code>~/.pi/agent/workflows/review.js</code></td><td>自选路径，例如 <code>&lt;项目&gt;/.pi/workflows/review.js</code></td></tr><tr><td>包资源加载配置</td><td><code>~/.pi/agent/settings.json</code></td><td><code>&lt;项目&gt;/.pi/settings.json</code></td></tr></tbody></table><p><strong>区别：</strong><code>prompts/</code> 下的直接 <code>.md</code> 子文件会被发现并变成模板命令；工作流 <code>.js</code> 文件不会因为放进某个目录就自动变成斜杠命令，需要显式指定路径调用。</p><section><h4>组合自定义工作流：Markdown 模板<a href="#组合自定义工作流markdown-模板"><span>#</span></a></h4><section><h5>方式 A：用自然语言描述完整流程<a href="#方式-a用自然语言描述完整流程"><span>#</span></a></h5><p>例如，创建：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>~/.pi/agent/prompts/implement-and-review.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内容：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>侦察、实现并独立审查一个已批准的任务</span></div></div><div><div><div>3</div></div><div><span>argument-hint</span><span>: </span><span>"&lt;任务&gt;"</span></div></div><div><div><div>4</div></div><div><span>---</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>针对以下任务，使用 pi-subagents：</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>$@</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>流程：</span></div></div><div><div><div>11</div></div><div><span><span>1.</span><span> 让 scout 检查相关代码，定位关键文件、约束和测试。</span></span></div></div><div><div><div>12</div></div><div><span><span>2.</span><span> 主 agent 汇总侦察结果。如果任务需要尚未批准的产品或架构决定，</span></span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>先询问用户，不直接实现。</span></div></div><div><div><div>14</div></div><div><span><span>3.</span><span> 让一个 worker 按已批准范围实现，并运行适当验证。</span></span></div></div><div><div><div>15</div></div><div><span><span>4.</span><span> 实现完成后，让两个 fresh-context reviewer 并行审查：</span></span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>-</span><span> 一个检查正确性和回归风险；</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>-</span><span> 一个检查测试覆盖和验证充分性。</span></div></div><div><div><div>18</div></div><div><span><span>5.</span><span> 主 agent 汇总有证据的问题，不盲目接受所有建议。</span></span></div></div><div><div><div>19</div></div><div><span><span>6.</span><span> 本轮仅报告审查结果，不自动追加修复。</span></span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>限制：</span></div></div><div><div><div>22</div></div><div><span><span>-</span><span> 同一工作区一次只允许一个 writer。</span></span></div></div><div><div><div>23</div></div><div><span><span>-</span><span> reviewer 不修改文件。</span></span></div></div><div><div><div>24</div></div><div><span><span>-</span><span> 最终报告修改文件、验证结果、审查发现及剩余风险。</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>保存后执行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/reload</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/implement-and-review 给登录接口增加速率限制，不改变现有响应格式</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里：</p><ul>
<li>文件名决定命令名：<code>implement-and-review.md</code> → <code>/implement-and-review</code>。</li>
<li><code>description</code>：命令说明。</li>
<li><code>argument-hint</code>：参数提示。</li>
<li><code>$@</code>：替换为调用时提供的全部参数。</li>
<li>正文：交给主 agent 的编排指导。</li>
</ul><div><div><div></div><div>Warning</div></div><div><p>这种模板不是固定执行图。主 agent 阅读说明后组织子 agent，可能会根据实际情况调整、暂停和澄清流程。</p></div></div></section><section><h5>方式 B：用 <code>/prompt-workflow</code> 串联多个模板<a href="#方式-b用-prompt-workflow-串联多个模板"><span>#</span></a></h5><p>如果希望每一步明确交给一个角色，可以拆成多个文件。</p><p><strong>1. 第一步：代码侦察</strong></p><p><code>~/.pi/agent/prompts/map-task.md</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>收集任务所需的代码上下文</span></div></div><div><div><div>3</div></div><div><span>subagent</span><span>: </span><span>scout</span></div></div><div><div><div>4</div></div><div><span>fresh</span><span>: </span><span>true</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>任务：$@</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>只收集上下文，不实现。</span></div></div><div><div><div>10</div></div><div><span>返回关键文件、接口、约束、测试位置和待确认事项。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>2. 第二步：执行已批准任务</strong></p><p><code>~/.pi/agent/prompts/build-task.md</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>根据上下文执行已批准任务</span></div></div><div><div><div>3</div></div><div><span>subagent</span><span>: </span><span>worker</span></div></div><div><div><div>4</div></div><div><span>fresh</span><span>: </span><span>true</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>已批准任务：$@</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>上一步的代码上下文：</span></div></div><div><div><div>10</div></div><div><span>{previous}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>执行最小必要修改并验证。</span></div></div><div><div><div>13</div></div><div><span>如遇到未批准的范围或架构选择，请向主 agent 请求决定。</span></div></div><div><div><div>14</div></div><div><span>返回修改文件、验证命令与结果、剩余风险。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>3. 第三步：独立审查</strong></p><p><code>~/.pi/agent/prompts/check-task.md</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>独立审查实现结果</span></div></div><div><div><div>3</div></div><div><span>subagent</span><span>: </span><span>reviewer</span></div></div><div><div><div>4</div></div><div><span>fresh</span><span>: </span><span>true</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>原任务：$@</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>实现者的交接说明：</span></div></div><div><div><div>10</div></div><div><span>{previous}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>直接检查当前文件和工作区 diff，不把交接说明当作正确性证明。</span></div></div><div><div><div>13</div></div><div><span>只报告有证据的问题，并注明文件、行号和最小修复建议。</span></div></div><div><div><div>14</div></div><div><span>不要修改文件。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>4. 总入口：串行组合</strong></p><p><code>~/.pi/agent/prompts/deliver-task.md</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>侦察、实现、审查</span></div></div><div><div><div>3</div></div><div><span>chain</span><span>: </span><span>map-task -&gt; build-task -&gt; check-task</span></div></div><div><div><div>4</div></div><div><span>---</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>通过串行步骤执行任务。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/prompt-workflow deliver-task "为已确定的登录方案补充速率限制" --bg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键字段与占位符：</p>

<table><thead><tr><th>字段或语法</th><th>含义</th></tr></thead><tbody><tr><td><code>subagent</code></td><td>该步骤使用的 agent；省略时默认是 <code>delegate</code>。</td></tr><tr><td><code>fresh: true</code></td><td>该步骤采用新上下文。</td></tr><tr><td><code>fork: true</code></td><td>该步骤采用继承的父会话上下文；不要同时设置 <code>fresh</code>。</td></tr><tr><td><code>model</code></td><td>可选，指定该步骤的模型。</td></tr><tr><td><code>skill</code></td><td>可选，选择该步骤需要的技能。</td></tr><tr><td><code>chain</code></td><td>用 <code>-&gt;</code> 连接模板名称，按顺序执行。</td></tr><tr><td><code>$@</code></td><td>用户传给总入口的参数，各步骤均可使用。</td></tr><tr><td><code>{previous}</code></td><td>上一步的输出，由适配器传给下一步。</td></tr><tr><td><code>--bg</code></td><td>后台运行，避免阻塞主聊天。</td></tr></tbody></table></section></section><section><h4>组合自定义工作流：JavaScript 脚本<a href="#组合自定义工作流javascript-脚本"><span>#</span></a></h4><p>需要并行或明确判断执行结果时，使用脚本更合适。</p><p>例如创建：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;项目&gt;/.pi/workflows/review-change.js</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内容：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>target</span><span> </span><span>=</span><span><span> </span><span>args</span><span>.</span></span><span>target</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>if</span><span> (</span><span>typeof</span><span><span> </span><span>target</span><span> </span></span><span>!==</span><span> </span><span>"string"</span><span> </span><span>||</span><span> </span><span>!</span><span><span>target</span><span>.</span></span><span>trim</span><span>()) {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>"必须提供非空的 args.target"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// 先串行侦察。</span></div></div><div><div><div>8</div></div><div><span>const</span><span> </span><span>scan</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>runs</span><span>.</span></span><span>run</span><span>(</span><span>"scan"</span><span>, {</span></div></div><div><div><div>9</div></div><div><span>  </span><span>label</span><span>:</span><span> </span><span>"梳理审查目标"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>agent</span><span>:</span><span> </span><span>"scout"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>context</span><span>:</span><span> </span><span>"fresh"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>task</span><span>:</span><span> </span><span>`检查 </span><span>${</span><span>target</span><span>}</span><span> 的当前改动，定位相关文件、约束和风险。不要实现。`</span></div></div><div><div><div>13</div></div><div><span>});</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>if</span><span> (</span><span>!</span><span><span>scan</span><span>.</span></span><span>ok</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>  </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>`侦察未成功完成：</span><span>${</span><span>scan</span><span>.</span><span>runId</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>// 再并行审查，runs.all 返回按输入顺序排列的数组。</span></div></div><div><div><div>20</div></div><div><span>const</span><span> </span><span>reviews</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>runs</span><span>.</span></span><span>all</span><span>([</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>22</div></div><div><span>    </span><span>key</span><span>:</span><span> </span><span>"correctness"</span><span>,</span></div></div><div><div><div>23</div></div><div><span>    </span><span>label</span><span>:</span><span> </span><span>"审查正确性"</span><span>,</span></div></div><div><div><div>24</div></div><div><span>    </span><span>agent</span><span>:</span><span> </span><span>"reviewer"</span><span>,</span></div></div><div><div><div>25</div></div><div><span>    </span><span>context</span><span>:</span><span> </span><span>"fresh"</span><span>,</span></div></div><div><div><div>26</div></div><div><span>    </span><span>task</span><span>:</span><span> [</span></div></div><div><div><div>27</div></div><div><span>      </span><span>`审查目标：</span><span>${</span><span>target</span><span>}</span><span>`</span><span>,</span></div></div><div><div><div>28</div></div><div><span>      </span><span>"只读检查正确性和回归风险。直接检查文件与当前 diff。"</span><span>,</span></div></div><div><div><div>29</div></div><div><span>      </span><span>"以下上下文仅用于定位，不作为正确性证明："</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>      </span></span><span>scan</span><span>.</span><span>output</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>].</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>34</div></div><div><span>    </span><span>key</span><span>:</span><span> </span><span>"tests"</span><span>,</span></div></div><div><div><div>35</div></div><div><span>    </span><span>label</span><span>:</span><span> </span><span>"审查测试覆盖"</span><span>,</span></div></div><div><div><div>36</div></div><div><span>    </span><span>agent</span><span>:</span><span> </span><span>"reviewer"</span><span>,</span></div></div><div><div><div>37</div></div><div><span>    </span><span>context</span><span>:</span><span> </span><span>"fresh"</span><span>,</span></div></div><div><div><div>38</div></div><div><span>    </span><span>task</span><span>:</span><span> [</span></div></div><div><div><div>39</div></div><div><span>      </span><span>`审查目标：</span><span>${</span><span>target</span><span>}</span><span>`</span><span>,</span></div></div><div><div><div>40</div></div><div><span>      </span><span>"只读检查测试覆盖、断言质量和验证缺口。"</span><span>,</span></div></div><div><div><div>41</div></div><div><span>      </span><span>"以下上下文仅用于定位："</span><span>,</span></div></div><div><div><div>42</div></div><div><span><span>      </span></span><span>scan</span><span>.</span><span>output</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>].</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>]);</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>return</span><span> {</span></div></div><div><div><div>48</div></div><div><span>  </span><span>ok</span><span>:</span><span><span> </span><span>reviews</span><span>.</span></span><span>every</span><span><span>(</span><span>result</span><span> </span></span><span>=&gt;</span><span><span> </span><span>result</span><span>.</span></span><span>ok</span><span>),</span></div></div><div><div><div>49</div></div><div><span>  </span><span>reviews</span><span>:</span><span><span> </span><span>reviews</span><span>.</span></span><span>map</span><span><span>((</span><span>result</span><span>, </span><span>index</span><span>) </span></span><span>=&gt;</span><span> ({</span></div></div><div><div><div>50</div></div><div><span>    </span><span>angle</span><span>:</span><span><span> </span><span>index</span><span> </span></span><span>===</span><span> </span><span>0</span><span> </span><span>?</span><span> </span><span>"correctness"</span><span> </span><span>:</span><span> </span><span>"tests"</span><span>,</span></div></div><div><div><div>51</div></div><div><span>    </span><span>ok</span><span>:</span><span><span> </span><span>result</span><span>.</span></span><span>ok</span><span>,</span></div></div><div><div><div>52</div></div><div><span>    </span><span>runId</span><span>:</span><span><span> </span><span>result</span><span>.</span></span><span>runId</span><span>,</span></div></div><div><div><div>53</div></div><div><span>    </span><span>output</span><span>:</span><span><span> </span><span>result</span><span>.</span></span><span>output</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>}))</span></div></div><div><div><div>55</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>让 Pi 调用它，对应工具参数是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>subagent</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>workflow</span><span>:</span><span> </span><span>"./.pi/workflows/review-change.js"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>cwd</span><span>:</span><span> </span><span>"/你的项目绝对路径"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>args</span><span>:</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>target</span><span>:</span><span> </span><span>"src/auth/ 的当前工作区改动"</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>7</div></div><div><span>  </span><span>async</span><span>:</span><span> </span><span>true</span></div></div><div><div><div>8</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>你可以直接说：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请运行 .pi/workflows/review-change.js，</span></div></div><div><div><div>2</div></div><div><span>target 是 src/auth/ 的当前工作区改动，后台执行。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也可以先要求只验证脚本、不启动子 agent：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>subagent</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>action</span><span>:</span><span> </span><span>"validate"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>workflow</span><span>:</span><span> </span><span>"./.pi/workflows/review-change.js"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>cwd</span><span>:</span><span> </span><span>"/你的项目绝对路径"</span></div></div><div><div><div>5</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>脚本中的核心概念：</p>

<table><thead><tr><th>API 或字段</th><th>用途</th></tr></thead><tbody><tr><td><code>runs.run(...)</code></td><td>启动一个具备稳定 key 的步骤。</td></tr><tr><td><code>runs.all([...])</code></td><td>并行执行步骤，返回有序数组。</td></tr><tr><td><code>runs.lanes([...])</code></td><td>多条路线并行，每条路线内部串行推进。</td></tr><tr><td><code>args</code></td><td>调用传入的只读 JSON 参数。</td></tr><tr><td><code>key</code></td><td>稳定的步骤标识，用于追踪和协调。</td></tr><tr><td><code>label</code></td><td>显示给用户的简短步骤名称。</td></tr><tr><td><code>result.ok</code></td><td>子 agent 是否成功完成；<strong>不等于代码一定正确或审查一定通过</strong>。</td></tr><tr><td><code>result.output</code></td><td>子 agent 返回的文本。</td></tr></tbody></table><p>脚本是扩展执行的 JavaScript 语句体：</p><ul>
<li>可以使用顶层 <code>await</code> 和 <code>return</code>。</li>
<li>没有任意文件系统、shell 或主机全局变量访问。</li>
<li>不要定义嵌套的 <code>async</code> 函数或 async 箭头函数。</li>
<li>每个子任务应明确范围、权限、验收条件和停止条件。</li>
<li>一般让外层工作流 <code>async: true</code>；<strong>不要为等待最终结果的子步骤显式设置 <code>async: true</code></strong>，否则它返回的可能只是启动回执，而不是完成结果。</li>
</ul></section></section></section></section>
<section><h1>2. 内部工作原理<a href="#2-内部工作原理"><span>#</span></a></h1><p>subagent 并不仅仅是插入主上下文的另一段提示。它是一个 <strong>独立的子 agent 会话</strong>。</p><p>假设你说：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Use scout to understand the authentication system.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在概念上，Pi 会做类似这样的事情：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Pi 主会话</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>│ subagent({</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>│   agent: "scout",</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>│   task: "Understand the authentication system"</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>│ })</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>▼</span></div></div><div><div><div>9</div></div><div><span>新的 Pi 子会话</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>├── scout system prompt</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>├── scout tool permissions</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>├── selected model</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>└── task</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>▼</span></div></div><div><div><div>17</div></div><div><span>探索 repository</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>▼</span></div></div><div><div><div>20</div></div><div><span>final report</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>▼</span></div></div><div><div><div>23</div></div><div><span>Pi 主会话继续 reasoning</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h2>2.1 Agent 的定义<a href="#21-agent-的定义"><span>#</span></a></h2><p>从根本上说，一个 agent 就是一个带 YAML frontmatter 的 Markdown 文件。例如，文档中给出的等价写法是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>name: scout</span></div></div><div><div><div>3</div></div><div><span>description: Fast codebase recon</span></div></div><div><div><div>4</div></div><div><span>tools: read, grep, find, ls</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Your system prompt goes here.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因此，一个 agent 定义会指定如下内容：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>role</span></div></div><div><div><div>2</div></div><div><span>system prompt</span></div></div><div><div><div>3</div></div><div><span>tools</span></div></div><div><div><div>4</div></div><div><span>model</span></div></div><div><div><div>5</div></div><div><span>thinking level</span></div></div><div><div><div>6</div></div><div><span>skills</span></div></div><div><div><div>7</div></div><div><span>extensions</span></div></div><div><div><div>8</div></div><div><span>output behavior</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因此你可以创建自己的专家 agent。</p><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.pi/agents/</span></div></div><div><div><div>2</div></div><div><span>├── database-expert.md</span></div></div><div><div><div>3</div></div><div><span>├── security-reviewer.md</span></div></div><div><div><div>4</div></div><div><span>├── python-teacher.md</span></div></div><div><div><div>5</div></div><div><span>└── frontend-reviewer.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>项目定义的 agent 可以覆盖运行时同名的打包/内置 agent。</p></section><section><h2>2.2 前台与后台 subagent<a href="#22-前台与后台-subagent"><span>#</span></a></h2><p><code>pi-subagents</code> 支持两种差异较大的执行模式。</p><p><strong>前台子会话（foreground child）</strong> 在父 Pi 进程内运行，父级会等待它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Parent</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├── launch reviewer</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│       ↓</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│    reviewing...</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│       ↓</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>│    result</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>▼</span></div></div><div><div><div>10</div></div><div><span>continue</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>后台子会话（background child）</strong> 通过一个脱离的 runner 运行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                    </span></span><span>┌─ reviewer A ──►</span></div></div><div><div><div>2</div></div><div><span>Parent ─────────────┼─ reviewer B ──►</span></div></div><div><div><div>3</div></div><div><span><span>                    </span></span><span>└─ reviewer C ──►</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>Parent can continue working</span></div></div><div><div><div>6</div></div><div><span>while those agents run.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它会维护异步运行状态、日志、产物和 run ID。Pi 的 TUI 会获得一个常驻的 <strong>FleetView</strong>，而 <code>/subagents-fleet</code> 提供了一个用于查看活动子会话的检查器。</p><p>你可以：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inspect a running child</span></div></div><div><div><div>2</div></div><div><span>read its transcript</span></div></div><div><div><div>3</div></div><div><span>steer it</span></div></div><div><div><div>4</div></div><div><span>stop it</span></div></div><div><div><div>5</div></div><div><span>wait for it</span></div></div><div><div><div>6</div></div><div><span>resume certain completed children</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/apai-01/</id>
      <title type="text">如何高效计算高维行列式与逆矩阵</title>
      <published>2026-10-01T00:00:00.000Z</published>
      <updated>2026-10-01T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/apai-01/"/>
      <summary type="text">当维度很大时，如何高效计算行列式和逆矩阵？如何高效计算矩阵-向量乘法？</summary>
      <content type="html"><![CDATA[<p>本篇主要回答下面这个问题：</p>
<div><div><div></div><div>Faq</div></div><div><p>当维度 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 很大时，如何高效计算 <span><span>det⁡(Σ)\det(\Sigma)</span><span><span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span></span></span></span> 和 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>？如何高效计算矩阵-向量乘法？</p></div></div>
<p>这个问题其实是<strong>两部分</strong>，而且第二句（矩阵-向量乘法）才是关键——它在暗示我们：<strong>我们根本不需要真的算出 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></strong>.</p>
<p>答案：用矩阵分解代替求逆。</p>
<p>对对称正定的 <span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 做 <strong>Cholesky 分解</strong> <span><span>Σ=LLT\Sigma=LL^T</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，于是</p>
<ul>
<li><span><span>det⁡(Σ)=(∏iLii)2\det(\Sigma)=(\prod_i L_{ii})^2</span><span><span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>∏</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，计算成本极小；</li>
<li><span><span>Σ−1v\Sigma^{-1}v</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>v</span></span></span></span> 通过两次三角求解（前代 + 回代）得到，每次 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，不用显式求逆。</li>
</ul>
<p>总代价：一次分解 <span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，之后每次查询 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。</p>
<section><h1>1. 如何高效计算 <span><span>det⁡(Σ)\det(\Sigma)</span><span><span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span></span></span></span> 和 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span><a href="#1-如何高效计算-detσdetsigmadetσ-和-σ1sigma-1σ1"><span>#</span></a></h1><section><h2>1.1 “直接求逆” 的坏处<a href="#11-直接求逆-的坏处"><span>#</span></a></h2><p>朴素做法有三宗罪：</p>

<table><thead><tr><th>做法</th><th>代价</th><th>问题</th></tr></thead><tbody><tr><td>显式算 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>（如 Gauss–Jordan）</td><td><span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td>数值不稳定；多花 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 内存</td></tr><tr><td>直接展开算 <span><span>det⁡(Σ)\det(\Sigma)</span><span><span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span></span></span></span></td><td>比 <span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 还糟</td><td>大 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 下<strong>上溢/下溢</strong></td></tr><tr><td>然后矩阵-向量乘</td><td>每次 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td>但其实分解后能更省</td></tr></tbody></table><section><h3>1.1.1 为什么显式求逆是 <span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span>？<a href="#111-为什么显式求逆是-od3od3od3"><span>#</span></a></h3><p>我们可以数”扫过矩阵的次数”：消到第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个主元时，要把主元下方约 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 行的该列清零，每次行变换要更新整行（约 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 个数），故单个主元 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>；共 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 个主元：</p><span><span><span>d⏟主元个数×O(d2)⏟单主元工作量=O(d3)\underbrace{d}_{\text{主元个数}}\times\underbrace{O(d^2)}_{\text{单主元工作量}}=O(d^3)</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>主元个数</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>单主元工作量</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>更精确地，待消的块逐次缩小，<span><span>∑k=1dk2≈13d3\sum_{k=1}^{d}k^2\approx\tfrac13 d^3</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>k</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>，加上回代共约 <span><span>23d3\tfrac23 d^3</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>——这正是 Gauss 消元 / LU 是 <span><span>23d3\tfrac23 d^3</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 的来源。</p><hr /><p>关于避免直接求逆，最核心的原因在于：<strong>在实际问题里，我们几乎从不需要 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 本身</strong>，我们需要的只是它能作用于一个向量：</p><span><span><span>Σ−1v,以及det⁡(Σ) (或 log⁡det⁡Σ).\Sigma^{-1}v,\qquad \text{以及}\qquad \det(\Sigma)\ (\text{或}\ \log\det\Sigma).</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>v</span><span>,</span><span></span><span></span><span><span>以及</span></span><span></span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span><span> </span><span>(</span><span><span>或</span></span><span> </span><span></span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span>Σ</span><span>)</span><span>.</span></span></span></span></span><p>也就是说，所有用到 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的公式，都会以上面两种形式出现。我们以多元高斯对数密度为例：</p><span><span><span>log⁡p(x)=−d2log⁡(2π)−12log⁡det⁡Σ⏟标量−12(x−μ)TΣ−1(x−μ)⏟一次"咬向量"\log p(x)=-\frac d2\log(2\pi)-\frac12\underbrace{\log\det\Sigma}_{\text{标量}}-\frac12\underbrace{(x-\mu)^T\Sigma^{-1}(x-\mu)}_{\text{一次"咬向量"}}</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>p</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span>(</span><span>2</span><span>π</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span><span>标量</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span>Σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span><span>一次</span><span>"</span><span>咬向量</span><span>"</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>拆开看第二项：先算 <span><span>u=Σ−1(x−μ)u=\Sigma^{-1}(x-\mu)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span></span></span></span>，再算 <span><span>(x−μ)Tu(x-\mu)^T u</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>u</span></span></span></span>（一个内积）。整个过程只用了”作用一次”的结果，从来没有需要逆矩阵本身。</p><p>于是就可以<strong>跳过求逆</strong>，改用分解（Cholesky）：</p><span><span><span>Σ=LLT  ⟹  Σ−1v 变成解 Ly=v 再解 LTx=y\Sigma = LL^T \;\Longrightarrow\; \Sigma^{-1}v \text{ 变成解 } Ly=v \text{ 再解 } L^Tx=y</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>v</span><span><span> </span><span>变成解</span><span> </span></span><span>L</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span><span><span> </span><span>再解</span><span> </span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>y</span></span></span></span></span><p>下面我们将详细介绍 Cholesky 分解。</p></section></section><section><h2>1.2 Cholesky 分解<a href="#12-cholesky-分解"><span>#</span></a></h2><div><div><div></div><div>Cholesky Decomposition</div></div><div><p>设 <span><span>A∈Rd×dA\in\mathbb{R}^{d\times d}</span><span><span><span></span><span>A</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>d</span><span>×</span><span>d</span></span></span></span></span></span></span></span></span></span></span></span> <strong>对称正定</strong>（symmetric positive definite, SPD），即</p><span><span><span>A=AT,xTAx&gt;0对所有 0≠x∈RdA=A^T,\qquad \mathbf{x}^TA\mathbf{x}&gt;0\quad \text{对所有 } \mathbf{0}\neq\mathbf{x}\in\mathbb{R}^d</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span><span></span><span><span>对所有</span><span> </span></span><span>0</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>x</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span></span></span></span></span></span></span></span></span><p>则存在<strong>唯一</strong>的下三角矩阵 <span><span>L∈Rd×dL\in\mathbb{R}^{d\times d}</span><span><span><span></span><span>L</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>d</span><span>×</span><span>d</span></span></span></span></span></span></span></span></span></span></span></span>，其对角元<strong>全为正</strong>（<span><span>Lii&gt;0L_{ii}&gt;0</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>），使得</p><span><span><span>A=LLTA=LL^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 称为 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的 <strong>Cholesky 因子</strong>（Cholesky factor）。</p><p>更详细的介绍参见 <a href="https://en.wikipedia.org/wiki/Cholesky_decomposition" target="_blank">Cholesky Decomposition</a>.</p></div></div><p>协方差矩阵 <span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 对称正定，正好命中 Cholesky 的适用条件：</p><span><span><span>Σ=LLT,L 为下三角\Sigma=LL^T,\qquad L\text{ 为下三角}</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>L</span><span><span> </span><span>为下三角</span></span></span></span></span></span><p>下面我们用一个例子来展示：</p><p>我们的目标是求解方程</p><p><span><span>Σx=v\Sigma x = v</span><span><span><span></span><span>Σ</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span></p><p>其中</p><span><span><span>Σ=[4223]  ⟹  L=[2012],LLT=[2012][2102]=[4223]  ✓\Sigma=\begin{bmatrix}4&amp;2\\2&amp;3\end{bmatrix}
\;\Longrightarrow\;
L=\begin{bmatrix}2&amp;0\\1&amp;\sqrt2\end{bmatrix},
\qquad
LL^T=\begin{bmatrix}2&amp;0\\1&amp;\sqrt2\end{bmatrix}\begin{bmatrix}2&amp;1\\0&amp;\sqrt2\end{bmatrix}=\begin{bmatrix}4&amp;2\\2&amp;3\end{bmatrix}\;\checkmark</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span></span><span>✓</span></span></span></span></span><p><span><span>v=(1,0)v=(1,0)</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span></p><p>我们有</p><p><span><span>Σx=v⟺LLTx=v\Sigma x = v \quad\Longleftrightarrow\quad LL^Tx=v</span><span><span><span></span><span>Σ</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span><span></span><span></span><span>⟺</span><span></span><span></span></span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span></p><p>设 <span><span>y=LTxy=L^Tx</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>，于是要解 <span><span>Σx=v\Sigma x = v</span><span><span><span></span><span>Σ</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span>，等价于解：</p><span><span><span>{Ly=v（前代）LTx=y（回代）\begin{cases}
Ly = v &amp;\text{（前代）}\\
L^T x = y &amp;\text{（回代）}
\end{cases}</span><span><span><span></span><span><span><span>{</span></span><span><span><span><span><span><span><span><span></span><span><span>L</span><span>y</span><span></span><span>=</span><span></span><span>v</span></span></span><span><span></span><span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>（前代）</span></span></span></span><span><span></span><span><span><span>（回代）</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span></span><p>第一步前代：<span><span>Ly=vLy=v</span><span><span><span></span><span>L</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span> 展开成</p><span><span><span>[2012][y1y2]=[10]⇓{2y1=11⋅y1+2 y2=0\begin{align}
\begin{bmatrix}2&amp;0\\1&amp;\sqrt2\end{bmatrix}\begin{bmatrix}y_1\\y_2\end{bmatrix}&amp;= \begin{bmatrix}1\\0\end{bmatrix}\\
&amp;\Downarrow \\
&amp;\begin{cases}
2y_1 = 1\\
1\cdot y_1 + \sqrt2\,y_2 = 0
\end{cases}
\end{align}</span><span><span><span></span><span><span><span><span><span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span><span><span></span><span><span></span><span></span><span>⇓</span></span></span><span><span></span><span><span></span><span></span><span><span><span>{</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>1</span></span></span><span><span></span><span><span>1</span><span></span><span>⋅</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><p>我们可以轻松解出来</p><span><span><span>{y1=0.5y2=−0.3536\begin{cases}
y_1 = 0.5\\
y_2 = -0.3536
\end{cases}</span><span><span><span></span><span><span><span>{</span></span><span><span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>0.5</span></span></span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>−</span><span>0.3536</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span></span><p>第二步回代：<span><span>LTx=yL^Tx=y</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>y</span></span></span></span> 展开（<span><span>LT=[2102]L^T=\begin{bmatrix}2&amp;1\\0&amp;\sqrt2\end{bmatrix}</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>）</p><span><span><span>{2x1+1⋅x2=0.52 x2=−0.3536\begin{cases}
2x_1 + 1\cdot x_2 = 0.5\\
\sqrt2\,x_2 = -0.3536
\end{cases}</span><span><span><span></span><span><span><span>{</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>1</span><span></span><span>⋅</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>0.5</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>−</span><span>0.3536</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span></span><p>然后算出 <span><span>x1=0.375,x2=−0.25x_1=0.375,x_2=-0.25</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.375</span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.25</span></span></span></span>.</p><p>简单验算一下：</p><span><span><span>Σ−1=18[3−2−24],Σ−1[10]=[0.375−0.25]  ✓\Sigma^{-1}=\frac{1}{8}\begin{bmatrix}3&amp;-2\\-2&amp;4\end{bmatrix},
\qquad
\Sigma^{-1}\begin{bmatrix}1\\0\end{bmatrix}=\begin{bmatrix}0.375\\-0.25\end{bmatrix}
\;\checkmark</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>8</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0.375</span></span></span><span><span></span><span><span>−</span><span>0.25</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span></span><span>✓</span></span></span></span></span><p>可以看到，<strong>我们从头到尾没有算过 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></strong>，但拿到了 <span><span>Σ−1v\Sigma^{-1}v</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>v</span></span></span></span>.</p><hr /><p>对于 <span><span>det⁡\det</span><span><span><span></span><span>det</span></span></span></span> 的计算也是一样的。我们知道，三角矩阵的行列式 = <strong>对角元之积</strong>（这是三角矩阵的好性质之一）。再由 <span><span>det⁡(LLT)=det⁡L⋅det⁡L\det(LL^T)=\det L\cdot\det L</span><span><span><span></span><span>det</span><span>(</span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>L</span><span></span><span>⋅</span><span></span></span><span><span></span><span>det</span><span></span><span>L</span></span></span></span>：</p><span><span><span>det⁡(Σ)=(∏iLii)2\det(\Sigma)=\Big(\prod_i L_{ii}\Big)^2</span><span><span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>(</span></span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∏</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span>)</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 已经在第 1 步算好了，所以取行列式只是把 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 个数乘一乘：<span><span>O(d)O(d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>)</span></span></span></span>。</p><div><div><div></div><div>在实际代码中取 <span><span>log⁡\log</span><span><span><span></span><span>lo<span>g</span></span></span></span></span></div></div><div></div></div><p><span><span>det⁡\det</span><span><span><span></span><span>det</span></span></span></span> 这个数<strong>会爆炸或消失</strong>。举个例子：<span><span>d=1000d=1000</span><span><span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>1000</span></span></span></span>、每个 <span><span>Lii=2L_{ii}=2</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，那么</p><span><span><span>det⁡(Σ)=22000≈10602\det(\Sigma)=2^{2000}\approx10^{602}</span><span><span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>2000</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>602</span></span></span></span></span></span></span></span></span></span></span></span></span><p>而计算机的双精度浮点数最大只能表示到约 <span><span>1.8×103081.8\times10^{308}</span><span><span><span></span><span>1.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>308</span></span></span></span></span></span></span></span></span></span></span></span>——<strong>直接溢出了，变成 <code>inf</code></strong>（反过来，<span><span>LiiL_{ii}</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小于 1 时会一路下溢成 0，信息全丢）。但取对数：</p><span><span><span>log⁡det⁡(Σ)=2×1000×ln⁡2≈1386.3\log\det(\Sigma)=2\times1000\times\ln 2\approx1386.3</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>det</span><span>(</span><span>Σ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1000</span><span></span><span>×</span><span></span></span><span><span></span><span>ln</span><span></span><span>2</span><span></span><span>≈</span><span></span></span><span><span></span><span>1386.3</span></span></span></span></span><p>所以，在实际代码里（比如算高斯密度时）<strong>永远取 <span><span>log⁡\log</span><span><span><span></span><span>lo<span>g</span></span></span></span></span> 再往下算</strong>，这也是 “机器学习的 log-likelihood 为什么到处是 log” 的一个技术原因。</p></section><section><h2>1.3 回到高斯密度：整套流程<a href="#13-回到高斯密度整套流程"><span>#</span></a></h2><p>把上面拼起来，多元正态密度的计算变成：</p><span><span><span>log⁡pX(x)=−d2log⁡(2π)−12log⁡det⁡Σ⏟由 L 得来−12(x−μ)TΣ−1(x−μ)⏟由两次三角求解得来\log p_X(x)=-\frac d2\log(2\pi)-\underbrace{\frac12\log\det\Sigma}_{\text{由 }L\text{ 得来}}-\underbrace{\frac12(x-\mu)^T\Sigma^{-1}(x-\mu)}_{\text{由两次三角求解得来}}</span><span><span><span></span><span>lo<span>g</span></span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>X</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span>(</span><span>2</span><span>π</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>由</span><span> </span></span><span>L</span><span><span> </span><span>得来</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span>Σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>由两次三角求解得来</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><ul>
<li><strong>一次 Cholesky</strong> <span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，之后可<strong>重复使用</strong>；</li>
<li>每个样本：<span><span>log⁡det⁡\log\det</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>det</span></span></span></span> 是 <span><span>O(d)O(d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>)</span></span></span></span>，马氏距离 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。</li>
</ul><div><div><div></div><div>两个阶段</div></div><div></div></div><p>上面给出了两种不同的时间复杂度，对应计算过程中的两个阶段：</p>

<table><thead><tr><th>阶段</th><th>依赖什么</th><th>要做几次</th><th>成本</th></tr></thead><tbody><tr><td>① 准备：对 <span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 做 Cholesky，得到 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span></td><td>只依赖<strong>分布</strong>（<span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span>）</td><td><strong>一次</strong></td><td><span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td></tr><tr><td>② 查询：给一个数据点 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>，算 <span><span>log⁡p(x)\log p(x)</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>p</span><span>(</span><span>x</span><span>)</span></span></span></span></td><td>每个<strong>样本</strong>各不相同</td><td><strong><span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 次</strong>（<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个样本）</td><td>每次 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td></tr></tbody></table><p>单个样本的公式是</p><span><span><span>log⁡p(x)=−d2log⁡(2π)−12log⁡det⁡Σ⏟常数项−12(x−μ)TΣ−1(x−μ)⏟马氏距离（Mahalanobis distance）\log p(x)=-\frac d2\log(2\pi)-\frac12\underbrace{\log\det\Sigma}_{\text{常数项}}-\frac12\underbrace{(x-\mu)^T\Sigma^{-1}(x-\mu)}_{\text{马氏距离（Mahalanobis distance）}}</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>p</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span>(</span><span>2</span><span>π</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span><span>常数项</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span>Σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span><span>马氏距离（</span><span>Mahalanobis distance</span><span>）</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><ol>
<li>
<p><strong>第一项</strong> <span><span>−d2log⁡(2π)-\frac d2\log(2\pi)</span><span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span>(</span><span>2</span><span>π</span><span>)</span></span></span></span>：纯常数，<span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>。</p>
</li>
<li>
<p><strong>第二项</strong> <span><span>log⁡det⁡Σ=2∑ilog⁡Lii\log\det\Sigma=2\sum_i\log L_{ii}</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>lo<span>g</span></span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：把 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 个对角元取对数再加起来，<span><span>O(d)O(d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>)</span></span></span></span>.
注意它<strong>根本不含 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span></strong>——对所有样本都一样，所以严格说算一次就够了（之后缓存成一个数，每次查表 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>）。</p>
</li>
<li>
<p><strong>第三项（马氏距离）</strong> 才是真正”每个样本都要重算”的部分：</p>
</li>
</ol><span><span><span>(x−μ)TΣ−1(x−μ)(x-\mu)^T\Sigma^{-1}(x-\mu)</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span></span></span></span></span><p>怎么算？<strong>不碰 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></strong>，而是解两次三角方程：</p><span><span><span>1◯令 b=x−μ  →  2◯解 Ly=b  (O(d2))  →  3◯解 LTu=y  (O(d2))  →  4◯算 bTu  (O(d))\text{\textcircled{1}令 } b=x-\mu \;\to\; \text{\textcircled{2}解 } Ly=b \;(O(d^2)) \;\to\; \text{\textcircled{3}解 } L^Tu=y\;(O(d^2)) \;\to\; \text{\textcircled{4}算 } b^Tu\;(O(d))</span><span><span><span></span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>◯</span></span></span></span></span></span></span><span>令</span><span> </span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span></span><span></span><span>→</span><span></span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>◯</span></span></span></span></span></span></span><span>解</span><span> </span></span><span>L</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>(</span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>))</span><span></span><span></span><span>→</span><span></span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>◯</span></span></span></span></span></span></span><span>解</span><span> </span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>y</span><span></span><span>(</span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>))</span><span></span><span></span><span>→</span><span></span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>◯</span></span></span></span></span></span></span><span>算</span><span> </span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>u</span><span></span><span>(</span><span>O</span><span>(</span><span>d</span><span>))</span></span></span></span></span><p>合计 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。<strong>每来一个新样本，就重复一次这个 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 的流程。</strong></p></section><section><h2>1.4 当 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 极大、连 <span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 都受不了时<a href="#14-当-ddd-极大连-od3od3od3-都受不了时"><span>#</span></a></h2><p>这是真正的”大维度”对策，和我们后面将要介绍的 SVD / 低秩 相关：</p><p><strong>(a) 低秩 + 对角结构（Woodbury 恒等式）</strong></p><p>若</p><span><span><span>Σ=D+UUT,D 对角, U∈Rd×k, k≪d\Sigma = D + UU^T,\qquad D\text{ 对角},\ U\in\mathbb R^{d\times k},\ k\ll d</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>D</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>D</span><span><span> </span><span>对角</span></span><span>,</span><span> </span><span></span><span>U</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>d</span><span>×</span><span>k</span></span></span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span>k</span><span></span><span>≪</span><span></span></span><span><span></span><span>d</span></span></span></span></span><p>则用 <strong>Woodbury 矩阵恒等式</strong>：</p><span><span><span>Σ−1=D−1−D−1U(I+UTD−1U)−1UTD−1\Sigma^{-1}=D^{-1}-D^{-1}U\big(I+U^T D^{-1}U\big)^{-1}U^T D^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>U</span><span><span>(</span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>U</span><span><span><span>)</span></span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><span><span><span>log⁡det⁡Σ=∑ilog⁡Dii+log⁡det⁡ ⁣(I+UTD−1U)\log\det\Sigma=\sum_i\log D_{ii}+\log\det\!\big(I+U^T D^{-1}U\big)</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>lo<span>g</span></span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>lo<span>g</span></span><span></span><span>det</span><span></span><span></span><span><span>(</span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>U</span><span><span>)</span></span></span></span></span></span><p>所有昂贵的运算都只作用在<strong>小矩阵</strong> <span><span>I+UTD−1UI+U^TD^{-1}U</span><span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>U</span></span></span></span>（<span><span>k×kk\times k</span><span><span><span></span><span>k</span><span></span><span>×</span><span></span></span><span><span></span><span>k</span></span></span></span>）上，代价降到 <span><span>O(dk2)O(dk^2)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span><span>k</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。</p><p><strong>(b) 稀疏 / 带状 / 层次结构</strong></p><p>若 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>（精度矩阵）稀疏（如高斯随机场、稀疏高斯过程），就用<strong>稀疏 Cholesky / 稀疏 LU</strong>，代价 <span><span>≈O(d1.5)\approx O(d^{1.5})</span><span><span><span></span><span>≈</span><span></span></span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>1.5</span></span></span></span></span></span></span></span></span><span>)</span></span></span></span> 甚至 <span><span>O(d)O(d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>)</span></span></span></span>。</p><p><strong>(c) 随机化方法</strong></p><p>用随机化 SVD 求低秩近似——这正是 Lec2 结尾预告的”randomized SVD”。</p><p><strong>(d) 不要 <span><span>det⁡\det</span><span><span><span></span><span>det</span></span></span></span> 本身，只要它的对数</strong> —— 之前已说明。</p></section></section>
<section><h1>2. 如何高效计算矩阵-向量乘法<a href="#2-如何高效计算矩阵-向量乘法"><span>#</span></a></h1><section><h2>2.1 稠密一般矩阵：<span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 是下界<a href="#21-稠密一般矩阵od2od2od2-是下界"><span>#</span></a></h2><p>对<strong>一般稠密</strong> <span><span>A∈Rd×dA\in\mathbb R^{d\times d}</span><span><span><span></span><span>A</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>d</span><span>×</span><span>d</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>y=Avy=Av</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>v</span></span></span></span> 需要</p><span><span><span>yi=∑j=1dAijvjy_i=\sum_{j=1}^d A_{ij}v_j</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>你必须<strong>碰到矩阵的每一个元素</strong>，所以 <span><span>FLOPs=O(d2)\text{FLOPs}=O(d^2)</span><span><span><span></span><span><span>FLOPs</span></span><span></span><span>=</span><span></span></span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 是<strong>渐近最优</strong>的，无法再降。</p><div><div><div></div><div><span><span>FLOPs\text{FLOPs}</span><span><span><span></span><span><span>FLOPs</span></span></span></span></span></div></div><div><p>FLOPs = Floating-point Operations（浮点运算次数），即做了多少次的浮点数加减乘除。</p><p>在上面的例子中，计算某个 <span><span>yiy_i</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 需要 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 次乘法 + <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 次加法，一共有 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 个 <span><span>yiy_i</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ，所以总运算量约为 <span><span>d×2d=2d2d\times 2d=2d^2</span><span><span><span></span><span>d</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，用大 <span><span>OO</span><span><span><span></span><span>O</span></span></span></span> 记就是 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>.</p></div></div><p>那”高效”从哪来？<strong>硬件层面</strong>：</p><ul>
<li>矩阵-向量乘法<strong>算术强度低</strong>（每读一个矩阵元素只做一次乘法+一次加法），是<strong>内存带宽瓶颈</strong>；</li>
<li>优化手段：cache 分块（blocking）、向量化、利用对称性（<span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 对称 → 只存一半）；</li>
<li><strong>批量</strong>做多个向量：把 <span><span>v1,…,vNv_1,\ldots,v_N</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>N</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 拼成矩阵，变成 <strong>Level 3 矩阵-矩阵乘法</strong>，算术强度大幅提升，性能天差地别。</li>
</ul></section><section><h2>2.2 利用结构：真正能把指数降下来的地方<a href="#22-利用结构真正能把指数降下来的地方"><span>#</span></a></h2>

<table><thead><tr><th>矩阵结构</th><th>朴素</th><th>结构化后</th><th>手段</th></tr></thead><tbody><tr><td>一般稠密</td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td>BLAS、分块、向量化</td></tr><tr><td>对称</td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>12O(d2)\tfrac12 O(d^2)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td>只存/算一半</td></tr><tr><td>低秩 <span><span>A=UVTA=UV^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>O(dk)O(dk)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>k</span><span>)</span></span></span></span></td><td>先算 <span><span>UTvU^Tv</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span></span></span></span>，再乘 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span></td></tr><tr><td>带状（带宽 <span><span>ww</span><span><span><span></span><span>w</span></span></span></span>）</td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>O(dw)O(dw)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>w</span><span>)</span></span></span></span></td><td>只遍历带宽</td></tr><tr><td>稀疏（<span><span>nnznnz</span><span><span><span></span><span>nn</span><span>z</span></span></span></span> 非零）</td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>O(nnz)O(nnz)</span><span><span><span></span><span>O</span><span>(</span><span>nn</span><span>z</span><span>)</span></span></span></span></td><td>稀疏存储 + SpMV</td></tr><tr><td>Toeplitz/循环</td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>O(dlog⁡d)O(d\log d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span></span><span>lo<span>g</span></span><span></span><span>d</span><span>)</span></span></span></span></td><td>用 FFT 卷积</td></tr><tr><td>层次/H-矩阵</td><td><span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>O(dlog⁡d)O(d\log d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span></span><span>lo<span>g</span></span><span></span><span>d</span><span>)</span></span></span></span> 或 <span><span>O(d)O(d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>)</span></span></span></span></td><td>低秩分块</td></tr></tbody></table><p><strong>回到高斯</strong>：<span><span>Σ−1v\Sigma^{-1}v</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>v</span></span></span></span> 之所以能”高效”，本质就是<strong>把 <span><span>Σ−1\Sigma^{-1}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 看成”三角分解的作用”而不是一个显式矩阵</strong>——用两次 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 的三角求解，取代一次 <span><span>O(d3)O(d^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 求逆 + 一次 <span><span>O(d2)O(d^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 乘法。</p></section><section><h2>2.3 近似计算<a href="#23-近似计算"><span>#</span></a></h2><p>若允许近似：</p><ul>
<li><strong>随机化投影 / Johnson–Lindenstrauss</strong>：把 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 维投影到低维再做；</li>
<li><strong>随机化 SVD</strong>（后续内容）：用少量矩阵-向量乘逼近低秩结构；</li>
<li>采样/草图（sketching）：<span><span>O(d)O(d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span>)</span></span></span></span> 或 <span><span>O(dlog⁡d)O(d\log d)</span><span><span><span></span><span>O</span><span>(</span><span>d</span><span></span><span>lo<span>g</span></span><span></span><span>d</span><span>)</span></span></span></span>。</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/linear-algebra-01/</id>
      <title type="text">正定矩阵与半正定矩阵</title>
      <published>2026-09-30T00:00:00.000Z</published>
      <updated>2026-09-30T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/linear-algebra-01/"/>
      <summary type="text">简要概括正定与半正定矩阵的概念</summary>
      <content type="html"><![CDATA[<p>正定矩阵和半正定矩阵是线性代数和优化理论中至关重要的概念。它们深刻地揭示了二次型（quadratic forms）的代数性质和与之相关的几何形状。</p>
<p>我们首先可以用一个简单的类比来理解：</p>
<ul>
<li>正定矩阵（Positive Definite Matrix, PD）就像是一个<strong>正数</strong>，对于任何非零向量 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> ，其对应的二次型 <span><span>xTAx\mathbf{x}^TA \mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 的结果总是严格 <span><span>&gt;0&gt;0</span><span><span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> .</li>
<li>半正定矩阵（Positive Semi-definite Matrix, PSD）就像是一个<strong>非负数</strong>，对于任何非零向量 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> ，其对应的二次型 <span><span>xTAx\mathbf{x}^TA \mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 的结果总是 <span><span>≥0\ge 0</span><span><span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span> .</li>
</ul>
<p>正式定义如下：</p>
<p>A symmetric matrix <span><span>A∈Rn×nA\in \mathbb{R}^{n\times n}</span><span><span><span></span><span>A</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span><span>×</span><span>n</span></span></span></span></span></span></span></span></span></span></span></span> is called <em>positive definite</em> (PD) if <span><span>xTAx&gt;0\mathbf{x}^TA \mathbf{x}&gt;0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> <span><span>∀ 0≠x∈Rn\forall\, \mathbf{0}\neq \mathbf{x}\in \mathbb{R}^{n}</span><span><span><span></span><span>∀</span><span></span><span>0</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>x</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span></span></span></span></span></span></span></span></span></span></span></span>.</p>
<p>A symmetric matrix <span><span>A∈Rn×nA\in \mathbb{R}^{n\times n}</span><span><span><span></span><span>A</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span><span>×</span><span>n</span></span></span></span></span></span></span></span></span></span></span></span> is called <em>positive semi-definite</em> (PSD) if <span><span>xTAx≥0\mathbf{x}^TA \mathbf{x}\ge0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span> <span><span>∀ 0≠x∈Rn\forall\, \mathbf{0}\neq \mathbf{x}\in \mathbb{R}^{n}</span><span><span><span></span><span>∀</span><span></span><span>0</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>x</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span></span></span></span></span></span></span></span></span></span></span></span>.</p>
<p>关于我们<strong>如何证明</strong>一个矩阵是半正定或是正定，有三种方法：</p>
<ol>
<li>用定义证。</li>
<li>对于 PD ，其所有的<strong>特征值</strong>都 <span><span>&gt;0&gt;0</span><span><span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>. 对于 PSD ，其所有<strong>特征值</strong>都 <span><span>≥0\ge 0</span><span><span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>.</li>
<li>对于 PD ，其所有主子矩阵（principal submatrix）的<strong>行列式都为正</strong>，也可以说所有的首项顺序主子式（leading principal minor）都为正。
<ul>
<li>但要注意，对于 <strong>PSD 没有后面那条性质</strong>，也就是说如果矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的所有首项顺序主子式都为非负，不能说矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是半正定矩阵。</li>
<li>反例：<span><span>A=[000−1]A=\begin{bmatrix}0 &amp; 0 \\ 0 &amp; -1\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> ，其所有首项顺序主子式都为非负（0），但因为 <span><span>(0,−1)[000−1](0−1)=−1&lt;0(0,-1)\begin{bmatrix}0 &amp; 0 \\ 0 &amp; -1\end{bmatrix} \begin{pmatrix}0 \\ -1\end{pmatrix}=-1&lt;0</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>(</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>)</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ，所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 并不是一个半正定矩阵。</li>
<li>实际上，对于 PSD ，有：其所有主子矩阵（principal submatrix）的<strong>行列式都为非负</strong>，即所有的顺序主子式（principal minor）为非负。</li>
</ul>
</li>
</ol>
<hr />
<p>关于最后一个方法，所谓的<strong>首项主子矩阵</strong>就如下图：</p>
<p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/241becf7a6b013f39b6d5286e1c85cc2.png" alt="" /></p>
<p>图中的每一个框都是一个首项主子矩阵，以此类推直到整个矩阵。</p>
<p>所以，所有的首项主子矩阵也可以写成下述形式</p>
<span><span><span>Ak=[a11a12a13⋯a1ka21a22a23⋯a2ka31a32a33⋯a3k⋮⋮⋮⋮⋮ak1ak2ak3⋯akk]k×k∀ k=1,2,…,nA_k=\begin{bmatrix}
a_{11} &amp; a_{12} &amp; a_{13} &amp; \cdots &amp; a_{1k} \\
a_{21} &amp; a_{22} &amp; a_{23} &amp; \cdots &amp; a_{2k} \\
a_{31} &amp; a_{32} &amp; a_{33} &amp; \cdots &amp; a_{3k} \\
\vdots &amp; \vdots &amp; \vdots &amp; \vdots &amp; \vdots \\
a_{k1} &amp; a_{k2} &amp; a_{k3} &amp; \cdots &amp; a_{kk}
\end{bmatrix}_{k \times k} \enspace \forall\, k =1,2,\ldots,n</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>k</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>k</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>23</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>33</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>k</span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>1</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>2</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>3</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>k</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>×</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span></span><span>∀</span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>n</span></span></span></span></span>
<p>而所谓的<strong>首项顺序主子式</strong>就是首项主子矩阵的行列式。</p>
<p>对于一般的<strong>主子矩阵</strong>，指的是删除相同的行和列而形成的子矩阵；而一般的<strong>顺序主子式</strong>就是主子矩阵的行列式。</p>
<hr />
<p>关于最后一个方法，所谓的<strong>对称子矩阵</strong>定义如下：</p>
<p>A submatrix <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> of <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> is called symmetric if it is of the form</p>
<span><span><span>B=[ai1i1ai1i2ai1i3⋯ai1ikai2i1ai2i2ai2i3⋯ai2ikai3i1ai3i2ai3i3⋯ai3ik⋮⋮⋮⋮⋮aiki1aiki2aiki3⋯aikik]k×k  ⟺  aijis an entry of B iffajiis also an entry of B.\begin{align}
&amp;B=\begin{bmatrix}
a_{i_1i_1} &amp; a_{i_1i_2} &amp; a_{i_1i_3} &amp; \cdots &amp; a_{i_1i_k} \\
a_{i_2i_1} &amp; a_{i_2i_2} &amp; a_{i_2i_3} &amp; \cdots &amp; a_{i_2i_k} \\
a_{i_3i_1} &amp; a_{i_3i_2} &amp; a_{i_3i_3} &amp; \cdots &amp; a_{i_3i_k} \\
\vdots &amp; \vdots &amp; \vdots &amp; \vdots &amp; \vdots \\
a_{i_ki_1} &amp; a_{i_ki_2} &amp; a_{i_ki_3} &amp; \cdots &amp; a_{i_ki_k}
\end{bmatrix}_{k \times k} \\
&amp; \\
&amp; \iff a_{ij}\enspace \text{is an entry of $B$ iff}\enspace a_{ji} \enspace \text{is also an entry of $B$.}
\end{align}</span><span><span><span></span><span><span><span><span><span><span><span></span><span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span>B</span><span></span><span>=</span><span></span><span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>×</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span></span></span></span><span><span></span><span><span></span><span></span><span></span><span>⟺</span><span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>is an entry of </span><span>B</span><span> iff</span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>is also an entry of </span><span>B</span><span>.</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>
<p>其中：</p>
<ul>
<li>entry of matrix 指的是矩阵的元素。</li>
<li><span><span>1≤i1&lt;i2&lt;…&lt;ik≤n&amp;1≤k≤n1\le i_1 &lt; i_2 &lt; \ldots &lt; i_k \le n \enspace\&amp; \enspace 1\le k \le n</span><span><span><span></span><span>1</span><span></span><span>≤</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&lt;</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&lt;</span><span></span></span><span><span></span><span>…</span><span></span><span>&lt;</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≤</span><span></span></span><span><span></span><span>n</span><span></span><span>&amp;</span><span></span><span>1</span><span></span><span>≤</span><span></span></span><span><span></span><span>k</span><span></span><span>≤</span><span></span></span><span><span></span><span>n</span></span></span></span> 。</li>
</ul>
<p>此外，当对称矩阵 <span><span>A∈R2×2A\in \mathbb{R}^{2\times 2}</span><span><span><span></span><span>A</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>2</span><span>×</span><span>2</span></span></span></span></span></span></span></span></span></span></span></span> 时，有特殊的方法证明其是否为 PSD ，只要其满足下面两个条件：</p>
<ul>
<li><span><span>a11≥0,a22≥0a_{11}\ge 0,a_{22}\ge 0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span></li>
<li><span><span>∣A∣≥0|A|\ge 0</span><span><span><span></span><span>∣</span><span>A</span><span>∣</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span></li>
</ul>
<p>其中 <span><span>A=[a11a12a21a22]2×2A=\begin{bmatrix}a_{11} &amp; a_{12} \\ a_{21} &amp; a_{22}\end{bmatrix}_{2\times 2}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span><span>2</span><span>×</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></p>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/stat-04/</id>
      <title type="text">非层次聚类方法</title>
      <published>2026-09-29T00:00:00.000Z</published>
      <updated>2026-09-29T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/stat-04/"/>
      <summary type="text">介绍非层次聚类方法</summary>
      <content type="html"><![CDATA[<section><h2>5. 非层次聚类方法（Non-hierarchical clustering methods）<a href="#5-非层次聚类方法non-hierarchical-clustering-methods"><span>#</span></a></h2><p>非层次聚类方法从以下二者之一出发：</p><ul>
<li>将对象初始划分为若干组，或</li>
<li>一组初始种子点，它们将构成各簇的核心。</li>
</ul><p>一旦指定了要形成的簇数，对象便会被分配到各簇中。</p><div><div><div></div><div>Important</div></div><div><p>六簇解<strong>并不是</strong>由七簇解中的两个簇简单组合而成，而是通过直接寻找最优的六簇解得到的。</p></div></div><section><h3>5.1 K-means 方法<a href="#51-k-means-方法"><span>#</span></a></h3><section><h4>5.1.1 步骤<a href="#511-步骤"><span>#</span></a></h4><ol>
<li>将对象初始划分为 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 个簇。
<ul>
<li>随机分配或预先指定。</li>
</ul>
</li>
<li>计算各簇的质心。</li>
<li>逐一遍历各对象，将每个对象分配到质心（均值）距其最近的簇。</li>
<li>重复第 2、3 步，直到不再发生重新分配为止。</li>
</ol><p>对象到簇的最终分配在一定程度上取决于初始划分或种子点的初始选取。</p></section></section><section><h3>5.2 K-means 示例<a href="#52-k-means-示例"><span>#</span></a></h3><p>对四个对象测量两个变量 <span><span>X1X_1</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>X2X_2</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p>

<table><thead><tr><th>对象</th><th><span><span>x1x_1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>x2x_2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th></tr></thead><tbody><tr><td>A</td><td>5</td><td>3</td></tr><tr><td>B</td><td>-1</td><td>1</td></tr><tr><td>C</td><td>1</td><td>-2</td></tr><tr><td>D</td><td>-3</td><td>-2</td></tr></tbody></table><p>目标是将这些对象划分为</p><span><span><span>K=2K=2</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span></span><p>个簇。</p><section><h4>第 1 步：初始划分<a href="#第-1-步初始划分"><span>#</span></a></h4><p>任意地将对象划分为：</p><ul>
<li>簇 1：<span><span>(AB)(AB)</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span>)</span></span></span></span></li>
<li>簇 2：<span><span>(CD)(CD)</span><span><span><span></span><span>(</span><span>C</span><span>D</span><span>)</span></span></span></span></li>
</ul><p>初始质心为：</p>

<table><thead><tr><th>簇</th><th>成员</th><th><span><span>xˉ1\bar{x}_1</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>xˉ2\bar{x}_2</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th></tr></thead><tbody><tr><td>1</td><td><span><span>(AB)(AB)</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span>)</span></span></span></span></td><td>2</td><td>2</td></tr><tr><td>2</td><td><span><span>(CD)(CD)</span><span><span><span></span><span>(</span><span>C</span><span>D</span><span>)</span></span></span></span></td><td>-1</td><td>-2</td></tr></tbody></table><div><div><div></div><div>质心</div></div><div><p><strong>质心（centroid）就是簇内所有成员在每个变量上的均值</strong>。</p></div></div></section><section><h4>第 2 步：到初始质心（centroids）的距离<a href="#第-2-步到初始质心centroids的距离"><span>#</span></a></h4><p>对于对象 A：</p><span><span><span>d2(A,(AB))=(5−2)2+(3−2)2=10,d^2(A,(AB))
=
(5-2)^2+(3-2)^2
=
10,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>,</span><span></span><span>(</span><span>A</span><span>B</span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>5</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span>,</span></span></span></span></span><span><span><span>d2(A,(CD))=(5+1)2+(3+2)2=61.d^2(A,(CD))
=
(5+1)^2+(3+2)^2
=
61.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>,</span><span></span><span>(</span><span>C</span><span>D</span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>5</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>61.</span></span></span></span></span><p>A 距簇 1 更近，因此不重新分配。</p><p>对于对象 B：</p><span><span><span>d2(B,(AB))=(−1−2)2+(1−2)2=10,d^2(B,(AB))
=
(-1-2)^2+(1-2)^2
=
10,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>(</span><span>B</span><span>,</span><span></span><span>(</span><span>A</span><span>B</span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span>,</span></span></span></span></span><span><span><span>d2(B,(CD))=(−1+1)2+(1+2)2=9.d^2(B,(CD))
=
(-1+1)^2+(1+2)^2
=
9.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>(</span><span>B</span><span>,</span><span></span><span>(</span><span>C</span><span>D</span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>9.</span></span></span></span></span><p>B 距簇 2 更近，因此将 B 重新分配到簇 2。</p><p>所有对象到两个质心的平方距离为：</p>

<table><thead><tr><th>簇质心</th><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td><span><span>(AB)(AB)</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span>)</span></span></span></span></td><td>10</td><td>10</td><td>17</td><td>41</td></tr><tr><td><span><span>(CD)(CD)</span><span><span><span></span><span>(</span><span>C</span><span>D</span><span>)</span></span></span></span></td><td>61</td><td>9</td><td>4</td><td>4</td></tr></tbody></table><p>C 和 D 距簇 2 更近，因此它们留在簇 2 中。</p></section><section><h4>更新后的质心<a href="#更新后的质心"><span>#</span></a></h4><p>簇变为：</p><ul>
<li>簇 1：A</li>
<li>簇 2：<span><span>(BCD)(BCD)</span><span><span><span></span><span>(</span><span>B</span><span>C</span><span>D</span><span>)</span></span></span></span></li>
</ul><p>它们的质心为：</p>

<table><thead><tr><th>簇</th><th>成员</th><th><span><span>xˉ1\bar{x}_1</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>xˉ2\bar{x}_2</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th></tr></thead><tbody><tr><td>1</td><td>A</td><td>5</td><td>3</td></tr><tr><td>2</td><td><span><span>(BCD)(BCD)</span><span><span><span></span><span>(</span><span>B</span><span>C</span><span>D</span><span>)</span></span></span></span></td><td>-1</td><td>-1</td></tr></tbody></table><p>新的平方距离为：</p>

<table><thead><tr><th>簇质心</th><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>A</td><td>0</td><td>40</td><td>41</td><td>89</td></tr><tr><td><span><span>(BCD)(BCD)</span><span><span><span></span><span>(</span><span>B</span><span>C</span><span>D</span><span>)</span></span></span></span></td><td>52</td><td>4</td><td>5</td><td>5</td></tr></tbody></table><p>不再发生进一步的重新分配。</p><div><div><div></div><div>Note</div></div><div><p>最终的 <span><span>K=2K=2</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 个簇为：</p><ul>
<li>簇 1：A</li>
<li>簇 2：<span><span>(BCD)(BCD)</span><span><span><span></span><span>(</span><span>B</span><span>C</span><span>D</span><span>)</span></span></span></span></li>
</ul></div></div><hr /></section></section></section>
<section><h2>6. 簇的数目<a href="#6-簇的数目"><span>#</span></a></h2><p>选择簇的数目需要在以下二者之间权衡：</p><ul>
<li>中层管理者，他们可能偏好数量较少、范围较宽的战略性细分市场（<span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 小），以及</li>
<li>销售人员，他们可能偏好数量较多、界定狭窄的细分市场（<span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 大）。</li>
</ul><p>讲义中讨论的准则包括：</p><ul>
<li><span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span></li>
<li>簇间距离</li>
<li>方差比准则（Variance-ratio criterion）</li>
<li>轮廓系数（Silhouette coefficient）</li>
<li>簇的相对大小</li>
</ul><section><h3>6.1 方差比准则（Variance-ratio criterion）<a href="#61-方差比准则variance-ratio-criterion"><span>#</span></a></h3><p>方差比准则为</p><span><span><span>VRC=BSSESS×N−kk−1.VRC
=
\frac{BSS}{ESS}
\times
\frac{N-k}{k-1}.</span><span><span><span></span><span>V</span><span>R</span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>E</span><span>S</span><span>S</span></span></span><span><span></span><span></span></span><span><span></span><span><span>B</span><span>S</span><span>S</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>N</span><span></span><span>−</span><span></span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><ul>
<li><span><span>NN</span><span><span><span></span><span>N</span></span></span></span> ：对象总数，整个数据集里有多少个观测对象。
<ul>
<li>以上面的例子为例，例子里有 A、B、C、D 四个对象，所以 <span><span>N=4N=4</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>.</li>
</ul>
</li>
<li><span><span>kk</span><span><span><span></span><span>k</span></span></span></span> ：簇的个数，即 K-means 里设定的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>.</li>
<li><span><span>BSSBSS</span><span><span><span></span><span>B</span><span>S</span><span>S</span></span></span></span> ：组间平方和（Between-group Sum of Squares），度量<strong>各簇之间分得有多开</strong>。</li>
<li><span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> ：组内平方和（Error/Within-cluster Sum of Squares），度量<strong>每个簇内部有多紧凑</strong>。</li>
</ul><p>其中，</p><span><span><span>BSS=∑i=1mni∑j=1p(xˉij−xˉj)2,ESS=∑所有对象∑j=1p(xj−xˉ所在簇j)2\begin{aligned}
BSS
&amp;=
\sum_{i=1}^{m}
n_i
\sum_{j=1}^{p}
\left(
\bar{x}_{ij}-\bar{x}_j
\right)^2, \\
ESS &amp;= \sum_{\text{所有对象}}\sum_{j=1}^p(x_j-\bar x_{\text{所在簇} j})^2
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>B</span><span>S</span><span>S</span></span></span><span><span></span><span><span>E</span><span>S</span><span>S</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>m</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>p</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>,</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span><span><span><span><span></span><span><span><span><span>所有对象</span></span></span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span><span>所在簇</span></span><span>j</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>式中：</p><ul>
<li><span><span>xˉij\bar{x}_{ij}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个簇中第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个变量的均值，</li>
<li><span><span>xˉj\bar{x}_j</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个变量的总均值。</li>
<li><span><span>nin_i</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ：第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个簇的成员个数</li>
</ul><section><h4>6.1.1 整个 VRC 公式的含义<a href="#611-整个-vrc-公式的含义"><span>#</span></a></h4><span><span><span>VRC=BSSESS⏟组间/组内 变异比×N−kk−1⏟自由度修正VRC = \underbrace{\frac{BSS}{ESS}}_{\text{组间/组内 变异比}} \times \underbrace{\frac{N-k}{k-1}}_{\text{自由度修正}}</span><span><span><span></span><span>V</span><span>R</span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>组间</span><span>/</span><span>组内</span><span> </span><span>变异比</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>E</span><span>S</span><span>S</span></span></span><span><span></span><span></span></span><span><span></span><span><span>B</span><span>S</span><span>S</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>自由度修正</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>N</span><span></span><span>−</span><span></span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><ul>
<li><strong><span><span>BSSESS\frac{BSS}{ESS}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>E</span><span>S</span><span>S</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>B</span><span>S</span><span>S</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></strong>：就是 ANOVA 里 F 统计量的核心——组间差异相对组内差异越大，说明簇分得越 “干净利落”。</li>
<li><strong><span><span>N−kk−1\frac{N-k}{k-1}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>N</span><span>−</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></strong>：自由度修正。<span><span>k−1k-1</span><span><span><span></span><span>k</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 是 <span><span>BSSBSS</span><span><span><span></span><span>B</span><span>S</span><span>S</span></span></span></span> 的自由度（<span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个质心相对总均值的变异），<span><span>N−kN-k</span><span><span><span></span><span>N</span><span></span><span>−</span><span></span></span><span><span></span><span>k</span></span></span></span> 是 <span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> 的自由度（<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个对象相对各自簇质心的变异）。乘上它才构成真正的 F 型统计量，使不同 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 之间可比。</li>
</ul><div><div><div></div><div>变异</div></div><div><p>所谓的“变异”，指的就是“参差不齐的程度”：一组数围绕某个参照点散得有多开。统计里度量它的标准做法是：</p><span><span><span>变异=∑(每个值−参照点)2\text{变异}=\sum(\text{每个值}-\text{参照点})^2</span><span><span><span></span><span><span>变异</span></span><span></span><span>=</span><span></span></span><span><span></span><span>∑</span><span>(</span><span><span>每个值</span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>参照点</span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span><p>为什么平方？因为单纯算差值的话，正负会抵消（比如 +3 和 -3 加起来是 0，但明明散得很开）。平方之后全部为正，<strong>离参照点越远，惩罚越重</strong>。</p><p>所以，我们上面说“<span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个质心相对总均值的变异”，翻译成人话就是，<strong>各个簇的中心点，离全体数据的中心点有多远（平方后加起来）</strong>。</p></div></div><div><div><div></div><div>自由度</div></div><div><p>自由度的含义是：<strong>在约束条件下，还能”自由变动”的数值个数</strong>。</p><p>我们可以用一个简单的例子来理解。假设我们有 3 个数，均值是 10，也就是说，总和固定为 30.</p><ul>
<li>第一个数你可以随便取，比如 7 → 自由</li>
<li>第二个数也可以随便取，比如 2 → 自由</li>
<li>第三个数呢？<strong>没得选了</strong>，必须是  → 不自由</li>
</ul><p>所以”3 个数 + 均值已知”这个约束下，只有 <strong>2 个数是自由的，自由度 = 3 - 1 = 2</strong>。</p><p>回到 VRC 的两个自由度，<span><span>BSSBSS</span><span><span><span></span><span>B</span><span>S</span><span>S</span></span></span></span> 的自由度是 <span><span>k−1k-1</span><span><span><span></span><span>k</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>，我们可以想一想， <span><span>BSSBSS</span><span><span><span></span><span>B</span><span>S</span><span>S</span></span></span></span> 用到了 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个质心，但这些质心是围绕<strong>总均值</strong>来算偏差的。关键事实是：所有质心（按簇大小加权）的平均恰好等于总均值——这是一个约束。和上面的例子一样，<span><span>kk</span><span><span><span></span><span>k</span></span></span></span>  个质心里只有 <span><span>k−1k-1</span><span><span><span></span><span>k</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>  个能”自由跑”，最后一个被总均值锁死了。</p><p>而对于 <span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span>，它的自由度是 <span><span>N−kN-k</span><span><span><span></span><span>N</span><span></span><span>−</span><span></span></span><span><span></span><span>k</span></span></span></span>：<span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> 用到了全部  个对象，但每个对象是和<strong>自己簇的质心</strong>比较。每个簇内部都有一个约束：簇内成员的偏差之和必须为零（因为质心就是它们的均值）。一共有 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个簇，就有 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个约束，所以自由度 <span><span>=N−k=N-k</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>N</span><span></span><span>−</span><span></span></span><span><span></span><span>k</span></span></span></span>.</p><p>那么，为什么 VRC 要除以自由度？</p><p>这是因为 <span><span>BSSBSS</span><span><span><span></span><span>B</span><span>S</span><span>S</span></span></span></span> 和 <span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> 都是”加总”出来的量，<strong>加总的项越多，数值天然就越大</strong>，直接比不公平。类比一下：</p><ul>
<li>3 个质心产生的 <span><span>BSSBSS</span><span><span><span></span><span>B</span><span>S</span><span>S</span></span></span></span> 天然比 2 个质心的大——不是聚类更好，只是”分母项”多了；</li>
<li>100 个对象产生的 <span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> 天然比 4 个对象的大。</li>
</ul><p>除以自由度相当于算”<strong>平均每一份自由变动贡献了多少变异</strong>”，把它变成一个”单位化”的公平比值。这正是 F 统计量的标准构造：</p><span><span><span>F=BSS/(k−1)ESS/(N−k)=VRCF=\frac{BSS/(k-1)}{ESS/(N-k)}=VRC</span><span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>E</span><span>S</span><span>S</span><span>/</span><span>(</span><span>N</span><span></span><span>−</span><span></span><span>k</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>B</span><span>S</span><span>S</span><span>/</span><span>(</span><span>k</span><span></span><span>−</span><span></span><span>1</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span>R</span><span>C</span></span></span></span></span><p>可以看到，VRC 只是把分子分母调换写法，本质一模一样。也正因为做了这个校正，我们才能拿<span><span>k=2k=2</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 的 VRC 和 <span><span>k=5k=5</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 的 VRC <strong>直接比较大小</strong>——这正是 6.4 节那张表里”比较不同簇数的 VRC”做法合法的原因。</p></div></div><blockquote><p>💡 记忆：<strong>BSS 看簇与簇之间（越大越好），ESS 看簇内部（越小越好），VRC 是两者之比再做自由度校正</strong>。</p></blockquote></section></section><section><h3>6.2 轮廓系数（Silhouette coefficient）<a href="#62-轮廓系数silhouette-coefficient"><span>#</span></a></h3><p>对于对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>，</p><span><span><span>s(i)={bi−aimax⁡(ai,bi),ni&gt;1,0,ni=1.s(i)
=
\begin{cases}
\dfrac{b_i-a_i}{\max(a_i,b_i)}, &amp; n_i&gt;1,\\[6pt]
0, &amp; n_i=1.
\end{cases}</span><span><span><span></span><span>s</span><span>(</span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>⎩</span></span></span><span><span></span><span></span></span><span><span></span><span><span>⎨</span></span></span><span><span></span><span></span></span><span><span></span><span><span>⎧</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>max</span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span></span></span><span><span></span><span><span>0</span><span>,</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>n</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span><span>1</span><span>,</span></span></span><span><span></span><span><span><span>n</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span><span>1.</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span></span><p>式中：</p><ul>
<li><span><span>aia_i</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> = 对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 到同一簇中所有其他对象的平均距离，“我和我的队友有多近”；</li>
<li><span><span>bib_i</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> = 对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 到最近邻簇中所有对象的平均距离，“我和隔壁有多近”。</li>
</ul><p>我们可以设想两种情形：</p><section><h4>情形 1：<span><span>ni&gt;1n_i &gt; 1</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>1</span></span></span></span>（簇里不止自己一个人）<a href="#情形-1ni1n_i--1ni1簇里不止自己一个人"><span>#</span></a></h4><span><span><span>s(i)=bi−aimax⁡(ai,bi)s(i) = \frac{b_i - a_i}{\max(a_i, b_i)}</span><span><span><span></span><span>s</span><span>(</span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>max</span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>分子 <span><span>bi−aib_i - a_i</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是”离隔壁的距离”减去”离队友的距离”：</p><ul>
<li><strong><span><span>bi≫aib_i \gg a_i</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≫</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></strong>：队友很近、隔壁很远 → 分对了！分子为正且大</li>
<li><strong><span><span>bi≈aib_i \approx a_i</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></strong>：两边差不多远 → 我站在边界上，分到哪边都说得过去</li>
<li><strong><span><span>bi&lt;aib_i &lt; a_i</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&lt;</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></strong>：隔壁反而比队友更近 → <strong>我可能被分错簇了</strong>！分子为负</li>
</ul><p><strong>分母为什么除以 <span><span>max⁡(ai,bi)\max(a_i, b_i)</span><span><span><span></span><span>max</span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>？</strong> 这是为了把结果<strong>归一化</strong>到 <span><span>[−1,1][-1, 1]</span><span><span><span></span><span>[</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>]</span></span></span></span> 区间。可以验证两种极端：</p><ul>
<li>完美聚类：<span><span>ai→0a_i \to 0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span>（紧贴队友），此时 <span><span>s(i)=bi−0bi=1s(i) = \dfrac{b_i - 0}{b_i} = 1</span><span><span><span></span><span>s</span><span>(</span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></li>
<li>完全分错：<span><span>bi→0b_i \to 0</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span>，此时 <span><span>s(i)=0−aiai=−1s(i) = \dfrac{0 - a_i}{a_i} = -1</span><span><span><span></span><span>s</span><span>(</span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>0</span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span></li>
</ul><p>这样不同对象、不同数据集的分数就可以直接比较了。</p></section><section><h4>情形 2：<span><span>ni=1n_i = 1</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（簇里只有自己）<a href="#情形-2ni1n_i--1ni1簇里只有自己"><span>#</span></a></h4><p>如果簇里只有一个对象，<span><span>aia_i</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 根本没法算（没有”队友”），所以<strong>约定 <span><span>s(i)=0s(i) = 0</span><span><span><span></span><span>s</span><span>(</span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></strong>——表示”无法评价”，既不表扬也不批评。</p><hr /><p>平均轮廓分数满足</p><span><span><span>−1≤sˉ≤1.-1
\leq
\bar{s}
\leq
1.</span><span><span><span></span><span>−</span><span>1</span><span></span><span>≤</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>s</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span></span><span>≤</span><span></span></span><span><span></span><span>1.</span></span></span></span></span><p>解释：</p><ul>
<li>取值接近 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>：被错误分类，</li>
<li>取值接近 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>：聚类良好。</li>
</ul><p>也就是说，轮廓系数回答的问题就是：对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 待在现在这个簇里，合不合适？</p></section></section><section><h3>6.3 示例：VRC 与轮廓系数<a href="#63-示例vrc-与轮廓系数"><span>#</span></a></h3><p>使用上一示例中最终的 <span><span>K=2K=2</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 解：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916112650313.png" alt="" /></p><p>上图的 <span><span>1,2,2,21,2,2,2</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span></span></span></span> 表示 A 属于簇 1，BCD 属于簇 2.</p><p>总均值为</p><span><span><span>xˉ1=0.5,xˉ2=0.\bar{x}_1=0.5,
\qquad
\bar{x}_2=0.</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span><span>,</span><span></span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.</span></span></span></span></span><p>则</p><span><span><span>BSS=(5−0.5)2+(3−0)2+3(−1−0.5)2+3(−1)2=39,BSS
=
(5-0.5)^2
+
(3-0)^2
+
3(-1-0.5)^2
+
3(-1)^2
=
39,</span><span><span><span></span><span>B</span><span>S</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>5</span><span></span><span>−</span><span></span></span><span><span></span><span>0.5</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>0</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>(</span><span>−</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>0.5</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>(</span><span>−</span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>39</span><span>,</span></span></span></span></span><p>上述四项的结构是：</p><span><span><span>(5−0.5)2⏟簇1, X1+(3−0)2⏟簇1, X2+3(−1−0.5)2⏟簇2, X1, 乘 n2+3(−1)2⏟簇2, X2, 乘 n2\underbrace{(5-0.5)^2}_{\text{簇1, }X_1} + \underbrace{(3-0)^2}_{\text{簇1, }X_2} + \underbrace{3(-1-0.5)^2}_{\text{簇2, }X_1\text{, 乘 }n_2} + \underbrace{3(-1)^2}_{\text{簇2, }X_2\text{, 乘 }n_2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>簇</span><span>1, </span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>(</span><span>5</span><span></span><span>−</span><span></span><span>0.5</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>簇</span><span>1, </span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>(</span><span>3</span><span></span><span>−</span><span></span><span>0</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>簇</span><span>2, </span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>, </span><span>乘</span><span> </span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>3</span><span>(</span><span>−</span><span>1</span><span></span><span>−</span><span></span><span>0.5</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>簇</span><span>2, </span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>, </span><span>乘</span><span> </span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>3</span><span>(</span><span>−</span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>且</p><span><span><span>ESS=(5−5)2+(3−3)2+(−1+1)2+(1+1)2+(1+1)2+(−2+1)2+(−3+1)2+(−2+1)2=14.ESS
=
(5-5)^2
+
(3-3)^2
+
(-1+1)^2
+
(1+1)^2
+
(1+1)^2
+
(-2+1)^2
+
(-3+1)^2
+
(-2+1)^2
=
14.</span><span><span><span></span><span>E</span><span>S</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>5</span><span></span><span>−</span><span></span></span><span><span></span><span>5</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>14.</span></span></span></span></span><p>因此，</p><span><span><span>VRC=3914×4−22−1=5.5714.VRC
=
\frac{39}{14}
\times
\frac{4-2}{2-1}
=
5.5714.</span><span><span><span></span><span>V</span><span>R</span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>14</span></span></span><span><span></span><span></span></span><span><span></span><span><span>39</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>4</span><span></span><span>−</span><span></span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5.5714.</span></span></span></span></span><p>四个对象之间的欧氏距离为：</p>

<table><thead><tr><th></th><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>A</td><td>0</td><td>6.3246</td><td>6.4031</td><td>9.4340</td></tr><tr><td>B</td><td>6.3246</td><td>0</td><td>3.6056</td><td>3.6056</td></tr><tr><td>C</td><td>6.4031</td><td>3.6056</td><td>0</td><td>4.0000</td></tr><tr><td>D</td><td>9.4340</td><td>3.6056</td><td>4.0000</td><td>0</td></tr></tbody></table><p>轮廓系数的计算如下：</p>

<table><thead><tr><th><span><span>ii</span><span><span><span></span><span>i</span></span></span></span></th><th><span><span>aia_i</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>bib_i</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>max⁡(ai,bi)\max(a_i,b_i)</span><span><span><span></span><span>max</span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></th><th><span><span>s(i)s(i)</span><span><span><span></span><span>s</span><span>(</span><span>i</span><span>)</span></span></span></span></th></tr></thead><tbody><tr><td>A</td><td>0</td><td>7.3872</td><td>7.3872</td><td>0</td></tr><tr><td>B</td><td>3.6056</td><td>6.3246</td><td>6.3246</td><td>0.4299</td></tr><tr><td>C</td><td>3.8028</td><td>6.4031</td><td>6.4031</td><td>0.4061</td></tr><tr><td>D</td><td>3.8028</td><td>9.4340</td><td>9.4340</td><td>0.5969</td></tr></tbody></table><p>从而，</p><span><span><span>sˉ=0+0.4299+0.4061+0.59694=0.3582.\bar{s}
=
\frac{
0+0.4299+0.4061+0.5969
}{4}
=
0.3582.</span><span><span><span></span><span><span><span><span><span><span></span><span>s</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span></span></span><span><span></span><span><span>0</span><span></span><span>+</span><span></span><span>0.4299</span><span></span><span>+</span><span></span><span>0.4061</span><span></span><span>+</span><span></span><span>0.5969</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3582.</span></span></span></span></span><hr /></section></section>
<section><h2>6.4 示例 1：顾客支出细分<a href="#64-示例-1顾客支出细分"><span>#</span></a></h2><p>一项针对 24 名顾客的调查记录了其在九个类别上的支出百分比：</p>

<table><thead><tr><th>变量</th><th>类别</th></tr></thead><tbody><tr><td><span><span>X1X_1</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>食品</td></tr><tr><td><span><span>X2X_2</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>交通</td></tr><tr><td><span><span>X3X_3</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>生活</td></tr><tr><td><span><span>X4X_4</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>通讯</td></tr><tr><td><span><span>X5X_5</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>娱乐</td></tr><tr><td><span><span>X6X_6</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>其他</td></tr><tr><td><span><span>X7X_7</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>衣着</td></tr><tr><td><span><span>X8X_8</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>教育</td></tr><tr><td><span><span>X9X_9</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>储蓄</td></tr></tbody></table><p>目标是通过聚类分析进行细分。</p><section><h3>6.4.1 方法<a href="#641-方法"><span>#</span></a></h3><p>共有九个连续属性。</p><ul>
<li>采用欧氏距离平方作为相异性度量。</li>
<li>对观测值进行标准化。</li>
<li>所考虑的层次凝聚方法：
<ul>
<li>单连接法，</li>
<li>Ward 法。</li>
</ul>
</li>
</ul></section><section><h3>6.4.2 标准化示例<a href="#642-标准化示例"><span>#</span></a></h3><p>原始数据为</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916152938474.png" alt="" /></p><p>对于观测值 1，</p><span><span><span>zfood=3.3−15.9110.73=−1.176,z_{\text{food}}
=
\frac{3.3-15.91}{10.73}
=
-1.176,</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span><span><span>food</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>10.73</span></span></span><span><span></span><span></span></span><span><span></span><span><span>3.3</span><span></span><span>−</span><span></span><span>15.91</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1.176</span><span>,</span></span></span></span></span><p>且</p><span><span><span>ztransport=0.9−1.271.00=−0.365.z_{\text{transport}}
=
\frac{0.9-1.27}{1.00}
=
-0.365.</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span><span><span>transport</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1.00</span></span></span><span><span></span><span></span></span><span><span></span><span><span>0.9</span><span></span><span>−</span><span></span><span>1.27</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.365.</span></span></span></span></span><div><div><div></div><div>标准化公式</div></div><div><span><span><span>z=原始值−该变量的均值该变量的标准差z=\frac{\text{原始值}-\text{该变量的均值}}{\text{该变量的标准差}}</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>该变量的标准差</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>原始值</span></span><span></span><span>−</span><span></span><span><span>该变量的均值</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>对于 z-score 的含义，以上面的 -1.176 为例，它读作“<strong>这位顾客的食品支出比平均水平低 1.176 个标准差</strong>。”</p></div></div><p>对于观测值 1 和 2，标准化空间中的欧氏距离平方为</p><span><span><span>(−1.176+0.626)2+(−0.365+1.163)2+⋯+(0.362−0.273)2=4.74.(-1.176+0.626)^2
+
(-0.365+1.163)^2
+
\cdots
+
(0.362-0.273)^2
=
4.74.</span><span><span><span></span><span>(</span><span>−</span><span>1.176</span><span></span><span>+</span><span></span></span><span><span></span><span>0.626</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.365</span><span></span><span>+</span><span></span></span><span><span></span><span>1.163</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>0.362</span><span></span><span>−</span><span></span></span><span><span></span><span>0.273</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.74.</span></span></span></span></span><p>可以看出：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916153503900.png" alt="" /></p><ul>
<li>观测值 2 与 16 属于距离最近的一对，</li>
<li>观测值 18 与 22 也属于距离最近的一对。</li>
</ul></section><section><h3>6.4.3 单连接法<a href="#643-单连接法"><span>#</span></a></h3><p>凝聚过程开始：</p><ul>
<li>第 1 阶段：观测值 2 与 16 形成一个簇，距离 <span><span>=2.2562=2.2562</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>2.2562</span></span></span></span>。</li>
<li>第 2 阶段：观测值 18 与 22 形成一个簇，距离 <span><span>=3.3621=3.3621</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>3.3621</span></span></span></span>。</li>
<li>第 3 阶段：观测值 1 与 3 形成一个簇，距离 <span><span>=3.4443=3.4443</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>3.4443</span></span></span></span>。</li>
<li>第 4 阶段：簇 <span><span>(1,3)(1,3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 与簇 <span><span>(2,16)(2,16)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>16</span><span>)</span></span></span></span> 合并，距离 <span><span>=3.6693=3.6693</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>3.6693</span></span></span></span>。</li>
<li>……</li>
</ul><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916155933022.png" alt="" /></p><p>簇数准则为：</p>

<table><thead><tr><th>簇的数目</th><th>2</th><th>3</th><th>4</th><th>5</th></tr></thead><tbody><tr><td>VRC</td><td>2.1048</td><td>1.8285</td><td>4.3751</td><td>3.6540</td></tr><tr><td>轮廓系数</td><td>0.1983</td><td>0.0694</td><td>0.1631</td><td>0.0595</td></tr></tbody></table><p>讲义中的观察：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916160030073.png" alt="" /></p><ul>
<li>VRC 最大：<strong>4 簇解</strong>。</li>
<li>轮廓系数最大：<strong>2 簇解</strong>。</li>
<li>距离图显示从 4 簇到 3 簇、或从 2 簇到 1 簇处出现跳跃。</li>
<li>讲义建议取 <strong>4 个簇</strong>。</li>
</ul><hr /><p>这里简单介绍一下怎么读上面的图。</p><p>首先是 height 的含义：<strong>height = 该阶段合并两个簇时的距离（相异度）</strong>，其实就是树状图的纵轴。凝聚聚类每一步都把”距离最近的两个簇”合并成一个，height 就记录这次合并花了多大的”代价”：</p><ul>
<li>height 小 → 合并的两个簇本来就很像，合并顺理成章</li>
<li>height 大 → 被硬凑在一起的两个簇其实差异很大，合并很勉强</li>
</ul><p>其次就是图的读法。读法是<strong>从右往左看</strong>：阶段越靠后，剩下的簇越少。</p><hr /><p>假定分为四个簇：</p><ul>
<li><span><span>C1=(1,2,3,4,5,6,7,8,9,10,11,12,13,14,16,17)C_1=(1,2,3,4,5,6,7,8,9,10,11,12,13,14,16,17)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>6</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>9</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>11</span><span>,</span><span></span><span>12</span><span>,</span><span></span><span>13</span><span>,</span><span></span><span>14</span><span>,</span><span></span><span>16</span><span>,</span><span></span><span>17</span><span>)</span></span></span></span></li>
<li><span><span>C2=(15)C_2=(15)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>15</span><span>)</span></span></span></span></li>
<li><span><span>C3=(18,19,20,21,22,23)C_3=(18,19,20,21,22,23)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>18</span><span>,</span><span></span><span>19</span><span>,</span><span></span><span>20</span><span>,</span><span></span><span>21</span><span>,</span><span></span><span>22</span><span>,</span><span></span><span>23</span><span>)</span></span></span></span></li>
<li><span><span>C4=(24)C_4=(24)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span></li>
</ul></section><section><h3>6.4.4 Ward 法<a href="#644-ward-法"><span>#</span></a></h3><p>凝聚过程包括：</p><ul>
<li>第 1 阶段：观测值 2 与 16 形成一个簇，</li>
</ul><span><span><span>d(2,16)=2.26.d(2,16)=2.26.</span><span><span><span></span><span>d</span><span>(</span><span>2</span><span>,</span><span></span><span>16</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>2.26.</span></span></span></span></span><ul>
<li>第 2 阶段：观测值 18 与 22 形成一个簇，</li>
</ul><span><span><span>d(18,22)=3.36.d(18,22)=3.36.</span><span><span><span></span><span>d</span><span>(</span><span>18</span><span>,</span><span></span><span>22</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>3.36.</span></span></span></span></span><ul>
<li>……</li>
<li>第 5 阶段：簇 <span><span>(1,3)(1,3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 与观测值 8 合并成一个簇。</li>
</ul><p>讲义中展示的 Ward 更新公式为</p><span><span><span>d(13),8=(N8+N1)d18+(N8+N3)d38−N8d13N1+N3+N8.d_{(13),8}
=
\frac{
(N_8+N_1)d_{18}
+
(N_8+N_3)d_{38}
-
N_8d_{13}
}{
N_1+N_3+N_8
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>13</span><span>)</span><span>,</span><span>8</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>18</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>38</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>代入所列数值，</p><span><span><span>d(13),8=(1+1)(4.62)+(1+1)(4.52)−1(3.44)1+1+1=4.95.d_{(13),8}
=
\frac{
(1+1)(4.62)
+
(1+1)(4.52)
-
1(3.44)
}{
1+1+1
}
=
4.95.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>13</span><span>)</span><span>,</span><span>8</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>1</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>(</span><span>4.62</span><span>)</span><span></span><span>+</span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>(</span><span>4.52</span><span>)</span><span></span><span>−</span><span></span><span>1</span><span>(</span><span>3.44</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.95.</span></span></span></span></span><p>簇数准则为：</p>

<table><thead><tr><th>簇的数目</th><th>2</th><th>3</th><th>4</th><th>5</th></tr></thead><tbody><tr><td>VRC</td><td>7.8698</td><td>6.9538</td><td>7.2853</td><td>6.7826</td></tr><tr><td>轮廓系数</td><td>0.2552</td><td>0.2243</td><td>0.2168</td><td>0.2133</td></tr></tbody></table><p>讲义指出：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916161536761.png" alt="" /></p><ul>
<li>VRC 最大：<strong>2 簇解</strong>。</li>
<li>轮廓系数最大：<strong>2 簇解</strong>。</li>
<li>距离图显示从 4 簇到 3 簇出现相对较大的增幅。</li>
</ul><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916161631711.png" alt="" /></p><p>为了与单连接法解对比，讲义还给出了一种四簇的 Ward 解：</p><ul>
<li><span><span>C1=(1,2,3,4,8,9,11,13,16)C_1=(1,2,3,4,8,9,11,13,16)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>9</span><span>,</span><span></span><span>11</span><span>,</span><span></span><span>13</span><span>,</span><span></span><span>16</span><span>)</span></span></span></span></li>
<li><span><span>C2=(5,12,14,18,22,23,24)C_2=(5,12,14,18,22,23,24)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>5</span><span>,</span><span></span><span>12</span><span>,</span><span></span><span>14</span><span>,</span><span></span><span>18</span><span>,</span><span></span><span>22</span><span>,</span><span></span><span>23</span><span>,</span><span></span><span>24</span><span>)</span></span></span></span></li>
<li><span><span>C3=(6,7,10,15,17)C_3=(6,7,10,15,17)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>6</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>15</span><span>,</span><span></span><span>17</span><span>)</span></span></span></span></li>
<li><span><span>C4=(19,20,21)C_4=(19,20,21)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>19</span><span>,</span><span></span><span>20</span><span>,</span><span></span><span>21</span><span>)</span></span></span></span></li>
</ul><div><div><div></div><div>Important</div></div><div><p>单连接法与 Ward 法的解并不相同。讲义指出，<strong>单连接法倾向于形成较大的簇</strong>。</p></div></div></section><section><h3>6.4.5 四个 Ward 簇的解释<a href="#645-四个-ward-簇的解释"><span>#</span></a></h3><p>讲义将标准化均值轮廓总结如下：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916161736119.png" alt="" /></p><ul>
<li><strong>C1：</strong> 在”其他、衣着、教育”方面支出占比较大；在”食品、交通”方面支出占比较小。</li>
<li><strong>C2：</strong> 在”食品”方面支出占比较大；在”生活、通讯”方面支出占比较小。</li>
<li><strong>C3：</strong> 在”娱乐”方面支出占比较大；在”教育、储蓄”方面支出占比较小。</li>
<li><strong>C4：</strong> 在”交通、生活、通讯、储蓄”方面支出占比较大；在”其他、衣着”方面支出占比较小。</li>
</ul><hr /></section></section>
<section><h2>6.5 示例 2：顾客面部特征<a href="#65-示例-2顾客面部特征"><span>#</span></a></h2><p>一个下午内共有 12 名顾客进入一家商店。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916161825125.png" alt="" /></p><p>目标是基于五个二元面部特征对顾客进行细分：</p><ul>
<li>性别</li>
<li>眼镜</li>
<li>胡须</li>
<li>微笑</li>
<li>帽子</li>
</ul><section><h3>6.5.1 方法<a href="#651-方法"><span>#</span></a></h3><p>共有五个二元属性。</p><p>采用简单失配法（Simple mismatching）：</p><ul>
<li>对”是”与”否”两种取值均统计失配数，</li>
<li>并将该计数作为相异性度量。</li>
</ul><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916161951134.png" alt="" /></p><p>采用<strong>完全连接法</strong>的层次凝聚方法。</p><p>邻近性度量为五个属性中失配特征的个数。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916162033881.png" alt="" /></p><p>例如：</p><ul>
<li>观测值 1 与 2 有两处失配，故距离 <span><span>=2=2</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，</li>
<li>观测值 1 与 3 有一处失配，故距离 <span><span>=1=1</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。</li>
</ul><p>对于完全连接法，簇间距离由分属两簇、彼此相距最远的两个观测值确定。</p></section><section><h3>6.5.2 凝聚过程<a href="#652-凝聚过程"><span>#</span></a></h3><p>讲义重点提到：</p><ul>
<li>第 1 阶段：观测值 1 与 7 形成一个簇；失配数 <span><span>=0=0</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
<li>之后：观测值 3 并入簇 <span><span>(1,7)(1,7)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span>；失配距离 <span><span>=1=1</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。</li>
</ul></section><section><h3>6.5.3 簇数目的选择<a href="#653-簇数目的选择"><span>#</span></a></h3>

<table><thead><tr><th>簇的数目</th><th>3</th><th>4</th><th>5</th><th>6</th></tr></thead><tbody><tr><td>VRC</td><td>13.03</td><td>16.75</td><td>17.79</td><td>15.55</td></tr></tbody></table><p>VRC 在五簇时最大，但距离图显示从三簇到两簇出现大幅跳跃。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916162232992.png" alt="" /></p><p>因此讲义建议采用<strong>三簇解</strong>：</p><ul>
<li><span><span>C1=(1,3,7,9)C_1=(1,3,7,9)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>9</span><span>)</span></span></span></span></li>
<li><span><span>C2=(2,4,5,8,10,12)C_2=(2,4,5,8,10,12)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>12</span><span>)</span></span></span></span></li>
<li><span><span>C3=(6,11)C_3=(6,11)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>6</span><span>,</span><span></span><span>11</span><span>)</span></span></span></span></li>
</ul></section><section><h3>6.5.4 簇轮廓<a href="#654-簇轮廓"><span>#</span></a></h3>

<table><thead><tr><th>簇</th><th>% 男性</th><th>% 眼镜</th><th>% 胡须</th><th>% 微笑</th><th>% 帽子</th></tr></thead><tbody><tr><td>C1</td><td>100</td><td>100</td><td>25</td><td>75</td><td>0</td></tr><tr><td>C2</td><td>50</td><td>0</td><td>0</td><td>50</td><td>0</td></tr><tr><td>C3</td><td>100</td><td>0</td><td>100</td><td>0</td><td>50</td></tr></tbody></table><p>讲义给出的解释：</p><ul>
<li><strong>C1：</strong> 男性、戴眼镜、不戴帽子。</li>
<li><strong>C2：</strong> 不戴眼镜、无胡须、不戴帽子。</li>
<li><strong>C3：</strong> 男性、不戴眼镜、有胡须、不微笑。</li>
</ul><hr /></section></section>
<section><h2>6.6 示例 3：美国公用事业公司<a href="#66-示例-3美国公用事业公司"><span>#</span></a></h2><p>收集了 22 家美国公用事业公司 1975 年的数据。</p><p>变量如下：</p>

<table><thead><tr><th>变量</th><th>说明</th></tr></thead><tbody><tr><td><span><span>X1X_1</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>固定费用保障比率（收益/债务）</td></tr><tr><td><span><span>X2X_2</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>资本回报率</td></tr><tr><td><span><span>X3X_3</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>单位装机容量成本（每 KW）</td></tr><tr><td><span><span>X4X_4</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>年负荷率</td></tr><tr><td><span><span>X5X_5</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>1974 至 1975 年高峰 KWH 需求增长</td></tr><tr><td><span><span>X6X_6</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>销售量（每年 KWH 用电量）</td></tr><tr><td><span><span>X7X_7</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>核电百分比</td></tr><tr><td><span><span>X8X_8</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>燃料总成本（美分/KWH）</td></tr></tbody></table><p>目标是通过聚类分析对这些公司进行定位。</p><section><h3>6.6.1 方法<a href="#661-方法"><span>#</span></a></h3><p>共有八个连续属性。</p><ul>
<li>采用欧氏距离平方作为相异性度量。</li>
<li>对观测值进行标准化。</li>
<li>采用 K-means 方法。</li>
</ul></section><section><h3>6.6.2 簇的数目<a href="#662-簇的数目"><span>#</span></a></h3>

<table><thead><tr><th><span><span>kk</span><span><span><span></span><span>k</span></span></span></span></th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th></tr></thead><tbody><tr><td>VRC</td><td>5.6093</td><td>6.0913</td><td>4.5813</td><td>5.5113</td><td>4.9967</td></tr><tr><td>轮廓系数</td><td>0.1853</td><td>0.2200</td><td>0.1169</td><td>0.1947</td><td>0.1343</td></tr></tbody></table><p>讲义接下来在 <strong>5 个簇</strong>的假定下继续分析：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916171923253.png" alt="" /></p></section><section><h3>6.6.3 簇轮廓<a href="#663-簇轮廓"><span>#</span></a></h3><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260916172002422.png" alt="" /></p><ul>
<li>
<p><strong>簇 1</strong></p>
<ul>
<li>7 家公司。</li>
<li>在资本回报率 <span><span>(X2)(X_2)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最高。</li>
<li>在成本 <span><span>(X3)(X_3)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最低。</li>
</ul>
</li>
<li>
<p><strong>簇 2</strong></p>
<ul>
<li>6 家公司。</li>
<li>在核电 <span><span>(X7)(X_7)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最高。</li>
</ul>
</li>
<li>
<p><strong>簇 3</strong></p>
<ul>
<li>1 家公司。</li>
<li>在固定费用保障比率 <span><span>(X1)(X_1)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 与燃料成本 <span><span>(X8)(X_8)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最高。</li>
<li>在资本回报率 <span><span>(X2)(X_2)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>、年负荷率 <span><span>(X4)(X_4)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>、高峰 KWH 需求增长 <span><span>(X5)(X_5)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 与销售量 <span><span>(X6)(X_6)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最低。</li>
</ul>
</li>
<li>
<p><strong>簇 4</strong></p>
<ul>
<li>5 家公司。</li>
<li>在年负荷率 <span><span>(X4)(X_4)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 与燃料成本 <span><span>(X8)(X_8)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最高。</li>
<li>在固定费用保障比率 <span><span>(X1)(X_1)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>、资本回报率 <span><span>(X2)(X_2)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 与核电 <span><span>(X7)(X_7)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最低。</li>
</ul>
</li>
<li>
<p><strong>簇 5</strong></p>
<ul>
<li>3 家公司。</li>
<li>在成本 <span><span>(X3)(X_3)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>、高峰 KWH 需求增长 <span><span>(X5)(X_5)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 与销售量 <span><span>(X6)(X_6)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最高。</li>
<li>在固定费用保障比率 <span><span>(X1)(X_1)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>、资本回报率 <span><span>(X2)(X_2)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>、核电 <span><span>(X7)(X_7)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 与燃料成本 <span><span>(X8)(X_8)</span><span><span><span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 上最低。</li>
</ul>
</li>
</ul></section><section><h3>6.6.4 与 Ward 法的比较<a href="#664-与-ward-法的比较"><span>#</span></a></h3><p>讲义指出，对于这一五簇解，K-means 与 Ward 法的划分结果一致。</p><p>采用讲义中给出的 Ward 簇标签：</p><section><h4>C1：南方公司群体<a href="#c1南方公司群体"><span>#</span></a></h4><ul>
<li>Oklahoma Gas and Electric Company</li>
<li>Texas Utilities Company</li>
<li>Arizona Public Service</li>
<li>The Southern Company</li>
<li>Central Louisiana Electric Co.</li>
<li>Kentucky Utilities Co.</li>
<li>Florida Power &amp; Light Co.</li>
</ul></section><section><h4>C2：东部+中部公司群体<a href="#c2东部中部公司群体"><span>#</span></a></h4><ul>
<li>Madison Gas &amp; Electric Co.</li>
<li>Northern States Power Co.</li>
<li>Commonwealth Edison Co.</li>
<li>Wisconsin Electric Power Co.</li>
<li>Boston Edison Co.</li>
<li>Virginia Electric &amp; Power Co.</li>
</ul></section><section><h4>C3：独立公司<a href="#c3独立公司"><span>#</span></a></h4><ul>
<li>Consolidated Edison Co. (New York)</li>
</ul></section><section><h4>C4：东西部近海岸公司<a href="#c4东西部近海岸公司"><span>#</span></a></h4><ul>
<li>New England Electric Co.</li>
<li>United Illuminating Co.</li>
<li>Pacific Gas &amp; Electric Co.</li>
<li>Hawaiian Electric Co.</li>
<li>San Diego Gas &amp; Electric Co.</li>
</ul></section><section><h4>C5：西北部公司群体<a href="#c5西北部公司群体"><span>#</span></a></h4><ul>
<li>Idaho Power Co.</li>
<li>Puget Sound Power &amp; Light Co.</li>
<li>Nevada Power Co.</li>
</ul></section></section><section><h3>6.6.5 地理解释<a href="#665-地理解释"><span>#</span></a></h3><p>讲义的结论是：公用事业公司往往按地理位置聚成群体或展开竞争。</p><ul>
<li>C4：东部与西部，但靠近海岸。</li>
<li>C2：东部+中部。</li>
<li>C1：南部。</li>
<li>C5：西北部。</li>
<li>Consolidated Edison 独自成簇。</li>
</ul><p>讲义给出的可能原因：</p><ul>
<li>位置相近的公司往往在电厂中使用相似的燃料，并面对共同的市场，</li>
<li>因此这些公司在发电类型、成本、增长率及相关变量上应相对同质。</li>
</ul><hr /></section></section>
<section><h2>7. 与聚类的关联分析<a href="#7-与聚类的关联分析"><span>#</span></a></h2><section><h3>7.1 单因素方差分析模型（1-way ANOVA model）<a href="#71-单因素方差分析模型1-way-anova-model"><span>#</span></a></h3><p>单因素方差分析研究以下两者之间的关系：</p><ul>
<li>一个定量因变量（quantitative dependent variable），以及</li>
<li>一个定性自变量（qualitative independent variable）。</li>
</ul><p>设：</p><ul>
<li><span><span>rr</span><span><span><span></span><span>r</span></span></span></span> = 因子的水平数（Number of levels of factor），</li>
<li><span><span>nTn_T</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> = 案例总数（Total number of cases）。</li>
</ul><p>假设为</p><span><span><span>H0:μ1=μ2=⋯=μr,H_0:
\mu_1
=
\mu_2
=
\cdots
=
\mu_r,</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>:</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>⋯</span><span></span><span>=</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span></span></span></span></span><span><span><span>H1:not all μi are equal.H_1:
\text{not all }\mu_i\text{ are equal}.</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>:</span><span></span></span><span><span></span><span><span>not all </span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span> are equal</span></span><span>.</span></span></span></span></span><p>当下式成立时拒绝 <span><span>H0H_0</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>F&gt;Fα,r−1,nT−rF
&gt;
F_{\alpha,r-1,n_T-r}</span><span><span><span></span><span>F</span><span></span><span>&gt;</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>α</span><span>,</span><span>r</span><span>−</span><span>1</span><span>,</span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>−</span><span>r</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>显著性水平为 <span><span>α\alpha</span><span><span><span></span><span>α</span></span></span></span>。</p></section><section><h3>7.2 <span><span>χ2\chi^2</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 检验<a href="#72-χ2chi2χ2-检验"><span>#</span></a></h3><p><span><span>χ2\chi^2</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 检验研究两个定性变量之间的关系。</p><p>设：</p><ul>
<li><span><span>rr</span><span><span><span></span><span>r</span></span></span></span> = 变量 A 的水平数，</li>
<li><span><span>cc</span><span><span><span></span><span>c</span></span></span></span> = 变量 B 的水平数。</li>
</ul><p>假设为</p><span><span><span>H0:There is no association between the two variables,H_0:
\text{There is no association between the two variables},</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>:</span><span></span></span><span><span></span><span><span>There is no association between the two variables</span></span><span>,</span></span></span></span></span><span><span><span>H1:There is an association between the two variables.H_1:
\text{There is an association between the two variables}.</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>:</span><span></span></span><span><span></span><span><span>There is an association between the two variables</span></span><span>.</span></span></span></span></span><p>当下式成立时拒绝 <span><span>H0H_0</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>χ2&gt;χα,(r−1)(c−1)2\chi^2
&gt;
\chi^2_{\alpha,(r-1)(c-1)}</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>α</span><span>,</span><span>(</span><span>r</span><span>−</span><span>1</span><span>)</span><span>(</span><span>c</span><span>−</span><span>1</span><span>)</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>显著性水平为 <span><span>α\alpha</span><span><span><span></span><span>α</span></span></span></span>。</p></section><section><h3>7.3 示例：与顾客簇的关联分析<a href="#73-示例与顾客簇的关联分析"><span>#</span></a></h3><p>讲义提出以下问题：</p><ol>
<li>各聚类变量在不同簇之间是否存在差异？</li>
<li>聚类结果与顾客类型之间是否存在关联？其中：
<ul>
<li>shop <span><span>=1=1</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>：百货商店，</li>
<li>shop <span><span>=2=2</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>：超市，</li>
<li>shop <span><span>=3=3</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>：本地杂货店？</li>
</ul>
</li>
</ol><section><h4>7.3.1 单因素方差分析<a href="#731-单因素方差分析"><span>#</span></a></h4><p>对标准化后的聚类变量进行单因素方差分析。</p><p>临界值为</p><span><span><span>F0.05,3,20=3.098.F_{0.05,3,20}
=
3.098.</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>0.05</span><span>,</span><span>3</span><span>,</span><span>20</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3.098.</span></span></span></span></span>

<table><thead><tr><th>变量</th><th><span><span>FF</span><span><span><span></span><span>F</span></span></span></span></th><th>p-value</th></tr></thead><tbody><tr><td>食品</td><td>18.7</td><td><span><span>&lt;0.001&lt;0.001</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.001</span></span></span></span></td></tr><tr><td>交通</td><td>7.4</td><td>0.0016</td></tr><tr><td>生活</td><td>4.2</td><td>0.0186</td></tr><tr><td>通讯</td><td>4.59</td><td>0.0133</td></tr><tr><td>娱乐</td><td>5.3</td><td>0.0075</td></tr><tr><td>其他</td><td>8.46</td><td><span><span>&lt;0.001&lt;0.001</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.001</span></span></span></span></td></tr><tr><td>衣着</td><td>19.9</td><td><span><span>&lt;0.001&lt;0.001</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.001</span></span></span></span></td></tr><tr><td>教育</td><td>13.1</td><td><span><span>&lt;0.001&lt;0.001</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.001</span></span></span></span></td></tr><tr><td>储蓄</td><td>1.96</td><td>0.1528</td></tr></tbody></table><blockquote><p>[!result]
在 5% 显著性水平下，除<strong>储蓄</strong>外的所有聚类变量在四个簇之间均存在显著差异。</p></blockquote></section><section><h4>7.3.2 <span><span>χ2\chi^2</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 检验<a href="#732-χ2chi2χ2-检验"><span>#</span></a></h4><p>带列百分比的二维频数表如下：</p>

<table><thead><tr><th>顾客类型</th><th>簇 1</th><th>簇 2</th><th>簇 3</th><th>簇 4</th></tr></thead><tbody><tr><td>百货商店</td><td>6 (66.67%)</td><td>1 (14.29%)</td><td>2 (40.00%)</td><td>0 (0%)</td></tr><tr><td>超市</td><td>3 (33.33%)</td><td>2 (28.57%)</td><td>3 (60.00%)</td><td>0 (0%)</td></tr><tr><td>本地杂货店</td><td>0 (0%)</td><td>4 (57.14%)</td><td>0 (0%)</td><td>3 (100%)</td></tr></tbody></table><p>检验统计量为</p><span><span><span>χ2=17.4&gt;12.59=χ0.05,62.\chi^2
=
17.4
&gt;
12.59
=
\chi^2_{0.05,6}.</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>17.4</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>12.59</span><span></span><span>=</span><span></span></span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>0.05</span><span>,</span><span>6</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>并且，</p><span><span><span>p-value=0.0076&lt;0.05.p\text{-value}
=
0.0076
&lt;
0.05.</span><span><span><span></span><span>p</span><span><span>-value</span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.0076</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.05.</span></span></span></span></span><p>因此，商店类型与聚类结果之间存在关联。</p><p>讲义对各簇的刻画如下：</p><ul>
<li>簇 1：百货商店</li>
<li>簇 2：本地杂货店</li>
<li>簇 3：超市</li>
<li>簇 4：本地杂货店</li>
</ul></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/others-01/</id>
      <title type="text">OpenFic 部署全过程</title>
      <published>2026-09-15T00:00:00.000Z</published>
      <updated>2026-09-15T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/others-01/"/>
      <summary type="text">介绍在服务器上部署自托管 OpenFic 的过程</summary>
      <content type="html"><![CDATA[<p>关于 OpenFic 的简介，请访问<a href="https://github.com/syrizelink/OpenFic" target="_blank">原项目链接</a>。本篇仅记录我在 VPS 上部署自托管 OpenFic 的全过程。</p>
<p>我的部署流程大致分为两个阶段：</p>
<ol>
<li>第一阶段：直接部署官方 repo</li>
<li>第二阶段：部署自己 fork 的版本（添加了一些新功能支持）</li>
</ol>
<section><h1>1. 官方版本部署<a href="#1-官方版本部署"><span>#</span></a></h1><p>由于我的 VPS 上先前已经部署了 Dcomost，所以对于 OpenFic，我的部署方案结构大致为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                         </span></span><span>VPS</span></div></div><div><div><div>2</div></div><div><span><span>                          </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>                    </span></span><span>Docker Caddy</span></div></div><div><div><div>4</div></div><div><span><span>                   </span></span><span>:80 / :443</span></div></div><div><div><div>5</div></div><div><span><span>                    </span></span><span>/        \</span></div></div><div><div><div>6</div></div><div><span><span>                   </span></span><span>/          \</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>story.example.com             fiction.example.com</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>│                            │</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>▼                            ▼</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>docmost:3000                 openfic:8000</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>│</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>┌───┴────┐</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>▼        ▼</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>PostgreSQL  Redis</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里简要介绍一下 Docker Caddy：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/caddy_intro.png" alt="" /></p><p>由于先前的 Docmost 已经引入了一个 Caddy，所以这里就直接复用了先前的。</p><section><h2>1.1 前期工作<a href="#11-前期工作"><span>#</span></a></h2><section><h3>1.1.1 新增子域名<a href="#111-新增子域名"><span>#</span></a></h3><p>在此之前，我已经有了一个 Dcomost 的子域名：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A    story      1.2.3.4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个域名我们保留不动，然后再添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A    fiction    1.2.3.4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>于是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>story.example.com   → 1.2.3.4</span></div></div><div><div><div>2</div></div><div><span>fiction.example.com → 1.2.3.4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后我们可以检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>getent</span><span> </span><span>ahostsv4</span><span> </span><span>fiction.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条指令应该返回我们的 VPS 公网 IP。</p><p>而关于公网端口的开放，下列端口</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>22</span></div></div><div><div><div>2</div></div><div><span>80</span></div></div><div><div><div>3</div></div><div><span>443</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>已经足够。</p></section><section><h3>1.1.2 建立 Caddy 和 OpenFic 的共享 Docker 网络<a href="#112-建立-caddy-和-openfic-的共享-docker-网络"><span>#</span></a></h3><p>执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>network</span><span> </span><span>create</span><span> </span><span>reverse_proxy</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>network</span><span> </span><span>ls</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们应该看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>reverse_proxy</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>1.2 修改现有 Docmost Compose<a href="#12-修改现有-docmost-compose"><span>#</span></a></h2><p>由于我先前已经部署了 Docmost，所以我先进入：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编辑：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>nano</span><span> </span><span>docker-compose.yml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我当时的 Caddy 大概是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>services</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>caddy</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>image</span><span>: </span><span>caddy:2</span></div></div><div><div><div>4</div></div><div><span>    </span><span>ports</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>- </span><span>"80:80"</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>- </span><span>"443:443"</span></div></div><div><div><div>7</div></div><div><span>    </span><span>volumes</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>- </span><span>./Caddyfile:/etc/caddy/Caddyfile:ro</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>- </span><span>caddy_data:/data</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>- </span><span>caddy_config:/config</span></div></div><div><div><div>11</div></div><div><span>    </span><span>depends_on</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>- </span><span>docmost</span></div></div><div><div><div>13</div></div><div><span>    </span><span>restart</span><span>: </span><span>unless-stopped</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>将其修改成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>services</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>caddy</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>image</span><span>: </span><span>caddy:2</span></div></div><div><div><div>4</div></div><div><span>    </span><span>ports</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>- </span><span>"80:80"</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>- </span><span>"443:443"</span></div></div><div><div><div>7</div></div><div><span>    </span><span>volumes</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>- </span><span>./Caddyfile:/etc/caddy/Caddyfile:ro</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>- </span><span>caddy_data:/data</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>- </span><span>caddy_config:/config</span></div></div><div><div><div>11</div></div><div><span>    </span><span>depends_on</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>- </span><span>docmost</span></div></div><div><div><div>13</div></div><div><span>    </span><span>restart</span><span>: </span><span>unless-stopped</span></div></div><div><div><div>14</div></div><div><span>    </span><span>networks</span><span>:</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>- </span><span>default</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>- </span><span>reverse_proxy</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>- </span><span>default</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>非常重要，因为 Caddy 仍然需要访问：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>docmost:3000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后在这个 Compose 文件<strong>最底部</strong>，当时已经有：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>volumes</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>docmost</span><span>:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>db_data</span><span>:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>redis_data</span><span>:</span></div></div><div><div><div>5</div></div><div><span>  </span><span>caddy_data</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>caddy_config</span><span>:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在它后面添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>networks</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>reverse_proxy</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>external</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以结尾应该类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>volumes</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>docmost</span><span>:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>db_data</span><span>:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>redis_data</span><span>:</span></div></div><div><div><div>5</div></div><div><span>  </span><span>caddy_data</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>caddy_config</span><span>:</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>networks</span><span>:</span></div></div><div><div><div>9</div></div><div><span>  </span><span>reverse_proxy</span><span>:</span></div></div><div><div><div>10</div></div><div><span>    </span><span>external</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>config</span><span> </span><span>-q</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有输出就说明 YAML 基本正常。</p></section><section><h2>1.3 让现有 Caddy 加入新网络<a href="#13-让现有-caddy-加入新网络"><span>#</span></a></h2><p>应用修改：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>up</span><span> </span><span>-d</span><span> </span><span>caddy</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>ps</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该仍然：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>caddy     Up</span></div></div><div><div><div>2</div></div><div><span>docmost   Up</span></div></div><div><div><div>3</div></div><div><span>db        Up</span></div></div><div><div><div>4</div></div><div><span>redis     Up</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再确认 Docmost 没受影响：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>-fsS</span><span> </span><span>https://story.example.com/api/health</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>正常的话说明我们目前的修改成功。</p></section><section><h2>1.4 生成 OpenFic 密码<a href="#14-生成-openfic-密码"><span>#</span></a></h2><p>和 Docmost 类似，我也创建了一个 <code>~/openfic</code> 目录，与 <code>~/docmost</code> 平级，我的目录结构类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>~</span></div></div><div><div><div>2</div></div><div><span>├── docmost/</span></div></div><div><div><div>3</div></div><div><span>│   ├── Caddyfile</span></div></div><div><div><div>4</div></div><div><span>│   └── docker-compose.yml</span></div></div><div><div><div>5</div></div><div><span>│</span></div></div><div><div><div>6</div></div><div><span>└── openfic/</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>├── .env</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>├── docker-compose.yml</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>└── data/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>接着我们执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>openssl</span><span> </span><span>rand</span><span> </span><span>-base64</span><span> </span><span>36</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一步生成的就是一个随机的强密码，当然你也可以用自己的，我们复制输出结果。</p><p>然后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>vi</span><span> </span><span>~/openfic/.env</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>填写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>OPENFIC_AUTH_PASSWORD</span><span><span>=</span><span>这里换成刚生成的密码</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>OPENFIC_AUTH_PASSWORD</span><span><span>=</span><span>VJKy2pFTZ7A9VdgJmF9r2CVg8SLwKUZmP9C8X7fhdnL7F9hJ</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保存后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>chmod</span><span> </span><span>600</span><span> </span><span>~/openfic/.env</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果不配置这个变量，应用级密码保护就是关闭状态。也就是说，任何人访问你的域名都能进入你的系统。</p></section><section><h2>1.5 创建 OpenFic Compose<a href="#15-创建-openfic-compose"><span>#</span></a></h2><p>执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/openfic</span></div></div><div><div><div>2</div></div><div><span>vi</span><span> </span><span>docker-compose.yml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>写入：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>services</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>openfic</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>image</span><span>: </span><span>ghcr.io/syrizelink/openfic:latest</span></div></div><div><div><div>4</div></div><div><span>    </span><span>container_name</span><span>: </span><span>openfic</span></div></div><div><div><div>5</div></div><div><span>    </span><span>restart</span><span>: </span><span>unless-stopped</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>environment</span><span>:</span></div></div><div><div><div>8</div></div><div><span>      </span><span>OPENFIC_DATA_DIR</span><span>: </span><span>/data</span></div></div><div><div><div>9</div></div><div><span>      </span><span>OPENFIC_AUTH_PASSWORD</span><span>: </span><span>"${OPENFIC_AUTH_PASSWORD}"</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>      </span><span># Caddy 位于另一个 Docker 网络地址，</span></div></div><div><div><div>12</div></div><div><span>      </span><span># 允许 Uvicorn 接受反向代理提供的 HTTPS 信息。</span></div></div><div><div><div>13</div></div><div><span>      </span><span>FORWARDED_ALLOW_IPS</span><span>: </span><span>"*"</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>volumes</span><span>:</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>- </span><span>./data:/data</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>networks</span><span>:</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>- </span><span>reverse_proxy</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>networks</span><span>:</span></div></div><div><div><div>22</div></div><div><span>  </span><span>reverse_proxy</span><span>:</span></div></div><div><div><div>23</div></div><div><span>    </span><span>external</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们让 OpenFic 官方镜像本身在容器内监听 <code>0.0.0.0:8000</code>，同时把 <code>/data</code> 设置成其持久化数据目录。</p><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>config</span><span> </span><span>-q</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>1.6 启动 OpenFic 并验证 Caddy 能不能找到 OpenFic<a href="#16-启动-openfic-并验证-caddy-能不能找到-openfic"><span>#</span></a></h2><p>首先：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/openfic</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>pull</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>up</span><span> </span><span>-d</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>ps</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>NAME      IMAGE                                   STATUS</span></div></div><div><div><div>2</div></div><div><span>openfic   ghcr.io/syrizelink/openfic:latest      Up</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查看日志：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>logs</span><span> </span><span>--tail=100</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果想实时查看：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>logs</span><span> </span><span>-f</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>退出日志查看：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Ctrl+C</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p>因为现在 OpenFic没有映射宿主机端口，所以我们不能再：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>127.0.0.1:8000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以直接从 Caddy 容器测试 Docker 网络：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>exec</span><span> </span><span>caddy</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>wget</span><span> </span><span>-qO-</span><span> </span><span>http://openfic:8000/api/v1/health</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>正常的话应该看到类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"status"</span><span>:</span><span>"healthy"</span><span>,</span><span>"version"</span><span>:</span><span>"..."</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果成功，说明：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Caddy → OpenFic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条链路已经打通。</p></section><section><h2>1.7 修改现有的 Caddyfile<a href="#17-修改现有的-caddyfile"><span>#</span></a></h2><p>现在回到：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>2</div></div><div><span>vi</span><span> </span><span>Caddyfile</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我当时应该类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>story.example.com {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>reverse_proxy docmost:3000</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是我原来的部署方式。</p><p>然后我将其改成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>story.example.com {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>reverse_proxy docmost:3000</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fiction.example.com {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>reverse_proxy openfic:8000</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Info</div></div><div><p>将两个域名替换成你自己的真实域名。</p></div></div></section><section><h2>1.8 检查并重新加载 Caddy<a href="#18-检查并重新加载-caddy"><span>#</span></a></h2><p>先验证：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>exec</span><span> </span><span>caddy</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>caddy</span><span> </span><span>validate</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>--config</span><span> </span><span>/etc/caddy/Caddyfile</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果看到类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Valid configuration</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>重新加载：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>exec</span><span> </span><span>caddy</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>caddy</span><span> </span><span>reload</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>--config</span><span> </span><span>/etc/caddy/Caddyfile</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p>然后我们就可以访问 OpenFic，用浏览器打开：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://fiction.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Caddy 会负责：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DNS</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>VPS :443</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>Caddy</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>openfic:8000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>并自动为新的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fiction.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>申请 HTTPS 证书。</p><p>然后 OpenFic 会要求输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>OPENFIC_AUTH_PASSWORD</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>设置的密码。</p><p>它验证成功后会生成 HTTP-only Cookie；通过 HTTPS 使用时会设置 Secure Cookie。</p><section><h3>1.8.1 如果出现错误<a href="#181-如果出现错误"><span>#</span></a></h3><p>这一步我遇到了一个问题，就是浏览器访问我的域名显示：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>此网站无法提供安全连接</span></div></div><div><div><div>2</div></div><div><span>【我的域名】发送的响应无效。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>ERR_SSL_PROTOCOL_ERROR</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果遇到了这个问题，可以按下面步骤排查：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 原来的 Docmost 是否正常</span></div></div><div><div><div>2</div></div><div><span>curl</span><span> </span><span>-I</span><span> </span><span>https://story.你的域名</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 2. 新域名解析到了哪里</span></div></div><div><div><div>5</div></div><div><span>getent</span><span> </span><span>ahostsv4</span><span> </span><span>fiction.你的域名</span></div></div><div><div><div>6</div></div><div><span>getent</span><span> </span><span>ahostsv6</span><span> </span><span>fiction.你的域名</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 3. 查看实际 Caddyfile</span></div></div><div><div><div>9</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>10</div></div><div><span>cat</span><span> </span><span>Caddyfile</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span># 4. 验证并重新加载</span></div></div><div><div><div>13</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>exec</span><span> </span><span>caddy</span><span> </span><span>\</span></div></div><div><div><div>14</div></div><div><span>  </span><span>caddy</span><span> </span><span>validate</span><span> </span><span>--config</span><span> </span><span>/etc/caddy/Caddyfile</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>exec</span><span> </span><span>caddy</span><span> </span><span>\</span></div></div><div><div><div>17</div></div><div><span>  </span><span>caddy</span><span> </span><span>reload</span><span> </span><span>--config</span><span> </span><span>/etc/caddy/Caddyfile</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span># 5. 看证书申请日志</span></div></div><div><div><div>20</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>logs</span><span> </span><span>--tail=200</span><span> </span><span>caddy</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span># 6. 测试 HTTPS 握手</span></div></div><div><div><div>23</div></div><div><span>curl</span><span> </span><span>-vkI</span><span> </span><span>https://fiction.你的域名</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>你可以直接把上面指令的输出贴给 AI 让它帮你查问题。就我的经历来说，我问题的根因是：虽然在 <code>~/docmost/Caddyfile</code> 里已经包含了新的 OpenFic 域名，但正在运行的 Caddy 容器实际加载的仍然是旧版 Caddyfile，也就是只包含 Docmost 的域名。</p><p>以我的 VPS 为例，我执行</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>exec</span><span> </span><span>caddy</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>cat</span><span> </span><span>/etc/caddy/Caddyfile</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后就发现只有</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>story.example.com {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>reverse_proxy docmost:3000</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就需要重新创建 Caddy 容器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>up</span><span> </span><span>-d</span><span> </span><span>--force-recreate</span><span> </span><span>caddy</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样，新的容器就会重新执行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>宿主机 ./Caddyfile</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>bind mount</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>/etc/caddy/Caddyfile</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>1.9 以后两个项目的管理方式<a href="#19-以后两个项目的管理方式"><span>#</span></a></h2><p>这是采用两个 Compose 项目最大的好处。</p><p>以我的 VPS 为例，如果要管理 Docmost：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/docmost</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>ps</span></div></div><div><div><div>4</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>logs</span><span> </span><span>-f</span></div></div><div><div><div>5</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>restart</span><span> </span><span>docmost</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>管理 OpenFic：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/openfic</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>ps</span></div></div><div><div><div>4</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>logs</span><span> </span><span>-f</span></div></div><div><div><div>5</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>restart</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>更新 OpenFic：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/openfic</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>pull</span></div></div><div><div><div>4</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>up</span><span> </span><span>-d</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样，两边就互不影响。唯一共用的组件是就 Caddy，因此以后不用在 <code>~/openfic</code> 中再创建 Caddy 容器。</p></section></section>
<section><h1>2. Fork 版本部署<a href="#2-fork-版本部署"><span>#</span></a></h1><section><h2>2.1 记录已有配置<a href="#21-记录已有配置"><span>#</span></a></h2><p>我们先找到当前 OpenFic 容器，并记录现有配置。查看所有运行中的容器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>ps</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>或者只显示比较有用的信息：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>ps</span><span> </span><span>--format</span><span> </span><span>'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们应该能看到类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>NAMES      IMAGE                                  STATUS       PORTS</span></div></div><div><div><div>2</div></div><div><span>openfic    ghcr.io/syrizelink/openfic:latest      Up 3 days    0.0.0.0:8000-&gt;8000/tcp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面假设容器名就是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你的实际容器名不同，把后面命令里的 <code>openfic</code> 替换掉。</p><p>先记录当前镜像：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>inspect</span><span> </span><span>openfic</span><span> </span><span>--format</span><span> </span><span>'Image={{.Config.Image}}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再记录端口：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>port</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再查看 restart policy：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>inspect</span><span> </span><span>openfic</span><span> </span><span>--format</span><span> </span><span>'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后查看环境变量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>inspect</span><span> </span><span>openfic</span><span> </span><span>--format</span><span> </span><span>'{{range .Config.Env}}{{println .}}{{end}}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>2.2 确认 <code>data</code> 有没有持久化挂载。<a href="#22-确认-data-有没有持久化挂载"><span>#</span></a></h2><p>如果是按照我的第一阶段部署方式，那么数据是持久化的。也就是说，以后更新版本，重新创建容器不会影响服务器端保存的数据。</p><p>不过我们还是可以检查一下，运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>inspect</span><span> </span><span>openfic</span><span> </span><span>--format</span><span> </span><span>'{{range .Mounts}}{{printf "Type=%s Name=%s Source=%s Destination=%s\n" .Type .Name .Source .Destination}}{{end}}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你看到类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Type=volume Name=openfic Source=/var/lib/docker/volumes/openfic/_data Destination=/data</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>那么就没啥问题，这表示：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>容器 /data</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>Docker volume: openfic</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>/var/lib/docker/volumes/openfic/_data</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>换镜像、换容器都不会自动删除这个 volume。</p><p>如果看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Type=bind Name= Source=/home/user/openfic-data Destination=/data</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也没问题。</p><p>这是 bind mount：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>服务器 /home/user/openfic-data</span></div></div><div><div><div>2</div></div><div><span><span>                </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>容器 /data</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同样是持久化的。</p><p>但如果这条命令<strong>完全没有出现 <code>Destination=/data</code></strong>，就要停止后续部署操作，因为这意味着你的数据可能还在旧容器 writable layer 里，具体的迁移方法可以参考其他教程，或者就具体情况询问 AI.</p><p>你还可以专门只检查 <code>/data</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>inspect</span><span> </span><span>openfic</span><span> </span><span>--format</span><span> </span><span>'{{range .Mounts}}{{if eq .Destination "/data"}}Type={{.Type}} Name={{.Name}} Source={{.Source}}{{end}}{{end}}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果按照官方 README 的命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>run</span><span> </span><span>-d</span><span> </span><span>-p</span><span> </span><span>8000:8000</span><span> </span><span>-v</span><span> </span><span>"openfic:/data"</span><span> </span><span>--name</span><span> </span><span>openfic</span><span> </span><span>ghcr.io/syrizelink/openfic:latest</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>部署，那么正常应该得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Type=volume Name=openfic ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>2.3 检查 <code>/data</code> 内的数据是否真的存在。<a href="#23-检查-data-内的数据是否真的存在"><span>#</span></a></h2><p>运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>exec</span><span> </span><span>openfic</span><span> </span><span>sh</span><span> </span><span>-lc</span><span> </span><span>'ls -lah /data'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>正常情况下应该能看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.key</span></div></div><div><div><div>2</div></div><div><span>openfic.db</span></div></div><div><div><div>3</div></div><div><span>checkpoints.db</span></div></div><div><div><div>4</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也可以专门检查两个最关键文件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>exec</span><span> </span><span>openfic</span><span> </span><span>sh</span><span> </span><span>-lc</span><span> </span><span>'</span></div></div><div><div><div>2</div></div><div><span>test -s /data/openfic.db &amp;&amp; echo "openfic.db: OK" || echo "openfic.db: MISSING";</span></div></div><div><div><div>3</div></div><div><span>test -s /data/.key &amp;&amp; echo ".key: OK" || echo ".key: MISSING";</span></div></div><div><div><div>4</div></div><div><span>'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>理想输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>openfic.db: OK</span></div></div><div><div><div>2</div></div><div><span>.key: OK</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其中 <code>.key</code> 很重要。OpenFic 启动时会优先读取数据目录里的 <code>.key</code>；如果没有，就会生成一个新的 key。已经保存在数据库里的 API Key 是用旧 key 加密的，因此如果你丢了 <code>.key</code>，旧的 Provider API Key 可能就无法正常解密。</p></section><section><h2>2.4 从你的 GitHub 仓库拉取修改后的源码。<a href="#24-从你的-github-仓库拉取修改后的源码"><span>#</span></a></h2><p>clone 你的 fork：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>https://github.com/YOUR_USERNAME/OpenFic.git</span><span> </span><span>OpenFic-custom</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>OpenFic-custom</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查当前分支：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>branch</span><span> </span><span>--show-current</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查最后一次 commit：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>-1</span><span> </span><span>--oneline</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查工作目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>status</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>理想状态：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nothing to commit, working tree clean</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你的代码以后已经 clone 过了，就不需要重新 clone：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/OpenFic-custom</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>pull</span><span> </span><span>--ff-only</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以把 Docker 镜像 tag 直接和 Git commit 绑定：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>TAG</span><span><span>=</span><span>$(</span></span><span>git</span><span> </span><span>rev-parse</span><span> </span><span>--short</span><span> </span><span>HEAD</span><span>)</span></div></div><div><div><div>2</div></div><div><span>echo</span><span> </span><span>"</span><span>$TAG</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>比如输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>8f47e21</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>那么你的镜像就叫：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>openfic-custom:8f47e21</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样将来出了问题，我们就可以知道服务器跑的到底是哪版源码。</p><hr /></section><section><h2>2.5 在旧 OpenFic 继续运行的情况下，构建新镜像。<a href="#25-在旧-openfic-继续运行的情况下构建新镜像"><span>#</span></a></h2><p>这一步先不碰旧容器，运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>build</span><span> </span><span>-t</span><span> </span><span>openfic-custom:</span><span>$TAG</span><span> </span><span>.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>想看更完整日志可以：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>build</span><span> </span><span>--progress=plain</span><span> </span><span>-t</span><span> </span><span>openfic-custom:</span><span>$TAG</span><span> </span><span>.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>OpenFic 当前 Dockerfile 会先编译前端，然后安装 Python 后端依赖，再复制后端源码，最后把前端产物和 Python 后端一起放进镜像；最终应用仍然把运行数据目录定义为 <code>/data</code>。</p><p>有一个地方需要特别注意：Dockerfile 使用了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>uv sync --frozen</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因此，如果你修改了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>backend/pyproject.toml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>增加或者修改 Python 依赖，却没有同步提交：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>backend/uv.lock</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>构建可能直接失败。</p><p>如果你只是改了 OpenFic 自己的 Python 源码，例如 Vertex adapter，而没有新增依赖，通常不会遇到这个问题。</p><p>构建完成后检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>images</span><span> | </span><span>grep</span><span> </span><span>openfic-custom</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该出现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>openfic-custom    8f47e21    ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>2.6 先用空数据启动一次新镜像做 smoke test<a href="#26-先用空数据启动一次新镜像做-smoke-test"><span>#</span></a></h2><p>先启动临时测试容器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>run</span><span> </span><span>-d</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--name</span><span> </span><span>openfic-smoke</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-p</span><span> </span><span>18000:8000</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>openfic-custom:</span><span>$TAG</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>ps</span><span> | </span><span>grep</span><span> </span><span>openfic-smoke</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查看日志：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>logs</span><span> </span><span>--tail</span><span> </span><span>100</span><span> </span><span>openfic-smoke</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试 HTTP：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>-I</span><span> </span><span>http://127.0.0.1:18000/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>或者：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>-fsS</span><span> </span><span>http://127.0.0.1:18000/</span><span> &gt;</span><span>/dev/null</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>"OpenFic smoke test OK"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>OpenFic smoke test OK</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>至少说明我们的镜像能正常启动。</p><p>然后删除测试容器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>stop</span><span> </span><span>openfic-smoke</span></div></div><div><div><div>2</div></div><div><span>docker</span><span> </span><span>rm</span><span> </span><span>openfic-smoke</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>2.7 正式切换前，停旧容器并完整备份 <code>/data</code><a href="#27-正式切换前停旧容器并完整备份-data"><span>#</span></a></h2><p>这一步可选，如果还没存什么重要数据，或者就自己一个人用，没有多用户的话，也可以直接跳。</p><p>创建备份目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>BACKUP_DIR</span><span>=</span><span>"</span><span>$HOME</span><span>/openfic-backup-$(</span><span>date</span><span> +%Y%m%d-%H%M%S)"</span></div></div><div><div><div>2</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>"</span><span>$BACKUP_DIR</span><span>/data"</span></div></div><div><div><div>3</div></div><div><span>echo</span><span> </span><span>"</span><span>$BACKUP_DIR</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先停止 OpenFic：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>stop</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后从停止后的容器中复制整个 <code>/data</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>cp</span><span> </span><span>openfic:/data/.</span><span> </span><span>"</span><span>$BACKUP_DIR</span><span>/data/"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意这里特意用了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/data/.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而不是简单 <code>/data/*</code>，这样 <code>.key</code> 这样的隐藏文件也会被复制。</p><p>检查备份：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ls</span><span> </span><span>-lah</span><span> </span><span>"</span><span>$BACKUP_DIR</span><span>/data"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>必须确认类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.key</span></div></div><div><div><div>2</div></div><div><span>openfic.db</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>都存在。</p></section><section><h2>2.8 保留旧容器，不要直接删除，然后启动你的新镜像。<a href="#28-保留旧容器不要直接删除然后启动你的新镜像"><span>#</span></a></h2><p>先把旧容器改名，而不是删除：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>OLD_CONTAINER</span><span>=</span><span>"openfic-old-$(</span><span>date</span><span> +%Y%m%d-%H%M%S)"</span></div></div><div><div><div>2</div></div><div><span>docker</span><span> </span><span>rename</span><span> </span><span>openfic</span><span> </span><span>"</span><span>$OLD_CONTAINER</span><span>"</span></div></div><div><div><div>3</div></div><div><span>echo</span><span> </span><span>"</span><span>$OLD_CONTAINER</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>openfic-old-20260915-005230</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>由于我在第一阶段已经有了 <code>~/openfic/docker-compose.yml</code>，所以启动新镜像这块儿就不手写 <code>docker run</code> 了，而是直接改 compose 文件用我自己的镜像。</p><p>我的原 compose 文件大致类似：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>services</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>openfic</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>image</span><span>: </span><span>ghcr.io/syrizelink/openfic:latest</span></div></div><div><div><div>4</div></div><div><span>    </span><span>container_name</span><span>: </span><span>openfic</span></div></div><div><div><div>5</div></div><div><span>    </span><span>restart</span><span>: </span><span>unless-stopped</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>environment</span><span>:</span></div></div><div><div><div>8</div></div><div><span>      </span><span>OPENFIC_DATA_DIR</span><span>: </span><span>/data</span></div></div><div><div><div>9</div></div><div><span>      </span><span>OPENFIC_AUTH_PASSWORD</span><span>: </span><span>"${OPENFIC_AUTH_PASSWORD}"</span></div></div><div><div><div>10</div></div><div><span>      </span><span>FORWARDED_ALLOW_IPS</span><span>: </span><span>"*"</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>volumes</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>- </span><span>./data:/data</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>networks</span><span>:</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>- </span><span>reverse_proxy</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>networks</span><span>:</span></div></div><div><div><div>19</div></div><div><span>  </span><span>reverse_proxy</span><span>:</span></div></div><div><div><div>20</div></div><div><span>    </span><span>external</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们只需要把原本的 <code>image: ghcr.io/syrizelink/openfic:latest</code> 给改成我们刚刚构建的镜像就可以，比如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>image</span><span>: </span><span>openfic-custom:abc1234</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>改完之后，在正式启动前，先让 compose 帮你检查配置：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/openfic</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>主要关注</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>image:</span></div></div><div><div><div>2</div></div><div><span>volumes:</span></div></div><div><div><div>3</div></div><div><span>networks:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>是否正确。</p></section><section><h2>2.9 如果新版本有问题，怎么快速回滚<a href="#29-如果新版本有问题怎么快速回滚"><span>#</span></a></h2><p>因为我们没有删除旧容器，所以回滚很简单。</p><p>先停止新版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>stop</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>删掉新容器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>rm</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把旧容器恢复名字：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>rename</span><span> </span><span>"</span><span>$OLD_CONTAINER</span><span>"</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>启动：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>start</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>logs</span><span> </span><span>--tail</span><span> </span><span>100</span><span> </span><span>openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样就又回到了原来的镜像。</p><p>不过这里有一个例外：如果你的新代码包含了<strong>数据库 schema migration</strong>，并且新版本已经修改了 <code>openfic.db</code>，旧程序未必能兼容新版数据库。</p><p>如果你的 fork 没有修改数据库结构，一般风险就较低。</p></section><section><h2>2.10 Google 应用默认凭证配置<a href="#210-google-应用默认凭证配置"><span>#</span></a></h2><p><a href="https://github.com/Old-Guardian/OpenFic" target="_blank">我的 fork 版本</a>修改了原项目的 provider，使其支持 Google Vertex AI. 想要使用 Vertex，需要按照 <a href="https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/start/gcp-auth?hl=zh_CN&amp;_gl=1*1owh78*_ga*ODk2MjAzMjU1LjE3ODkyMzAxMDY.*_ga_WH2QY8WWF5*czE3ODk0Mzc2NzgkbzgkZzEkdDE3ODk0Mzc3NjgkajM0JGwwJGgw#curl" target="_blank">Google 官方文档</a>配置应用默认凭证，下面就记录我的配置过程。</p><p>首先，在服务器上以平时的 <code>ubuntu</code> 用户名运行官方命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>bash</span><span> </span><span>&lt;(</span><span>curl</span><span> </span><span>-sSL</span><span> https://storage.googleapis.com/cloud-samples-data/adc/setup_adc.sh)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它会帮助安装/运行 <code>gcloud</code> 并设置 ADC. 这里要注意的点就是建议不要用 <code>sudo</code> 执行，否则 ADC 有可能最终落在 root 的 home，而不是我们用户名所在的目录。</p><p>完成认证后确认文件存在：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ls</span><span> </span><span>-lah</span><span> </span><span>~/.config/gcloud/application_default_credentials.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后把我们的 <code>~/openfic/docker-compose.yml</code> 文件改成类似：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>services</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>openfic</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>image</span><span>: </span><span>openfic-custom:124921b</span></div></div><div><div><div>4</div></div><div><span>    </span><span>container_name</span><span>: </span><span>openfic</span></div></div><div><div><div>5</div></div><div><span>    </span><span>restart</span><span>: </span><span>unless-stopped</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>environment</span><span>:</span></div></div><div><div><div>8</div></div><div><span>      </span><span>OPENFIC_DATA_DIR</span><span>: </span><span>/data</span></div></div><div><div><div>9</div></div><div><span>      </span><span>OPENFIC_AUTH_PASSWORD</span><span>: </span><span>"${OPENFIC_AUTH_PASSWORD}"</span></div></div><div><div><div>10</div></div><div><span>      </span><span>FORWARDED_ALLOW_IPS</span><span>: </span><span>"*"</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 这行</span></div></div><div><div><div>13</div></div><div><span>      </span><span>GOOGLE_APPLICATION_CREDENTIALS</span><span>: </span><span>/run/secrets/gcp/application_default_credentials.json</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>volumes</span><span>:</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>- </span><span>./data:/data</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 还有这行</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>- </span><span>/home/ubuntu/.config/gcloud/application_default_credentials.json:/run/secrets/gcp/application_default_credentials.json:ro</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>networks</span><span>:</span></div></div><div><div><div>22</div></div><div><span><span>      </span></span><span>- </span><span>reverse_proxy</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>networks</span><span>:</span></div></div><div><div><div>25</div></div><div><span>  </span><span>reverse_proxy</span><span>:</span></div></div><div><div><div>26</div></div><div><span>    </span><span>external</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>接着我们重新创建 OpenFic 容器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/openfic</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>compose</span><span> </span><span>up</span><span> </span><span>-d</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>ps</span><span> </span><span>--filter</span><span> </span><span>name=openfic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p>不要先测试 Vertex，先检查文件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>exec</span><span> </span><span>openfic</span><span> </span><span>sh</span><span> </span><span>-lc</span><span> </span><span>'</span></div></div><div><div><div>2</div></div><div><span>ls -l /run/secrets/gcp/application_default_credentials.json</span></div></div><div><div><div>3</div></div><div><span>'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该能看到类似：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-rw-------</span><span> </span><span>...</span><span> </span><span>/run/secrets/gcp/application_default_credentials.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后确认环境变量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>exec</span><span> </span><span>openfic</span><span> </span><span>sh</span><span> </span><span>-lc</span><span> </span><span>'</span></div></div><div><div><div>2</div></div><div><span>echo "$GOOGLE_APPLICATION_CREDENTIALS"</span></div></div><div><div><div>3</div></div><div><span>'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该输出：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>/run/secrets/gcp/application_default_credentials.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再做一个存在性测试：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>docker</span><span> </span><span>exec</span><span> </span><span>openfic</span><span> </span><span>sh</span><span> </span><span>-lc</span><span> </span><span>'</span></div></div><div><div><div>2</div></div><div><span>test -r "$GOOGLE_APPLICATION_CREDENTIALS" \</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>&amp;&amp; echo "ADC readable: OK" \</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|| echo "ADC readable: FAILED"</span></div></div><div><div><div>5</div></div><div><span>'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该得到：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ADC</span><span> </span><span>readable:</span><span> </span><span>OK</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后的最后，在浏览器里打开网页，测试 provider 连接与 model 连接是否正常，就大功告成！</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/stat-03/</id>
      <title type="text">层次聚类方法</title>
      <published>2026-09-10T00:00:00.000Z</published>
      <updated>2026-09-10T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/stat-03/"/>
      <summary type="text">介绍简单的层次聚类方法，解决穷举所有聚类方案不可行的问题。</summary>
      <content type="html"><![CDATA[<p>假设我们有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个可区分的对象，我们要将它们划分成 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个非空、不编号的簇，一共有多少种分法？</p>
<p>第二类斯特林数（Stirling numbers of the second kind）给出了答案：</p>
<span><span><span>S(n,k)=k⋅S(n−1,k)+S(n−1,k−1)=1k!∑i=0k(−1)k−i(ki)inS(n,k)=k \cdot S(n-1, k)+S(n-1, k-1)=\frac{1}{k!}\sum_{i=0}^k(-1)^{k-i}\begin{pmatrix}k \\ i\end{pmatrix}i^n </span><span><span><span></span><span>S</span><span>(</span><span>n</span><span>,</span><span></span><span>k</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span></span><span>⋅</span><span></span></span><span><span></span><span>S</span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>k</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>S</span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>k</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span>!</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>0</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span>−</span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>i</span></span></span></span></span></span></span></span></span><span></span><span><span><span>(</span></span><span><span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>)</span></span></span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span></span>
<p>可以想象，当对象非常多时，我们几乎不可能考虑所有的分组配置。这就引出了下面我们要介绍的主题——层次聚类方法。</p>
<div><div><div></div><div>Info</div></div><div><p>本篇节选自我的课程笔记，因此标题序号沿用了原笔记中的排列（懒得改）。</p></div></div>
<section><h2>4. 层次聚类方法（Hierarchical clustering methods）<a href="#4-层次聚类方法hierarchical-clustering-methods"><span>#</span></a></h2><p>层次聚类方法无需考察所有可能的分组配置，就能找到较为合理的簇。</p><ul>
<li>即使拥有强大而快速的计算机，也很少会考察所有的分组可能。</li>
<li>层次聚类技术要么通过一系列相继的合并、要么通过一系列相继的分裂来进行。</li>
</ul><p><strong>凝聚法（Agglomerative method）</strong>：</p><p>凝聚法是一种自下而上的构建过程。</p><ul>
<li>每个对象最初各自作为一个簇。</li>
<li>相近的簇或个体逐渐被合并。</li>
<li>该过程持续进行，直到所有个体被归入一个大簇。</li>
</ul><hr /><p><strong>分裂法（Divisive method ）</strong>：</p><p>分裂法是一种自上而下的拆分过程。</p><ul>
<li>它从一个包含全部个体的簇开始。</li>
<li>最不相似的观测被逐步分裂出去。</li>
<li>该过程持续进行，直到每个观测各自成为一个簇。</li>
</ul><hr /><p>上述技术的结果可以用一种称为<strong>谱系图</strong>（树状图，dendrogram）的二维图形来展示：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904151037833.png" alt="" /></p><p>从左往右是凝聚，从右往左是分裂。</p><section><h3>4.1 连结方法（Linkage methods）<a href="#41-连结方法linkage-methods"><span>#</span></a></h3><p>连结方法是凝聚型的层次聚类程序。</p><section><h4>4.1.1 一般步骤<a href="#411-一般步骤"><span>#</span></a></h4><ol>
<li>从 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个簇开始，每个簇只含一个个体，并有一个 <span><span>N×NN\times N</span><span><span><span></span><span>N</span><span></span><span>×</span><span></span></span><span><span></span><span>N</span></span></span></span> 的对称距离矩阵或相似性矩阵</li>
</ol><span><span><span>D={dik}.D=\{d_{ik}\}.</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ik</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span>.</span></span></span></span></span><ol>
<li>在距离矩阵中搜索距离最近（或最相似）的簇对。设最相似的簇 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 与 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 之间的距离为</li>
</ol><span><span><span>dUV.d_{UV}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>V</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><ol>
<li>
<p>合并簇 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span>，并将新形成的簇标记为 <span><span>(UV)(UV)</span><span><span><span></span><span>(</span><span>U</span><span>V</span><span>)</span></span></span></span>。</p>
<p>按以下方式更新距离矩阵：</p>
<ul>
<li>删除对应于 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 的行与列，并且</li>
<li>添加一行一列，包含 <span><span>(UV)(UV)</span><span><span><span></span><span>(</span><span>U</span><span>V</span><span>)</span></span></span></span> 与其余各簇之间的距离。</li>
</ul>
</li>
<li>
<p>重复第 2、3 步共 <span><span>N−1N-1</span><span><span><span></span><span>N</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 次。</p>
<p>终止时，所有对象都归入一个簇。</p>
<p>记录：</p>
<ul>
<li>被合并簇的标识，以及</li>
<li>合并发生时的距离或相似性水平。</li>
</ul>
</li>
</ol><hr /></section><section><h4>4.1.2 单连结（Single linkage）<a href="#412-单连结single-linkage"><span>#</span></a></h4><p>簇间距离由<strong>两个最接近的对象</strong>（各来自一个簇）之间的距离定义。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152204600.png" alt="" /></p><p>对于合并后的簇 <span><span>(UV)(UV)</span><span><span><span></span><span>(</span><span>U</span><span>V</span><span>)</span></span></span></span> 和另一个簇 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span>，</p><span><span><span>d(UV)W=min⁡{dUW,dVW}.d_{(UV)W}
=
\min\{d_{UW},d_{VW}\}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>U</span><span>V</span><span>)</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span>.</span></span></span></span></span><p>这里，</p><ul>
<li><span><span>dUWd_{UW}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是簇 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 与 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 的最近邻之间的距离。</li>
<li><span><span>dVWd_{VW}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是簇 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 与 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 的最近邻之间的距离。</li>
</ul><section><h5>单连结例子<a href="#单连结例子"><span>#</span></a></h5><p>五个对象两两之间的距离为：</p><span><span><span>D={dik}=[09361190751037092659081110280].D=\{d_{ik}\}
=
\begin{bmatrix}
0 &amp; 9 &amp; 3 &amp; 6 &amp; 11\\
9 &amp; 0 &amp; 7 &amp; 5 &amp; 10\\
3 &amp; 7 &amp; 0 &amp; 9 &amp; 2\\
6 &amp; 5 &amp; 9 &amp; 0 &amp; 8\\
11 &amp; 10 &amp; 2 &amp; 8 &amp; 0
\end{bmatrix}.</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ik</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>11</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>11</span></span></span><span><span></span><span><span>10</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>也即：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152250309.png" alt="" /></p><section><h6>第 1 阶段<a href="#第-1-阶段"><span>#</span></a></h6><p>五个簇，每个簇只含一个观测（observation）。</p></section><section><h6>第 2 阶段<a href="#第-2-阶段"><span>#</span></a></h6><p>对象 5 与 3 合并成簇 <span><span>(35)(35)</span><span><span><span></span><span>(</span><span>35</span><span>)</span></span></span></span>，最近距离为</p><span><span><span>d53=2.d_{53}=2.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>53</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.</span></span></span></span></span><p>于是</p><span><span><span>d(35)1=min⁡{d31,d51}=min⁡{3,11}=3,d_{(35)1}
=
\min\{d_{31},d_{51}\}
=
\min\{3,11\}
=
3,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>51</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span>3</span><span>,</span><span></span><span>11</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>,</span></span></span></span></span><span><span><span>d(35)2=min⁡{d32,d52}=min⁡{7,10}=7,d_{(35)2}
=
\min\{d_{32},d_{52}\}
=
\min\{7,10\}
=
7,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>52</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span>7</span><span>,</span><span></span><span>10</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span><span>,</span></span></span></span></span><p>且</p><span><span><span>d(35)4=min⁡{d34,d54}=min⁡{9,8}=8.d_{(35)4}
=
\min\{d_{34},d_{54}\}
=
\min\{9,8\}
=
8.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>34</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>54</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span>9</span><span>,</span><span></span><span>8</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>8.</span></span></span></span></span><p>新的距离矩阵为</p><span><span><span>D(35)=[0963905765083780],D_{(35)}
=
\begin{bmatrix}
0 &amp; 9 &amp; 6 &amp; 3\\
9 &amp; 0 &amp; 5 &amp; 7\\
6 &amp; 5 &amp; 0 &amp; 8\\
3 &amp; 7 &amp; 8 &amp; 0
\end{bmatrix},</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>对应簇 <span><span>1,2,4,(35)1,2,4,(35)</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>(</span><span>35</span><span>)</span></span></span></span>，也就是：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152541842.png" alt="" /></p></section><section><h6>第 3 阶段<a href="#第-3-阶段"><span>#</span></a></h6><p>将簇 <span><span>(1)(1)</span><span><span><span></span><span>(</span><span>1</span><span>)</span></span></span></span> 与簇 <span><span>(35)(35)</span><span><span><span></span><span>(</span><span>35</span><span>)</span></span></span></span> 合并得到簇 <span><span>(135)(135)</span><span><span><span></span><span>(</span><span>135</span><span>)</span></span></span></span>，最近距离为</p><span><span><span>3.3.</span><span><span><span></span><span>3.</span></span></span></span></span><p>于是</p><span><span><span>d(135)2=min⁡{d(35)2,d12}=min⁡{7,9}=7,d_{(135)2}
=
\min\{d_{(35)2},d_{12}\}
=
\min\{7,9\}
=
7,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span>7</span><span>,</span><span></span><span>9</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span><span>,</span></span></span></span></span><p>且</p><span><span><span>d(135)4=min⁡{d(35)4,d14}=min⁡{8,6}=6.d_{(135)4}
=
\min\{d_{(35)4},d_{14}\}
=
\min\{8,6\}
=
6.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span>8</span><span>,</span><span></span><span>6</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>6.</span></span></span></span></span><p>新的距离矩阵为</p><span><span><span>D(135)=[076705650],D_{(135)}
=
\begin{bmatrix}
0 &amp; 7 &amp; 6\\
7 &amp; 0 &amp; 5\\
6 &amp; 5 &amp; 0
\end{bmatrix},</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>对应簇 <span><span>(135),2,4(135),2,4</span><span><span><span></span><span>(</span><span>135</span><span>)</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span></span></span></span>，也就是</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152729487.png" alt="" /></p></section><section><h6>第 4 阶段<a href="#第-4-阶段"><span>#</span></a></h6><p>将对象 4 与 2 合并得到簇 <span><span>(24)(24)</span><span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span>，最近距离为</p><span><span><span>5.5.</span><span><span><span></span><span>5.</span></span></span></span></span><p>于是</p><span><span><span>d(135)(24)=min⁡{d(135)2,d(135)4}=min⁡{7,6}=6.d_{(135)(24)}
=
\min\{d_{(135)2},d_{(135)4}\}
=
\min\{7,6\}
=
6.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span><span>(</span><span>24</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>{</span><span>7</span><span>,</span><span></span><span>6</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>6.</span></span></span></span></span><p>距离矩阵变为</p><span><span><span>D(135)(24)=[0660].D_{(135)(24)}
=
\begin{bmatrix}
0 &amp; 6\\
6 &amp; 0
\end{bmatrix}.</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>(</span><span>135</span><span>)</span><span>(</span><span>24</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>也就是：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152816799.png" alt="" /></p></section><section><h6>第 5 阶段<a href="#第-5-阶段"><span>#</span></a></h6><p>簇 <span><span>(135)(135)</span><span><span><span></span><span>(</span><span>135</span><span>)</span></span></span></span> 与 <span><span>(24)(24)</span><span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span> 合并成唯一的簇</p><span><span><span>(12345)(12345)</span><span><span><span></span><span>(</span><span>12345</span><span>)</span></span></span></span></span><p>合并时的距离为</p><span><span><span>6.6.</span><span><span><span></span><span>6.</span></span></span></span></span><p>上述 6 个阶段可以用下面的谱系图表示：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904153020407.png" alt="" /></p><p>右边数字表示对象，下面的数字表示距离。</p><hr /></section></section></section><section><h4>4.1.3 完全连结（Complete linkage）<a href="#413-完全连结complete-linkage"><span>#</span></a></h4><p>簇间距离由<strong>两个相距最远的元素</strong>（各来自一个簇）之间的距离决定。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904155651163.png" alt="" /></p><p>对于簇 <span><span>(UV)(UV)</span><span><span><span></span><span>(</span><span>U</span><span>V</span><span>)</span></span></span></span> 和另一个簇 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span>，</p><span><span><span>d(UV)W=max⁡{dUW,dVW}.d_{(UV)W}
=
\max\{d_{UW},d_{VW}\}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>U</span><span>V</span><span>)</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span>.</span></span></span></span></span><p>%%
如何证明上式成立？
%%
这里，</p><ul>
<li><span><span>dUWd_{UW}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是簇 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 与 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 中相距最远的成员之间的距离。</li>
<li><span><span>dVWd_{VW}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是簇 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 与 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 中相距最远的成员之间的距离。</li>
</ul><section><h5>完全连结例子<a href="#完全连结例子"><span>#</span></a></h5><p>使用同一原始距离矩阵：</p><span><span><span>D=[09361190751037092659081110280].D
=
\begin{bmatrix}
0 &amp; 9 &amp; 3 &amp; 6 &amp; 11\\
9 &amp; 0 &amp; 7 &amp; 5 &amp; 10\\
3 &amp; 7 &amp; 0 &amp; 9 &amp; 2\\
6 &amp; 5 &amp; 9 &amp; 0 &amp; 8\\
11 &amp; 10 &amp; 2 &amp; 8 &amp; 0
\end{bmatrix}.</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>11</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>11</span></span></span><span><span></span><span><span>10</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152250309.png" alt="" /></p><section><h6>第 1 阶段<a href="#第-1-阶段-1"><span>#</span></a></h6><p>五个簇，每个簇只含一个观测。</p></section><section><h6>第 2 阶段<a href="#第-2-阶段-1"><span>#</span></a></h6><p>对象 3 与 5 在距离</p><span><span><span>2.2.</span><span><span><span></span><span>2.</span></span></span></span></span><p>处合并成簇 <span><span>(35)(35)</span><span><span><span></span><span>(</span><span>35</span><span>)</span></span></span></span>。</p><p>于是</p><span><span><span>d(35)1=max⁡{d31,d51}=max⁡{3,11}=11,d_{(35)1}
=
\max\{d_{31},d_{51}\}
=
\max\{3,11\}
=
11,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>51</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span>3</span><span>,</span><span></span><span>11</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>11</span><span>,</span></span></span></span></span><span><span><span>d(35)2=max⁡{d32,d52}=10,d_{(35)2}
=
\max\{d_{32},d_{52}\}
=
10,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>52</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span>,</span></span></span></span></span><p>且</p><span><span><span>d(35)4=max⁡{d34,d54}=9.d_{(35)4}
=
\max\{d_{34},d_{54}\}
=
9.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>34</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>54</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>9.</span></span></span></span></span><p>新的距离矩阵为</p><span><span><span>D(35)=[09611905106509111090],D_{(35)}
=
\begin{bmatrix}
0 &amp; 9 &amp; 6 &amp; 11\\
9 &amp; 0 &amp; 5 &amp; 10\\
6 &amp; 5 &amp; 0 &amp; 9\\
11 &amp; 10 &amp; 9 &amp; 0
\end{bmatrix},</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>11</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>11</span></span></span><span><span></span><span><span>10</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>对应簇 <span><span>1,2,4,(35)1,2,4,(35)</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>(</span><span>35</span><span>)</span></span></span></span>：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904154033969.png" alt="" /></p></section><section><h6>第 3 阶段<a href="#第-3-阶段-1"><span>#</span></a></h6><p>对象 2 与 4 合并成簇 <span><span>(24)(24)</span><span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span>，距离为</p><span><span><span>5.5.</span><span><span><span></span><span>5.</span></span></span></span></span><p>于是</p><span><span><span>d(24)(35)=max⁡{d2(35),d4(35)}=max⁡{10,9}=10,d_{(24)(35)}
=
\max\{d_{2(35)},d_{4(35)}\}
=
\max\{10,9\}
=
10,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>2</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>4</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span>10</span><span>,</span><span></span><span>9</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span>,</span></span></span></span></span><p>且</p><span><span><span>d(24)1=max⁡{d21,d41}=9.d_{(24)1}
=
\max\{d_{21},d_{41}\}
=
9.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>41</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>9.</span></span></span></span></span><p>新的距离矩阵为</p><span><span><span>D=[0101110091190],D
=
\begin{bmatrix}
0 &amp; 10 &amp; 11\\
10 &amp; 0 &amp; 9\\
11 &amp; 9 &amp; 0
\end{bmatrix},</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>10</span></span></span><span><span></span><span><span>11</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>10</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>11</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>对应簇 <span><span>(35),(24),1(35),(24),1</span><span><span><span></span><span>(</span><span>35</span><span>)</span><span>,</span><span></span><span>(</span><span>24</span><span>)</span><span>,</span><span></span><span>1</span></span></span></span>：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904154122144.png" alt="" /></p></section><section><h6>第 4 阶段<a href="#第-4-阶段-1"><span>#</span></a></h6><p>将簇 <span><span>(24)(24)</span><span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span> 与对象 1 合并成簇 <span><span>(124)(124)</span><span><span><span></span><span>(</span><span>124</span><span>)</span></span></span></span>，距离为</p><span><span><span>9.9.</span><span><span><span></span><span>9.</span></span></span></span></span><p>于是</p><span><span><span>d(124)(35)=max⁡{d1(35),d(24)(35)}=max⁡{11,10}=11.d_{(124)(35)}
=
\max\{d_{1(35)},d_{(24)(35)}\}
=
\max\{11,10\}
=
11.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>124</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>1</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>{</span><span>11</span><span>,</span><span></span><span>10</span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span>11.</span></span></span></span></span><p>因此，</p><span><span><span>D=[011110].D
=
\begin{bmatrix}
0 &amp; 11\\
11 &amp; 0
\end{bmatrix}.</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>11</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>11</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>也即：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904154159759.png" alt="" /></p></section><section><h6>第 5 阶段<a href="#第-5-阶段-1"><span>#</span></a></h6><p>簇 <span><span>(35)(35)</span><span><span><span></span><span>(</span><span>35</span><span>)</span></span></span></span> 与 <span><span>(124)(124)</span><span><span><span></span><span>(</span><span>124</span><span>)</span></span></span></span> 合并成簇</p><span><span><span>(12345)(12345)</span><span><span><span></span><span>(</span><span>12345</span><span>)</span></span></span></span></span><p>合并时的距离为</p><span><span><span>11.11.</span><span><span><span></span><span>11.</span></span></span></span></span><div><div><div></div><div>Important</div></div><div><p>单连结与完全连结的差别体现在对象 1 的归属上。</p></div></div><p>上述过程的谱系图表示：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904154303357.png" alt="" /></p><hr /></section></section></section><section><h4>4.1.4 平均连结（Average linkage）<a href="#414-平均连结average-linkage"><span>#</span></a></h4><p>簇间距离由一个簇中所有个体到另一个簇中所有个体的<strong>平均距离</strong>定义。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904155739537.png" alt="" /></p><p>对于簇 <span><span>(UV)(UV)</span><span><span><span></span><span>(</span><span>U</span><span>V</span><span>)</span></span></span></span> 和另一个簇 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span>，</p><span><span><span>d(UV)W=NUdUW+NVdVWNU+NV.d_{(UV)W}
=
\frac{
N_Ud_{UW}+N_Vd_{VW}
}{
N_U+N_V
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>U</span><span>V</span><span>)</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>这里，<span><span>NUN_U</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>NVN_V</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 分别是簇 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 中对象的个数。</p><hr /></section><section><h4>4.1.5 重心连结（Centroid linkage）<a href="#415-重心连结centroid-linkage"><span>#</span></a></h4><p>簇间距离由各簇的<strong>重心</strong>（质心）之间的距离定义。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904155811786.png" alt="" /></p><p>设 <span><span>dijd_{ij}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 与 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 之间的平方欧氏距离。</p><p>用下式更新距离矩阵：</p><span><span><span>d(UV)W=NUdUW+NVdVWNU+NV−NUNVdUV(NU+NV)2.d_{(UV)W}
=
\frac{
N_Ud_{UW}+N_Vd_{VW}
}{
N_U+N_V
}
-
\frac{
N_UN_Vd_{UV}
}{
(N_U+N_V)^2
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>U</span><span>V</span><span>)</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>V</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>%%该更新公式中两项的含义？%%</p></section></section><section><h3>4.2 连结的意义<a href="#42-连结的意义"><span>#</span></a></h3><div><div><div></div><div>连结法的意义在哪？</div></div><div><p>既然最后所有的簇都要被聚类为一个簇，那我们中间这些步骤的意义在哪？不直接一开始就说它们是一个簇就完了吗？</p></div></div><p>连结法的意义并不在于最终的“一个大簇”，而恰恰在于中间的步骤。就像我们在开头提到的，层次聚类最大的优势是：<strong>你不需要事先决定分几类</strong>。做完树状图后，想分几类就切几刀：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>高度 ──────────────────────────</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>│                    ← 在这里切一刀 → 得到 1 个簇（没意义，不用）</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>│               ╭──── 在这里切 → 得到 2 个簇（通常有意义！）</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>│          ╭────╯</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>│     ╭────╯</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>│  ╭──╯</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>│  3  5  1  2  4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们不妨再回顾一下单连结例子中 5 个对象的合并记录：</p>

<table><thead><tr><th>次序</th><th>合并</th><th>距离</th></tr></thead><tbody><tr><td>1</td><td><span><span>{3},{5}\{3\},\{5\}</span><span><span><span></span><span>{</span><span>3</span><span>}</span><span>,</span><span></span><span>{</span><span>5</span><span>}</span></span></span></span></td><td>2</td></tr><tr><td>2</td><td><span><span>{35},{1}\{35\},\{1\}</span><span><span><span></span><span>{</span><span>35</span><span>}</span><span>,</span><span></span><span>{</span><span>1</span><span>}</span></span></span></span></td><td>3</td></tr><tr><td>3</td><td><span><span>{4},{2}\{4\},\{2\}</span><span><span><span></span><span>{</span><span>4</span><span>}</span><span>,</span><span></span><span>{</span><span>2</span><span>}</span></span></span></span></td><td>5</td></tr><tr><td>4</td><td><span><span>{135},{24}\{135\},\{24\}</span><span><span><span></span><span>{</span><span>135</span><span>}</span><span>,</span><span></span><span>{</span><span>24</span><span>}</span></span></span></span></td><td>6</td></tr></tbody></table><p>注意最后一步合并距离是 <strong>6</strong>，而前几步只有 2、3、5——这个<strong>距离跳变</strong>就是信息！它说明：</p><ul>
<li>对象 3、5、1 彼此很接近（距离 2~3 就合并了）；</li>
<li>对象 2、4 也彼此接近（距离 5 合并）；</li>
<li>但<strong>这两组之间</strong>相距 6，明显比组内距离大。</li>
</ul><p>所以如果我们在距离 5 和 6 <strong>之间</strong>切一刀，就得到两个有意义的簇：<span><span>{135}\{135\}</span><span><span><span></span><span>{</span><span>135</span><span>}</span></span></span></span> 和 <span><span>{24}\{24\}</span><span><span><span></span><span>{</span><span>24</span><span>}</span></span></span></span>。这比”一个大簇”有用得多，而且<strong>不用事先指定</strong> ——是数据自己告诉我们”大概分两组比较自然”。</p><p>从上面的表格也能看出，对于层次聚类：<strong>合并得越早（距离越小）的对象越相似，合并得越晚（距离越大）越疏远</strong>。</p></section><section><h3>4.3 Ward 法（Ward’ s method）<a href="#43-ward-法ward-s-method"><span>#</span></a></h3><p>Ward 法将簇内平方和定义为</p><span><span><span>ESS=∑K∑i∈K∑j=1p(xijK−xˉ.jK)2.ESS
=
\sum_K
\sum_{i\in K}
\sum_{j=1}^{p}
\left(
x_{ijK}
-
\bar{x}_{.jK}
\right)^2.</span><span><span><span></span><span>E</span><span>S</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>K</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>∈</span><span>K</span></span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>p</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span><span>K</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>.</span><span>j</span><span>K</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>这里，</p><ul>
<li><span><span>xijKx_{ijK}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span><span>K</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是簇 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 中第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个观测的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个变量的取值。</li>
<li><span><span>xˉ.jK\bar{x}_{.jK}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>.</span><span>j</span><span>K</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是簇 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 中第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个变量的样本均值。</li>
</ul><p>在每一阶段，都要在由上一阶段的两个簇合并所得的所有划分中，使簇内 <span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> 最小化。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904155937669.png" alt="" /></p><p>设 <span><span>dijd_{ij}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 与 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 之间的平方欧氏距离。</p><p>用下式更新距离矩阵：</p><span><span><span>d(UV)W=(NW+NU)dUW+(NW+NV)dVW−NWdUVNU+NV+NW.d_{(UV)W}
=
\frac{
(N_W+N_U)d_{UW}
+
(N_W+N_V)d_{VW}
-
N_Wd_{UV}
}{
N_U+N_V+N_W
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>U</span><span>V</span><span>)</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>W</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>W</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>W</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>V</span><span>W</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>W</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>U</span><span>V</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>其中：</p><ul>
<li><span><span>NWN_W</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>W</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ：簇 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 里包含多少个对象，<span><span>NU,NVN_U, N_V</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 以此类推。</li>
</ul><section><h4>4.3.1 Ward 法例子<a href="#431-ward-法例子"><span>#</span></a></h4><p>五个对象两两之间的平方欧氏距离为</p><span><span><span>D=[09361190751037092659081110280].D
=
\begin{bmatrix}
0 &amp; 9 &amp; 3 &amp; 6 &amp; 11\\
9 &amp; 0 &amp; 7 &amp; 5 &amp; 10\\
3 &amp; 7 &amp; 0 &amp; 9 &amp; 2\\
6 &amp; 5 &amp; 9 &amp; 0 &amp; 8\\
11 &amp; 10 &amp; 2 &amp; 8 &amp; 0
\end{bmatrix}.</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>11</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>11</span></span></span><span><span></span><span><span>10</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904152250309.png" alt="" /></p><section><h5>第 1 阶段<a href="#第-1-阶段-2"><span>#</span></a></h5><p>五个簇，每个簇只含一个观测。</p></section><section><h5>第 2 阶段<a href="#第-2-阶段-2"><span>#</span></a></h5><p>对象 3 与 5 在距离</p><span><span><span>2.2.</span><span><span><span></span><span>2.</span></span></span></span></span><p>处合并成簇 <span><span>(35)(35)</span><span><span><span></span><span>(</span><span>35</span><span>)</span></span></span></span>。</p><p>对于对象 1，</p><span><span><span>d(35)1=(N1+N3)d31+(N1+N5)d51−N1d35N3+N5+N1.d_{(35)1}
=
\frac{
(N_1+N_3)d_{31}
+
(N_1+N_5)d_{51}
-
N_1d_{35}
}{
N_3+N_5+N_1
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>51</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>由于所有簇的大小均为 1，</p><span><span><span>d(35)1=(1+1)3+(1+1)11−1×21+1+1=8.6667.d_{(35)1}
=
\frac{
(1+1)3+(1+1)11-1\times2
}{
1+1+1
}
=
8.6667.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>1</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>3</span><span></span><span>+</span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>11</span><span></span><span>−</span><span></span><span>1</span><span></span><span>×</span><span></span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>8.6667.</span></span></span></span></span><p>类似地，</p><span><span><span>d(35)2=(1+1)7+(1+1)10−1×21+1+1=10.6667,d_{(35)2}
=
\frac{
(1+1)7+(1+1)10-1\times2
}{
1+1+1
}
=
10.6667,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>1</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>7</span><span></span><span>+</span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>10</span><span></span><span>−</span><span></span><span>1</span><span></span><span>×</span><span></span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10.6667</span><span>,</span></span></span></span></span><p>且</p><span><span><span>d(35)4=(1+1)9+(1+1)8−1×21+1+1=10.6667.d_{(35)4}
=
\frac{
(1+1)9+(1+1)8-1\times2
}{
1+1+1
}
=
10.6667.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>35</span><span>)</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>1</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>9</span><span></span><span>+</span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>8</span><span></span><span>−</span><span></span><span>1</span><span></span><span>×</span><span></span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10.6667.</span></span></span></span></span><p>新的距离矩阵为</p><span><span><span>D=[08.610.610.68.609610.690510.6650],D
=
\begin{bmatrix}
0 &amp; 8.6 &amp; 10.6 &amp; 10.6\\
8.6 &amp; 0 &amp; 9 &amp; 6\\
10.6 &amp; 9 &amp; 0 &amp; 5\\
10.6 &amp; 6 &amp; 5 &amp; 0
\end{bmatrix},</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>8.6</span></span></span><span><span></span><span><span>10.6</span></span></span><span><span></span><span><span>10.6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>8.6</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>10.6</span></span></span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>10.6</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>对应簇 <span><span>(35),1,2,4(35),1,2,4</span><span><span><span></span><span>(</span><span>35</span><span>)</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span></span></span></span>：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904161813967.png" alt="" /></p></section><section><h5>第 3 阶段<a href="#第-3-阶段-2"><span>#</span></a></h5><p>对象 2 与 4 合并成簇 <span><span>(24)(24)</span><span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span>，距离为</p><span><span><span>5.5.</span><span><span><span></span><span>5.</span></span></span></span></span><p>于是</p><span><span><span>d(24)(35)=(N35+N2)d2(35)+(N35+N4)d4(35)−N35d24N2+N4+N35.d_{(24)(35)}
=
\frac{
(N_{35}+N_2)d_{2(35)}
+
(N_{35}+N_4)d_{4(35)}
-
N_{35}d_{24}
}{
N_2+N_4+N_{35}
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>2</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>4</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>24</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>代入数值，</p><span><span><span>d(24)(35)=(2+1)(10.6667)+(2+1)(10.6667)−2×51+1+2=13.5.d_{(24)(35)}
=
\frac{
(2+1)(10.6667)
+
(2+1)(10.6667)
-
2\times5
}{
1+1+2
}
=
13.5.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>1</span><span></span><span>+</span><span></span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>2</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>(</span><span>10.6667</span><span>)</span><span></span><span>+</span><span></span><span>(</span><span>2</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>(</span><span>10.6667</span><span>)</span><span></span><span>−</span><span></span><span>2</span><span></span><span>×</span><span></span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>13.5.</span></span></span></span></span><p>另外，</p><span><span><span>d(24)1=8.333.d_{(24)1}=8.333.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>8.333.</span></span></span></span></span><p>新的距离矩阵为</p><span><span><span>D=[013.58.666713.508.3338.66678.3330],D
=
\begin{bmatrix}
0 &amp; 13.5 &amp; 8.6667\\
13.5 &amp; 0 &amp; 8.333\\
8.6667 &amp; 8.333 &amp; 0
\end{bmatrix},</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>13.5</span></span></span><span><span></span><span><span>8.6667</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>13.5</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>8.333</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>8.6667</span></span></span><span><span></span><span><span>8.333</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>对应簇 <span><span>(35),(24),1(35),(24),1</span><span><span><span></span><span>(</span><span>35</span><span>)</span><span>,</span><span></span><span>(</span><span>24</span><span>)</span><span>,</span><span></span><span>1</span></span></span></span>：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904162128974.png" alt="" /></p></section><section><h5>第 4 阶段<a href="#第-4-阶段-2"><span>#</span></a></h5><p>将簇 <span><span>(24)(24)</span><span><span><span></span><span>(</span><span>24</span><span>)</span></span></span></span> 与对象 1 合并成簇 <span><span>(124)(124)</span><span><span><span></span><span>(</span><span>124</span><span>)</span></span></span></span>，距离为</p><span><span><span>8.333.8.333.</span><span><span><span></span><span>8.333.</span></span></span></span></span><p>于是</p><span><span><span>d(124)(35)=(N35+N1)d1(35)+(N35+N24)d(24)(35)−N35d1(24)N1+N24+N35.d_{(124)(35)}
=
\frac{
(N_{35}+N_1)d_{1(35)}
+
(N_{35}+N_{24})d_{(24)(35)}
-
N_{35}d_{1(24)}
}{
N_1+N_{24}+N_{35}
}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>124</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>24</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>1</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>24</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>24</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>1</span><span>(</span><span>24</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>代入数值，</p><span><span><span>d(124)(35)=(2+1)(8.667)+(2+2)(13.5)−2(8.333)1+2+2=12.6667.d_{(124)(35)}
=
\frac{
(2+1)(8.667)
+
(2+2)(13.5)
-
2(8.333)
}{
1+2+2
}
=
12.6667.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>(</span><span>124</span><span>)</span><span>(</span><span>35</span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>2</span><span></span><span>+</span><span></span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>2</span><span></span><span>+</span><span></span><span>1</span><span>)</span><span>(</span><span>8.667</span><span>)</span><span></span><span>+</span><span></span><span>(</span><span>2</span><span></span><span>+</span><span></span><span>2</span><span>)</span><span>(</span><span>13.5</span><span>)</span><span></span><span>−</span><span></span><span>2</span><span>(</span><span>8.333</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12.6667.</span></span></span></span></span><p>因此，</p><span><span><span>D=[012.666712.66670].D
=
\begin{bmatrix}
0 &amp; 12.6667\\
12.6667 &amp; 0
\end{bmatrix}.</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>12.6667</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>12.6667</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904162211725.png" alt="" /></p></section><section><h5>第 5 阶段<a href="#第-5-阶段-2"><span>#</span></a></h5><p>簇 <span><span>(35)(35)</span><span><span><span></span><span>(</span><span>35</span><span>)</span></span></span></span> 与 <span><span>(124)(124)</span><span><span><span></span><span>(</span><span>124</span><span>)</span></span></span></span> 合并成</p><span><span><span>(12345)(12345)</span><span><span><span></span><span>(</span><span>12345</span><span>)</span></span></span></span></span><p>合并时的距离为</p><span><span><span>12.6667.12.6667.</span><span><span><span></span><span>12.6667.</span></span></span></span></span><p>上述步骤的谱系图表示：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260904162250483.png" alt="" /></p></section></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/stat-02/</id>
      <title type="text">相似性度量</title>
      <published>2026-09-03T00:00:00.000Z</published>
      <updated>2026-09-10T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/stat-02/"/>
      <summary type="text">在聚类分析中，要产生一个简单的分组结构，我们就需要一种衡量接近程度或相似性的度量。</summary>
      <content type="html"><![CDATA[<p>在聚类分析中，要产生一个简单的分组结构，我们就需要一种衡量接近程度或相似性的度量。</p>
<div><div><div></div><div>Info</div></div><div><p>选择相似性度量时涉及很大的主观性。</p></div></div>
<div><div><div></div><div>Info</div></div><div><p>本篇节选自我的课程笔记，因此标题序号沿用了原笔记中的排列（懒得改）。</p></div></div>
<section><h3>3.1 连续型测量<a href="#31-连续型测量"><span>#</span></a></h3><p>对象可以用 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 维测量值表示：</p><span><span><span>x=[x1,…,xp],y=[y1,…,yp].\mathbf{x}=[x_1,\ldots,x_p],
\qquad
\mathbf{y}=[y_1,\ldots,y_p].</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span>,</span><span></span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span>.</span></span></span></span></span><section><h4>欧氏距离（Euclidean distance）<a href="#欧氏距离euclidean-distance"><span>#</span></a></h4><span><span><span>d(x,y)=(x1−y1)2+⋯+(xp−yp)2.d(\mathbf{x},\mathbf{y})
=
\sqrt{
(x_1-y_1)^2+\cdots+(x_p-y_p)^2
}.</span><span><span><span></span><span>d</span><span>(</span><span>x</span><span>,</span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>⋯</span><span></span><span>+</span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>.</span></span></span></span></span><p>欧氏距离就是两点间的直线距离，即勾股定理在高维的推广，是我们日常空间直觉中最自然的“距离”。</p></section><section><h4>平方欧氏距离<a href="#平方欧氏距离"><span>#</span></a></h4><span><span><span>d(x,y)=(x1−y1)2+⋯+(xp−yp)2.d(\mathbf{x},\mathbf{y})
=
(x_1-y_1)^2+\cdots+(x_p-y_p)^2.</span><span><span><span></span><span>d</span><span>(</span><span>x</span><span>,</span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260902144333140.png" alt="" /></p><p>欧氏距离<strong>去掉根号</strong>。它不再是严格意义上的“距离”，但保留了大小排序信息：<span><span>d2d^2</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 越大，两点越远。</p><p>去掉根号带来两个好处：</p><ul>
<li><strong>求导更方便</strong>：聚类、最优化中经常要对距离求导，带根号的式子导数繁琐，平方形式则很干净；</li>
<li><strong>可分解性</strong>：平方欧氏距离可以拆成各维度贡献之和，Ward 法的簇内平方和 <span><span>ESSESS</span><span><span><span></span><span>E</span><span>S</span><span>S</span></span></span></span> 正是基于这种分解。</li>
</ul></section><section><h4>统计距离<a href="#统计距离"><span>#</span></a></h4><p>欧氏距离有两个致命弱点，用一个例子说明：假设有两个变量：<strong>收入（元）</strong> 和 <strong>年龄（岁）</strong>。</p><ul>
<li><strong>弱点一：量纲敏感。</strong> 收入差 5000 元、年龄差 5 岁，算欧氏距离时收入完全主导结果——年龄几乎被“淹没”了（我们在 3.4 节会介绍标准化）。</li>
<li><strong>弱点二：忽略相关性。</strong> 设想数据点分布成一个倾斜的椭圆（收入与年龄正相关）。椭圆<strong>长轴方向</strong>上的波动是数据的自然变异，应该“算近一点”；<strong>短轴方向</strong>上偏离很多才是真正的异常，应该“算远一点”。而欧氏距离对各方向一视同仁，做不到这一点。</li>
</ul><p>统计距离表达：</p><span><span><span>d(x,y)=(x−y)TA(x−y).d(\mathbf{x},\mathbf{y})
=
\sqrt{
(\mathbf{x}-\mathbf{y})^T
A
(\mathbf{x}-\mathbf{y})
}.</span><span><span><span></span><span>d</span><span>(</span><span>x</span><span>,</span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>y</span><span>)</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>.</span></span></span></span></span><p>对于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> ，一个常见的选择是</p><span><span><span>A=S−1,A=S^{-1},</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>,</span></span></span></span></span><p>其中 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 为样本协方差矩阵。</p><p>当我们取 <span><span>A=S−1A=S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 时，它也被称为<strong>马氏距离（Mahalanobis distance）</strong>。它不仅看两点相隔多远，还考虑数据的分布形状。这样，它就解决了欧氏距离的两个问题：</p><ul>
<li>协方差矩阵的对角元素（各变量的方差）<strong>自动校正量纲</strong>——效果类似先标准化再算距离（标准化后算欧氏距离等价于取 <span><span>A=diag(S)−1A=\text{diag}(S)^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>S</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的统计距离，参见 3.4 节）；</li>
<li>非对角元素（协方差）<strong>考虑变量间的相关性</strong>，把椭圆“转正、拉圆”之后再量距离，各方向权重符合数据的真实分布。</li>
</ul></section><section><h4>闵可夫斯基度量（Minkowski metric）<a href="#闵可夫斯基度量minkowski-metric"><span>#</span></a></h4><p>闵可夫斯基度量则是一个统一的框架，前面的欧氏距离只是它的一个特例：</p><span><span><span>d(x,y)=[∑i=1p∣xi−yi∣m]1/m.d(\mathbf{x},\mathbf{y})
=
\left[
\sum_{i=1}^{p}
|x_i-y_i|^m
\right]^{1/m}.</span><span><span><span></span><span>d</span><span>(</span><span>x</span><span>,</span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>p</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>∣</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>∣</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span><span>1/</span><span>m</span></span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>它通过参数 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 控制距离的“性格”。<span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 取不同值时，就得到一系列熟悉的距离：</p>

<table><thead><tr><th><span><span>mm</span><span><span><span></span><span>m</span></span></span></span></th><th>距离名称</th><th>特点</th></tr></thead><tbody><tr><td><span><span>m=1m=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></td><td><strong>曼哈顿距离</strong>（Manhattan / 城市街区距离）</td><td>各维差值绝对值直接相加</td></tr><tr><td><span><span>m=2m=2</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span></td><td><strong>欧氏距离</strong></td><td>我们熟悉的直线距离</td></tr><tr><td><span><span>m→∞m\to\infty</span><span><span><span></span><span>m</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span></td><td><strong>切比雪夫距离</strong>（Chebyshev）</td><td>只看差距最大的那一维</td></tr></tbody></table><p><span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 的直觉在于，<strong><span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 越大，距离越被“大差距”的维度主导；<span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 越小，各维度越“平等”</strong>。</p><section><h5>这句话是什么意思？<a href="#这句话是什么意思"><span>#</span></a></h5><p>举个具体例子。假设两个点在 3 个维度上的差值是：</p><span><span><span>∣x1−y1∣=10,∣x2−y2∣=3,∣x3−y3∣=1|x_1-y_1|=10,\quad |x_2-y_2|=3,\quad |x_3-y_3|=1</span><span><span><span></span><span>∣</span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span>,</span><span></span><span></span><span>∣</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>,</span><span></span><span></span><span>∣</span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p>看不同 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 下各维度的“贡献”（乘方后的值）：</p>

<table><thead><tr><th><span><span>mm</span><span><span><span></span><span>m</span></span></span></span></th><th>维度1（差10）</th><th>维度2（差3）</th><th>维度3（差1）</th><th>距离值</th></tr></thead><tbody><tr><td><span><span>m=1m=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></td><td>10</td><td>3</td><td>1</td><td><span><span>≈14\approx 14</span><span><span><span></span><span>≈</span><span></span></span><span><span></span><span>14</span></span></span></span></td></tr><tr><td><span><span>m=2m=2</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span></td><td>100</td><td>9</td><td>1</td><td><span><span>≈10.5\approx 10.5</span><span><span><span></span><span>≈</span><span></span></span><span><span></span><span>10.5</span></span></span></span></td></tr><tr><td><span><span>m=3m=3</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span></td><td>1000</td><td>27</td><td>1</td><td><span><span>≈10.03\approx 10.03</span><span><span><span></span><span>≈</span><span></span></span><span><span></span><span>10.03</span></span></span></span></td></tr><tr><td><span><span>m→∞m\to\infty</span><span><span><span></span><span>m</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span></td><td>主导一切</td><td>忽略不计</td><td>忽略不计</td><td><span><span>=10=10</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span></td></tr></tbody></table><p>可以清楚看到：</p><ul>
<li><strong><span><span>m=1m=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（曼哈顿距离）</strong>：就是简单相加，每个维度都“有一票”，哪怕差 1 的小维度也算数。各维度<strong>平等</strong>。</li>
<li><strong><span><span>m=2m=2</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>（欧氏距离）</strong>：大差距被平方放大了（10→100），小差距被相对压制（3→9，1→1）。大维度开始“抢戏”。</li>
<li><strong><span><span>m→∞m\to\infty</span><span><span><span></span><span>m</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span>（切比雪夫距离）</strong>：乘方使得最大的那个差值<strong>碾压</strong>所有其他维度，距离完全由差距最大的那一维决定，其他维度形同虚设。</li>
</ul><p>直觉来说，<span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 相当于一个“偏心旋钮”：</p><ul>
<li><strong><span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 调大</strong> → 数学上 <span><span>max⁡\max</span><span><span><span></span><span>max</span></span></span></span> 占上风 → 距离反映“最差的那一维”（一票否决制）</li>
<li><strong><span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 调小</strong> → 越接近简单的加总 → 距离反映“所有维度的总和”（人人有份）</li>
</ul><p>所以：<strong><span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 决定了多维差距汇总时是“平均主义”还是“强者通吃”</strong>。这也解释了为什么表格里 <span><span>m=1m=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时小维度影响大、<span><span>m→∞m\to\infty</span><span><span><span></span><span>m</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span> 时只剩最大维度起作用。</p><hr /></section><section><h5>闵可夫斯基度量与统计距离<a href="#闵可夫斯基度量与统计距离"><span>#</span></a></h5>

<table><thead><tr><th></th><th>闵可夫斯基度量</th><th>统计距离</th></tr></thead><tbody><tr><td>公式</td><td><span><span>[∑∥xi−yi∥m]1/m\left[\sum\|x_i-y_i\|^m\right]^{1/m}</span><span><span><span></span><span><span><span>[</span><span>∑</span><span></span><span>∥</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>∥</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span><span>]</span></span><span><span><span><span><span><span></span><span><span><span>1/</span><span>m</span></span></span></span></span></span></span></span></span></span></span></span></td><td><span><span>(x−y)TA (x−y)\sqrt{(\mathbf{x}-\mathbf{y})^T A\,(\mathbf{x}-\mathbf{y})}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>y</span><span>)</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></td></tr><tr><td>处理量纲差异</td><td>❌ 不处理（对量纲敏感）</td><td>✅ 马氏距离 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的对角元素自动校正</td></tr><tr><td>处理变量相关性</td><td>❌ 各维度完全独立</td><td>✅ <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的非对角元素处理相关性</td></tr><tr><td>灵活性来源</td><td>参数 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>（改变汇总方式）</td><td>矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>（改变空间形状）</td></tr><tr><td>典型代表</td><td><span><span>m=1m=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 曼哈顿、<span><span>m=2m=2</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 欧氏、<span><span>m→∞m\to\infty</span><span><span><span></span><span>m</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span> 切比雪夫</td><td><span><span>A=S−1A=S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 马氏距离</td></tr></tbody></table><p>闵可夫斯基度量把 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 维空间当成<strong>各向同性</strong>的——每个维度权重相同、维度之间互不相干。</p><p>而统计距离相当于<strong>先对空间做“变形”</strong>（拉伸、旋转），再量直线距离。例如我们在 3.1 节里提到的：马氏距离是把椭圆“转正、拉圆”之后再量距离。</p><p>在实践中：</p><ul>
<li>如果变量之间可以视为独立，经过标准化后直接比较各维差异，就可以用闵可夫斯基距离；</li>
<li>如果变量尺度不同、存在明显相关性，而且能够可靠估计协方差结构，用统计距离，典型是马氏距离。</li>
</ul></section></section></section>
<section><h3>3.2 二元测量<a href="#32-二元测量"><span>#</span></a></h3><p>对象对可以依据某些特征的<strong>有无</strong>来进行比较。</p><ul>
<li>二元变量表示某特征的存在或缺失。</li>
<li>设 <span><span>xijx_{ij}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个二元变量在第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个对象上的取值，取 <span><span>11</span><span><span><span></span><span>1</span></span></span></span> 或 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>，其中</li>
</ul><span><span><span>j=1,…,p.j=1,\ldots,p.</span><span><span><span></span><span>j</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>p</span><span>.</span></span></span></span></span><p>例如：</p>

<table><thead><tr><th>对象</th><th>变量 1</th><th>变量 2</th><th>变量 3</th><th>变量 4</th><th>变量 5</th></tr></thead><tbody><tr><td><span><span>ii</span><span><span><span></span><span>i</span></span></span></span></td><td>1</td><td>0</td><td>0</td><td>1</td><td>1</td></tr><tr><td><span><span>kk</span><span><span><span></span><span>k</span></span></span></span></td><td>1</td><td>1</td><td>0</td><td>1</td><td>0</td></tr></tbody></table><section><h4>二元变量的平方欧氏距离<a href="#二元变量的平方欧氏距离"><span>#</span></a></h4><span><span><span>d(x,y)=∑i=1p(xi−yi)2.d(\mathbf{x},\mathbf{y})
=
\sum_{i=1}^{p}(x_i-y_i)^2.</span><span><span><span></span><span>d</span><span>(</span><span>x</span><span>,</span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>p</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>它是：</p><ul>
<li>不匹配的维度数。</li>
<li>当两个对象不相似时取值较大。</li>
<li>基于 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>-<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 匹配与 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>-<span><span>00</span><span><span><span></span><span>0</span></span></span></span> 匹配同等权重。</li>
</ul><p>然而，在某些情况下，<span><span>11</span><span><span><span></span><span>1</span></span></span></span>-<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 匹配比 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>-<span><span>00</span><span><span><span></span><span>0</span></span></span></span> 匹配更能表明相似性。</p><div><div><div></div><div>1-1 匹配与 0-0 匹配的含义</div></div><div><p>二元数据并不是总该使用欧氏距离，选择取决于 0 和 1 的含义：</p><ul>
<li><strong>对称二元变量</strong>：0 和 1 地位相同，例如性别编码或两个互斥状态。可以使用 Hamming、简单匹配距离或平方欧氏距离。</li>
<li><strong>非对称二元变量</strong>：1 表示稀有且重要的“出现”，0 只是“未出现”，例如疾病、购买行为、关键词。共同为 0 通常不应算作重要相似度，此时更适合 Jaccard 距离：</li>
</ul><span><span><span>dj=b+ca+b+cd_{j}=\frac{b+c}{a+b+c}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>j</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>b</span><span></span><span>+</span><span></span><span>c</span></span></span><span><span></span><span></span></span><span><span></span><span><span>b</span><span></span><span>+</span><span></span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span></div></div><p>对于上面的例子，</p><span><span><span>d(xi,xk)=(1−1)2+(0−1)2+(0−0)2+(1−1)2+(1−0)2=2.d(\mathbf{x}_i,\mathbf{x}_k)
=
(1-1)^2
+
(0-1)^2
+
(0-0)^2
+
(1-1)^2
+
(1-0)^2
=
2.</span><span><span><span></span><span>d</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>0</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>0</span><span></span><span>−</span><span></span></span><span><span></span><span>0</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>0</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.</span></span></span></span></span><p>也就是说，<span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 和 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 总共有 2 个不匹配的维度。</p></section></section>
<section><h3>3.3 其他相似性系数<a href="#33-其他相似性系数"><span>#</span></a></h3><p>设匹配与不匹配的频数汇总如下：</p>

<table><thead><tr><th></th><th>对象 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span>：1</th><th>对象 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span>：0</th><th>合计</th></tr></thead><tbody><tr><td><strong>对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>：1</strong></td><td><span><span>aa</span><span><span><span></span><span>a</span></span></span></span></td><td><span><span>bb</span><span><span><span></span><span>b</span></span></span></span></td><td><span><span>a+ba+b</span><span><span><span></span><span>a</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span></span></span></span></td></tr><tr><td><strong>对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>：0</strong></td><td><span><span>cc</span><span><span><span></span><span>c</span></span></span></span></td><td><span><span>dd</span><span><span><span></span><span>d</span></span></span></span></td><td><span><span>c+dc+d</span><span><span><span></span><span>c</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span></span></span></span></td></tr><tr><td><strong>合计</strong></td><td><span><span>a+ca+c</span><span><span><span></span><span>a</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span></span></span></span></td><td><span><span>b+db+d</span><span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span></span></span></span></td><td><span><span>T=a+b+c+dT=a+b+c+d</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span></span></span></span></td></tr></tbody></table><p>基于匹配比例的度量包括：</p>

<table><thead><tr><th>公式</th><th>依据</th></tr></thead><tbody><tr><td><span><span>a+dT\dfrac{a+d}{T}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>T</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td><span><span>11</span><span><span><span></span><span>1</span></span></span></span>-<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 匹配与 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>-<span><span>00</span><span><span><span></span><span>0</span></span></span></span> 匹配同等权重</td></tr><tr><td><span><span>aT\dfrac{a}{T}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>T</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td>分子中不含 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>-<span><span>00</span><span><span><span></span><span>0</span></span></span></span> 匹配</td></tr><tr><td><span><span>2(a+d)2(a+d)+b+c\dfrac{2(a+d)}{2(a+d)+b+c}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span>(</span><span>a</span><span></span><span>+</span><span></span><span>d</span><span>)</span><span></span><span>+</span><span></span><span>b</span><span></span><span>+</span><span></span><span>c</span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span>(</span><span>a</span><span></span><span>+</span><span></span><span>d</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td><span><span>11</span><span><span><span></span><span>1</span></span></span></span>-<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 匹配与 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>-<span><span>00</span><span><span><span></span><span>0</span></span></span></span> 匹配赋予双倍权重</td></tr><tr><td><span><span>a+da+d+2(b+c)\dfrac{a+d}{a+d+2(b+c)}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>d</span><span></span><span>+</span><span></span><span>2</span><span>(</span><span>b</span><span></span><span>+</span><span></span><span>c</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td>不匹配对赋予双倍权重</td></tr></tbody></table><section><h4>例子<a href="#例子"><span>#</span></a></h4>

<table><thead><tr><th></th><th>对象 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span>：1</th><th>对象 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span>：0</th><th>合计</th></tr></thead><tbody><tr><td><strong>对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>：1</strong></td><td>2</td><td>1</td><td>3</td></tr><tr><td><strong>对象 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>：0</strong></td><td>1</td><td>1</td><td>2</td></tr><tr><td><strong>合计</strong></td><td>3</td><td>2</td><td>5</td></tr></tbody></table><p>因此，</p><ul>
<li>普通匹配比例，将两种匹配视为同等重要：
<ul>
<li>适合 0 和 1 地位对称的变量。</li>
</ul>
</li>
</ul><span><span><span>d1(xi,xk)=a+dT=35,d_1(\mathbf{x}_i,\mathbf{x}_k)
=
\frac{a+d}{T}
=
\frac35,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>T</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span></span></span></span></span><ul>
<li>只奖励共同出现，只算 1-1，不看 0-0：
<ul>
<li>适合 1 代表重要事件的出现，而 0 只是未出现的情形。</li>
</ul>
</li>
</ul><span><span><span>d2(xi,xk)=aT=25,d_2(\mathbf{x}_i,\mathbf{x}_k)
=
\frac{a}{T}
=
\frac25,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>T</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span></span></span></span></span><ul>
<li>给匹配双倍权重，每个匹配贡献 2 分，每个不匹配贡献 1 分：
<ul>
<li>强调“相同”的证据。</li>
</ul>
</li>
</ul><span><span><span>d3(xi,xk)=2(a+d)2(a+d)+b+c=68,d_3(\mathbf{x}_i,\mathbf{x}_k)
=
\frac{2(a+d)}
{2(a+d)+b+c}
=
\frac68,</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span>(</span><span>a</span><span></span><span>+</span><span></span><span>d</span><span>)</span><span></span><span>+</span><span></span><span>b</span><span></span><span>+</span><span></span><span>c</span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span>(</span><span>a</span><span></span><span>+</span><span></span><span>d</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>8</span></span></span><span><span></span><span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span></span></span></span></span><ul>
<li>给不匹配双倍权重：</li>
</ul><span><span><span>d4(xi,xk)=a+da+d+2(b+c)=37.d_4(\mathbf{x}_i,\mathbf{x}_k)
=
\frac{a+d}
{a+d+2(b+c)}
=
\frac37.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>d</span><span></span><span>+</span><span></span><span>2</span><span>(</span><span>b</span><span></span><span>+</span><span></span><span>c</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>7</span></span></span><span><span></span><span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span></section></section>
<section><h3>3.4 标准化<a href="#34-标准化"><span>#</span></a></h3><p>相似性度量对量纲（尺度）敏感，离散程度大的变量对最终相似性值的影响更大。</p><p>对于观测 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 和变量 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span>，按变量进行的标准化为</p><span><span><span>zij=xij−xˉjsj.z_{ij}
=
\frac{x_{ij}-\bar{x}_j}{s_j}.</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span><p>标准化得分：</p><ul>
<li>消除量纲问题。</li>
<li>等价于使用统计距离并取</li>
</ul><span><span><span>A=diag⁡(S)−1,A=\operatorname{diag}(S)^{-1},</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>S</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>,</span></span></span></span></span><p>其中 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 为整个数据集的样本协方差矩阵。</p><p>要注意，这里的 <span><span>A=diag⁡(S)−1A=\operatorname{diag}(S)^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>S</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 表示只留下 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的对角元素，并把所有协方差设为 0，等价于各变量标准化后再计算欧氏距离的平方，要和前面的 <span><span>A=S−1A=S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 马氏距离区分开。</p><section><h4>例子<a href="#例子-1"><span>#</span></a></h4>

<table><thead><tr><th>对象</th><th>购买概率</th><th>广告观看时长（分钟）</th><th>广告观看时长（秒）</th><th>标准化购买概率</th><th>标准化观看时长</th></tr></thead><tbody><tr><td>A</td><td>60</td><td>3.0</td><td>180</td><td>-1.06</td><td>-1</td></tr><tr><td>B</td><td>65</td><td>3.5</td><td>210</td><td>0.93</td><td>0</td></tr><tr><td>C</td><td>63</td><td>4.0</td><td>240</td><td>0.13</td><td>1</td></tr></tbody></table><p>欧氏距离：</p>

<table><thead><tr><th>配对</th><th>时长（分钟）</th><th>排名</th><th>时长（秒）</th><th>排名</th><th>标准化值</th><th>排名</th></tr></thead><tbody><tr><td>A-B</td><td>5.025</td><td>3</td><td>30.41</td><td>2</td><td>2.22</td><td>2</td></tr><tr><td>A-C</td><td>3.162</td><td>2</td><td>60.07</td><td>3</td><td>2.33</td><td>3</td></tr><tr><td>B-C</td><td>2.062</td><td>1</td><td>30.06</td><td>1</td><td>1.28</td><td>1</td></tr></tbody></table><p>以 A-B 对为例，使用分钟进行计算：</p><span><span><span>d(A,B)=(60−65)2+(3.0−3.5)2≈5.025\begin{aligned}
d(A,B)&amp;=\sqrt{(60-65)^2+(3.0-3.5)^2} \\
&amp;\approx 5.025
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>d</span><span>(</span><span>A</span><span>,</span><span></span><span>B</span><span>)</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>60</span><span></span><span>−</span><span></span><span>65</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span>3.0</span><span></span><span>−</span><span></span><span>3.5</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span><span></span><span></span><span>≈</span><span></span><span>5.025</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>而购买概率不变，把分钟改成秒，也就是观看时长乘以 60 后，从上面的表格可以看出，A、B、C 本身没有发生任何实质变化，只是观看时长从分钟换成了秒，A-B 和 A-C 的排名却互换了。</p><p>原因是秒数的差异比分钟数大 60 倍；平方以后，其距离贡献变成原来的 <span><span>602=360060^2=3600</span><span><span><span></span><span>6</span><span><span>0</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3600</span></span></span></span> 倍，于是观看时长几乎完全支配了欧氏距离。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/stat-01/</id>
      <title type="text">矩阵代数回顾</title>
      <published>2026-09-01T00:00:00.000Z</published>
      <updated>2026-09-03T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/stat-01/"/>
      <summary type="text">STAT3613 前瞻，回顾矩阵代数的基本概念</summary>
      <content type="html"><![CDATA[<p>又开学了，因为有统计课，所以就借此契机复习下矩阵代数的基本概念。本篇不涉及高深的理论，仅供速查之用。</p>
<section><h2>1. 矩阵<a href="#1-矩阵"><span>#</span></a></h2><section><h3>1.1. 定义<a href="#11-定义"><span>#</span></a></h3><ul>
<li><strong>矩阵</strong>是一个矩形的数字阵列。</li>
<li>若矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是一个有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 行 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 列的数字阵列，则称 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 为 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵。</li>
<li>矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的元素记作 <span><span>aija_{ij}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，表示位于第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行、第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列的数。</li>
</ul><span><span><span>A=(aij)A=(a_{ij})</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>一般的 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵可以写成</p><span><span><span>A=[a11a12⋯a1na21a22⋯a2n⋮⋮⋱⋮am1am2⋯amn].A=
\begin{bmatrix}
a_{11} &amp; a_{12} &amp; \cdots &amp; a_{1n}\\
a_{21} &amp; a_{22} &amp; \cdots &amp; a_{2n}\\
\vdots &amp; \vdots &amp; \ddots &amp; \vdots\\
a_{m1} &amp; a_{m2} &amp; \cdots &amp; a_{mn}
\end{bmatrix}.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>m</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>m</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋱</span></span></span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>1</span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>2</span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>mn</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><ul>
<li>当 <span><span>n=1n=1</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时，<span><span>m×1m\times1</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 矩阵是一个<strong>向量</strong>。</li>
<li>当 <span><span>m=n=1m=n=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时，<span><span>1×11\times1</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 矩阵是一个<strong>标量</strong>。</li>
</ul><p>例如：</p><span><span><span>A=[3−21−76−4],b=[2−10],c=[2]=2.A=
\begin{bmatrix}
3 &amp; -2 &amp; 1\\
-7 &amp; 6 &amp; -4
\end{bmatrix},
\qquad
b=
\begin{bmatrix}
2\\
-1\\
0
\end{bmatrix},
\qquad
c=[2]=2.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>−</span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>2</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>2.</span></span></span></span></span></section><section><h3>1.2. 矩阵加法<a href="#12-矩阵加法"><span>#</span></a></h3><p>设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 为 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵。</p><ul>
<li><span><span>C=A+BC=A+B</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span></span></span></span> 是一个 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵。</li>
<li>若 <span><span>C=(cij)C=(c_{ij})</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，则</li>
</ul><span><span><span>cij=aij+bij.c_{ij}=a_{ij}+b_{ij}.</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>类似地，</p><ul>
<li><span><span>D=A−BD=A-B</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>B</span></span></span></span> 是一个 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵。</li>
<li>若 <span><span>D=(dij)D=(d_{ij})</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，则</li>
</ul><span><span><span>dij=aij−bij.d_{ij}=a_{ij}-b_{ij}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span></section><section><h3>1.3. 矩阵乘法<a href="#13-矩阵乘法"><span>#</span></a></h3><p>设</p><ul>
<li><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 为 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵，</li>
<li><span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 为 <span><span>n×pn\times p</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span> 矩阵，</li>
<li><span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 为标量。</li>
</ul><p>则 <span><span>C=ABC=AB</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>B</span></span></span></span> 是一个 <span><span>m×pm\times p</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span> 矩阵。</p><p>若 <span><span>C=(cij)C=(c_{ij})</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，则</p><span><span><span>cij=∑k=1naikbkj.c_{ij}=\sum_{k=1}^{n}a_{ik}b_{kj}.</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ik</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>k</span><span>j</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>例如：</p><span><span><span>[123567][321654143]=[182418586850].\begin{bmatrix}
1&amp;2&amp;3\\
5&amp;6&amp;7
\end{bmatrix}
\begin{bmatrix}
3&amp;2&amp;1\\
6&amp;5&amp;4\\
1&amp;4&amp;3
\end{bmatrix}
=
\begin{bmatrix}
18&amp;24&amp;18\\
58&amp;68&amp;50
\end{bmatrix}.</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>18</span></span></span><span><span></span><span><span>58</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>24</span></span></span><span><span></span><span><span>68</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>18</span></span></span><span><span></span><span><span>50</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><div><div><div></div><div>Important</div></div><div><p>一般而言，</p><span><span><span>AB≠BA.AB\neq BA.</span><span><span><span></span><span>A</span><span>B</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>B</span><span>A</span><span>.</span></span></span></span></span></div></div><p>对于标量乘法，</p><span><span><span>D=kAD=kA</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span>A</span></span></span></span></span><p>是一个 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵，且</p><span><span><span>dij=kaij.d_{ij}=ka_{ij}.</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>例如：</p><span><span><span>2[321212]=[642424].2
\begin{bmatrix}
3&amp;2&amp;1\\
2&amp;1&amp;2
\end{bmatrix}
=
\begin{bmatrix}
6&amp;4&amp;2\\
4&amp;2&amp;4
\end{bmatrix}.</span><span><span><span></span><span>2</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span></section><section><h3>1.4. 转置<a href="#14-转置"><span>#</span></a></h3><p>设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 为 <span><span>m×nm\times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵。</p><p><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的转置记作</p><span><span><span>B=A′B=A'</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><p>或</p><span><span><span>B=AT.B=A^T.</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>则</p><ul>
<li><span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是一个 <span><span>n×mn\times m</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span> 矩阵。</li>
<li>若 <span><span>B=(bij)B=(b_{ij})</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，则</li>
</ul><span><span><span>bij=aji.b_{ij}=a_{ji}.</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>例如：</p><span><span><span>[321212]T=[322112].\begin{bmatrix}
3 &amp; 2 &amp; 1\\
2 &amp; 1 &amp; 2
\end{bmatrix}^{T}
=
\begin{bmatrix}
3 &amp; 2\\
2 &amp; 1\\
1 &amp; 2
\end{bmatrix}.</span><span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>性质：</p><span><span><span>(A′)′=A(A')'=A</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span></span></span></span></span><p>以及</p><span><span><span>(AB)′=B′A′.(AB)'=B'A'.</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>等价地，</p><span><span><span>(AT)T=A,(AB)T=BTAT.(A^T)^T=A,
\qquad
(AB)^T=B^TA^T.</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>,</span><span></span><span></span><span>(</span><span>A</span><span>B</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span></section><section><h3>1.5. 若干定义<a href="#15-若干定义"><span>#</span></a></h3><ul>
<li>当 <span><span>m=nm=n</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span> 时，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是一个<strong>方阵</strong>。</li>
<li>当 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是方阵且满足</li>
</ul><span><span><span>bij=bji.b_{ij}=b_{ji}.</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>时，<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是<strong>对称</strong>的。</p><p>等价地，</p><span><span><span>BT=B.B^T=B.</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>B</span><span>.</span></span></span></span></span><ul>
<li>当 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 满足对所有 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 有</li>
</ul><span><span><span>aii=1a_{ii}=1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p>且对所有 <span><span>i≠ji\neq j</span><span><span><span></span><span>i</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>j</span></span></span></span> 有</p><span><span><span>aij=0a_{ij}=0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>时，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是一个<strong>单位矩阵</strong>。</p><p>单位矩阵记作 <span><span>InI_n</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 或简记为 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>。</p><p>例如，</p><span><span><span>I3=[100010001].I_3=
\begin{bmatrix}
1&amp;0&amp;0\\
0&amp;1&amp;0\\
0&amp;0&amp;1
\end{bmatrix}.</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><div><div><div></div><div>Question</div></div><div><p><span><span>X′XX'X</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>X</span></span></span></span> 会是什么？</p></div></div></section><section><h3>1.6. 逆<a href="#16-逆"><span>#</span></a></h3><p>设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 为 <span><span>n×nn\times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵。</p><p>若存在一个 <span><span>n×nn\times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 使得</p><span><span><span>AB=I,AB=I,</span><span><span><span></span><span>A</span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span>,</span></span></span></span></span><p>则称 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 为 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的<strong>逆</strong>：</p><span><span><span>B=A−1.B=A^{-1}.</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>同时，</p><span><span><span>AB=I  ⟺  BA=I.AB=I
\iff
BA=I.</span><span><span><span></span><span>A</span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span></span><span></span><span>⟺</span><span></span><span></span></span><span><span></span><span>B</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span>.</span></span></span></span></span><p>例如：</p><span><span><span>A=[0111],A−1=[−1110].A=
\begin{bmatrix}
0&amp;1\\
1&amp;1
\end{bmatrix},
\qquad
A^{-1}=
\begin{bmatrix}
-1&amp;1\\
1&amp;0
\end{bmatrix}.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>事实上，</p><span><span><span>[0111][−1110]=[1001].\begin{bmatrix}
0&amp;1\\
1&amp;1
\end{bmatrix}
\begin{bmatrix}
-1&amp;1\\
1&amp;0
\end{bmatrix}
=
\begin{bmatrix}
1&amp;0\\
0&amp;1
\end{bmatrix}.</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>若逆存在，则称 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 为<strong>非奇异的</strong>或<strong>可逆的</strong>。</p><p>若逆存在，则逆是唯一的。</p><section><h4>逆的性质<a href="#逆的性质"><span>#</span></a></h4><span><span><span>(kA)−1=1kA−1(kA)^{-1}=\frac{1}{k}A^{-1}</span><span><span><span></span><span>(</span><span>k</span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><span><span><span>(AB)−1=B−1A−1(AB)^{-1}=B^{-1}A^{-1}</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><span><span><span>(A′)−1=(A−1)′(A')^{-1}=(A^{-1})'</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><p>或等价地，</p><span><span><span>(AT)−1=(A−1)T.(A^T)^{-1}=(A^{-1})^T.</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>最后，</p><span><span><span>(A−1)−1=A.(A^{-1})^{-1}=A.</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>.</span></span></span></span></span></section><section><h4>例子：用行变换求逆<a href="#例子用行变换求逆"><span>#</span></a></h4><p>求以下矩阵的逆</p><span><span><span>H=[11213121314131415].H=
\begin{bmatrix}
1 &amp; \frac12 &amp; \frac13\\
\frac12 &amp; \frac13 &amp; \frac14\\
\frac13 &amp; \frac14 &amp; \frac15
\end{bmatrix}.</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>4</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>4</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>5</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>从增广矩阵 <span><span>[H∣I][H\mid I]</span><span><span><span></span><span>[</span><span>H</span><span></span><span>∣</span><span></span></span><span><span></span><span>I</span><span>]</span></span></span></span> 出发：</p><span><span><span>[11213100121314010131415001].\left[
\begin{array}{ccc|ccc}
1 &amp; \frac12 &amp; \frac13 &amp; 1&amp;0&amp;0\\
\frac12 &amp; \frac13 &amp; \frac14 &amp; 0&amp;1&amp;0\\
\frac13 &amp; \frac14 &amp; \frac15 &amp; 0&amp;0&amp;1
\end{array}
\right].</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>4</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>4</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>5</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>施以变换</p><span><span><span>R2←R2−12R1,R3←R3−13R1.R_2\leftarrow R_2-\frac12R_1,
\qquad
R_3\leftarrow R_3-\frac13R_1.</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>则得到</p><span><span><span>[112131000112112−12100112445−1301].\left[
\begin{array}{ccc|ccc}
1 &amp; \frac12 &amp; \frac13 &amp; 1&amp;0&amp;0\\
0 &amp; \frac1{12} &amp; \frac1{12} &amp; -\frac12&amp;1&amp;0\\
0 &amp; \frac1{12} &amp; \frac4{45} &amp; -\frac13&amp;0&amp;1
\end{array}
\right].</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>45</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>施以变换</p><span><span><span>R2←12R2.R_2\leftarrow12R_2.</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span>12</span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>则得到</p><span><span><span>[11213100011−61200112445−1301].\left[
\begin{array}{ccc|ccc}
1 &amp; \frac12 &amp; \frac13 &amp; 1&amp;0&amp;0\\
0 &amp; 1 &amp; 1 &amp; -6&amp;12&amp;0\\
0 &amp; \frac1{12} &amp; \frac4{45} &amp; -\frac13&amp;0&amp;1
\end{array}
\right].</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>45</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>6</span></span></span><span><span></span><span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>12</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>施以变换</p><span><span><span>R1←R1−12R2,R3←R3−112R2.R_1\leftarrow R_1-\frac12R_2,
\qquad
R_3\leftarrow R_3-\frac1{12}R_2.</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>12</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>则得到</p><span><span><span>[10−164−60011−612000118016−11].\left[
\begin{array}{ccc|ccc}
1 &amp; 0 &amp; -\frac16 &amp; 4&amp;-6&amp;0\\
0 &amp; 1 &amp; 1 &amp; -6&amp;12&amp;0\\
0 &amp; 0 &amp; \frac1{180} &amp; \frac16&amp;-1&amp;1
\end{array}
\right].</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>6</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>6</span></span></span><span><span></span><span><span>12</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>施以变换</p><span><span><span>R3←180R3.R_3\leftarrow180R_3.</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span>180</span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>则得到</p><span><span><span>[10−164−60011−612000130−180180].\left[
\begin{array}{ccc|ccc}
1 &amp; 0 &amp; -\frac16 &amp; 4&amp;-6&amp;0\\
0 &amp; 1 &amp; 1 &amp; -6&amp;12&amp;0\\
0 &amp; 0 &amp; 1 &amp; 30&amp;-180&amp;180
\end{array}
\right].</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>6</span></span></span><span><span></span><span><span>30</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>6</span></span></span><span><span></span><span><span>12</span></span></span><span><span></span><span><span>−</span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>最后，施以变换</p><span><span><span>R1←R1+16R3,R2←R2−R3.R_1\leftarrow R_1+\frac16R_3,
\qquad
R_2\leftarrow R_2-R_3.</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>于是</p><span><span><span>[1009−3630010−36192−18000130−180180].\left[
\begin{array}{ccc|ccc}
1&amp;0&amp;0&amp;9&amp;-36&amp;30\\
0&amp;1&amp;0&amp;-36&amp;192&amp;-180\\
0&amp;0&amp;1&amp;30&amp;-180&amp;180
\end{array}
\right].</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>−</span><span>36</span></span></span><span><span></span><span><span>30</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>36</span></span></span><span><span></span><span><span>192</span></span></span><span><span></span><span><span>−</span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>30</span></span></span><span><span></span><span><span>−</span><span>180</span></span></span><span><span></span><span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>因此，</p><span><span><span>H−1=[9−3630−36192−18030−180180].\boxed{
H^{-1}
=
\begin{bmatrix}
9&amp;-36&amp;30\\
-36&amp;192&amp;-180\\
30&amp;-180&amp;180
\end{bmatrix}
}.</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>−</span><span>36</span></span></span><span><span></span><span><span>30</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>36</span></span></span><span><span></span><span><span>192</span></span></span><span><span></span><span><span>−</span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>30</span></span></span><span><span></span><span><span>−</span><span>180</span></span></span><span><span></span><span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>.</span></span></span></span></span></section></section><section><h3>1.7. 正交矩阵<a href="#17-正交矩阵"><span>#</span></a></h3><p>对于一个 <span><span>n×nn\times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是<strong>正交的</strong>当且仅当</p><span><span><span>A′A=I.A'A=I.</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span>.</span></span></span></span></span><p>等价地，</p><span><span><span>ATA=I.A^TA=I.</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span>.</span></span></span></span></span><p>例如：</p><span><span><span>A=[cos⁡θsin⁡θ−sin⁡θcos⁡θ].A=
\begin{bmatrix}
\cos\theta &amp; \sin\theta\\
-\sin\theta &amp; \cos\theta
\end{bmatrix}.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>则</p><span><span><span>A′A=[cos⁡θ−sin⁡θsin⁡θcos⁡θ][cos⁡θsin⁡θ−sin⁡θcos⁡θ]=[1001].A'A
=
\begin{bmatrix}
\cos\theta &amp; -\sin\theta\\
\sin\theta &amp; \cos\theta
\end{bmatrix}
\begin{bmatrix}
\cos\theta &amp; \sin\theta\\
-\sin\theta &amp; \cos\theta
\end{bmatrix}
=
\begin{bmatrix}
1&amp;0\\
0&amp;1
\end{bmatrix}.</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span><span><span></span><span><span>sin</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>因此，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是正交矩阵。</p><p>设</p><span><span><span>x=[xy].x=
\begin{bmatrix}
x\\
y
\end{bmatrix}.</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>则</p><span><span><span>Ax=[cos⁡θsin⁡θ−sin⁡θcos⁡θ][xy]=[xcos⁡θ+ysin⁡θ−xsin⁡θ+ycos⁡θ].Ax
=
\begin{bmatrix}
\cos\theta &amp; \sin\theta\\
-\sin\theta &amp; \cos\theta
\end{bmatrix}
\begin{bmatrix}
x\\
y
\end{bmatrix}
=
\begin{bmatrix}
x\cos\theta+y\sin\theta\\
-x\sin\theta+y\cos\theta
\end{bmatrix}.</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>x</span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>+</span><span></span><span>y</span><span></span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>−</span><span>x</span><span></span><span>sin</span><span></span><span>θ</span><span></span><span>+</span><span></span><span>y</span><span></span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span></section><section><h3>1.8. 特征值与特征向量<a href="#18-特征值与特征向量"><span>#</span></a></h3><p>设 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 和 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 是 <span><span>n×nn\times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的一对特征值-特征向量。则</p><span><span><span>(A−λI)v=0.(A-\lambda I)v=0.</span><span><span><span></span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>0.</span></span></span></span></span><p>讲义指出，若 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可逆，则存在 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 对特征值与特征向量，其中 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的维数。</p><p>设 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span> 为包含所有特征值的对角矩阵，并设 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 为以相应的单位长度特征向量为列的矩阵。则</p><span><span><span>A=VΛV−1.A=V\Lambda V^{-1}.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span>Λ</span><span><span>V</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>若 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是对称矩阵，</p><span><span><span>A=VΛVT.A=V\Lambda V^T.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span>Λ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><hr /></section></section>
<section><h2>2. 统计量的矩阵表达<a href="#2-统计量的矩阵表达"><span>#</span></a></h2><section><h3>2.1. 随机向量<a href="#21-随机向量"><span>#</span></a></h3><p>随机向量的元素是随机变量。</p><span><span><span>Y=[y1⋮yn].Y=
\begin{bmatrix}
y_1\\
\vdots\\
y_n
\end{bmatrix}.</span><span><span><span></span><span>Y</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>变量 <span><span>y1,…,yny_1,\ldots,y_n</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 可以是相关的或独立的。</p><p>它们可以表示，例如：</p><ul>
<li><span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个独立观测，或</li>
<li>来自同一次观测的 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个随机变量。</li>
</ul></section><section><h3>2.2. 均值向量<a href="#22-均值向量"><span>#</span></a></h3><p>均值向量的元素是随机向量相应元素的期望值：</p><span><span><span>μ=E(Y)=[E(y1)⋮E(yn)].\mu=E(Y)
=
\begin{bmatrix}
E(y_1)\\
\vdots\\
E(y_n)
\end{bmatrix}.</span><span><span><span></span><span>μ</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span>(</span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>E</span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span>E</span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span></section><section><h3>2.3. 协方差矩阵<a href="#23-协方差矩阵"><span>#</span></a></h3><p>设</p><ul>
<li><span><span>XX</span><span><span><span></span><span>X</span></span></span></span> 为 <span><span>m×1m\times1</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 随机向量，</li>
<li><span><span>YY</span><span><span><span></span><span>Y</span></span></span></span> 为 <span><span>n×1n\times1</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 随机向量。</li>
</ul><p>方差-协方差矩阵，或称协方差矩阵，定义为</p><span><span><span>Cov⁡(X,Y)=E[(X−μX)(Y−μY)T].\operatorname{Cov}(X,Y)
=
E\left[
(X-\mu_X)(Y-\mu_Y)^T
\right].</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>X</span><span>,</span><span></span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span></span><span><span><span>[</span></span><span>(</span><span>X</span><span></span><span>−</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>X</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span>Y</span><span></span><span>−</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>Y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>若</p><span><span><span>Cov⁡(X,Y)=(σij),\operatorname{Cov}(X,Y)=(\sigma_{ij}),</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>X</span><span>,</span><span></span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span></span></span></span></span><p>则</p><span><span><span>σij=Cov⁡(xi,yj).\sigma_{ij}=\operatorname{Cov}(x_i,y_j).</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Cov</span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span><p><span><span>Cov⁡(X,Y)\operatorname{Cov}(X,Y)</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>X</span><span>,</span><span></span><span>Y</span><span>)</span></span></span></span> 的维数为</p><span><span><span>m×n.m\times n.</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span><span>.</span></span></span></span></span><p>同时，</p><span><span><span>Cov⁡(X,Y)=E(XYT)−E(X)E(Y)T.\operatorname{Cov}(X,Y)
=
E(XY^T)-E(X)E(Y)^T.</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>X</span><span>,</span><span></span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span>(</span><span>X</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>E</span><span>(</span><span>X</span><span>)</span><span>E</span><span>(</span><span>Y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>对于随机向量 <span><span>YY</span><span><span><span></span><span>Y</span></span></span></span>，</p><span><span><span>Σ=Var⁡(Y)=E[(Y−μY)(Y−μY)T].\Sigma
=
\operatorname{Var}(Y)
=
E\left[(Y-\mu_Y)(Y-\mu_Y)^T\right].</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Var</span></span><span>(</span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span></span><span><span><span>[</span></span><span>(</span><span>Y</span><span></span><span>−</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>Y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span>Y</span><span></span><span>−</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>Y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>等价地，</p><span><span><span>Σ=E(YYT)−E(Y)E(Y)T.\Sigma
=
E(YY^T)-E(Y)E(Y)^T.</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span>(</span><span>Y</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>E</span><span>(</span><span>Y</span><span>)</span><span>E</span><span>(</span><span>Y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span></section><section><h3>2.4. 线性组合<a href="#24-线性组合"><span>#</span></a></h3><p>设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 为常数矩阵，<span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 和 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 为常数向量，<span><span>XX</span><span><span><span></span><span>X</span></span></span></span> 和 <span><span>YY</span><span><span><span></span><span>Y</span></span></span></span> 为两个 <span><span>n×1n\times1</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 随机向量，且</p><span><span><span>Var⁡(Y)=Σ.\operatorname{Var}(Y)=\Sigma.</span><span><span><span></span><span><span>Var</span></span><span>(</span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>Σ.</span></span></span></span></span><p>则</p><span><span><span>E(AY+BX+b)=AE(Y)+BE(X)+b.E(AY+BX+b)
=
AE(Y)+BE(X)+b.</span><span><span><span></span><span>E</span><span>(</span><span>A</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>X</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>E</span><span>(</span><span>Y</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>E</span><span>(</span><span>X</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>.</span></span></span></span></span><p>同时，</p><span><span><span>Cov⁡(AY+b,  BY+c)=AΣB′.\operatorname{Cov}(AY+b,\;BY+c)
=
A\Sigma B'.</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>A</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>,</span><span></span><span></span><span>B</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>Σ</span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>等价地，</p><span><span><span>Cov⁡(AY+b,  BY+c)=AΣBT.\operatorname{Cov}(AY+b,\;BY+c)
=
A\Sigma B^T.</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>A</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>,</span><span></span><span></span><span>B</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>Σ</span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>最后，</p><span><span><span>Var⁡(AY+b)=AΣA′=AΣAT.\operatorname{Var}(AY+b)
=
A\Sigma A'
=
A\Sigma A^T.</span><span><span><span></span><span><span>Var</span></span><span>(</span><span>A</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>Σ</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>Σ</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span></section><section><h3>2.5. 多元正态分布<a href="#25-多元正态分布"><span>#</span></a></h3><p>当 <span><span>p×1p\times1</span><span><span><span></span><span>p</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 随机向量 <span><span>YY</span><span><span><span></span><span>Y</span></span></span></span> 服从多元正态分布时，</p><span><span><span>Y∼N(μ,Σ),Y\sim N(\mu,\Sigma),</span><span><span><span></span><span>Y</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>μ</span><span>,</span><span></span><span>Σ</span><span>)</span><span>,</span></span></span></span></span><p>其概率密度函数为</p><span><span><span>ϕ(Y)=1(2π)p/2∣Σ∣1/2exp⁡[−12(Y−μ)TΣ−1(Y−μ)].\phi(Y)
=
\frac{1}
{(2\pi)^{p/2}|\Sigma|^{1/2}}
\exp\left[
-\frac12
(Y-\mu)^T
\Sigma^{-1}
(Y-\mu)
\right].</span><span><span><span></span><span>ϕ</span><span>(</span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>2</span><span>π</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>p</span><span>/2</span></span></span></span></span></span></span></span></span><span>∣Σ</span><span><span>∣</span><span><span><span><span><span><span></span><span><span><span>1/2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>exp</span><span></span><span><span><span>[</span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>Y</span><span></span><span>−</span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>Y</span><span></span><span>−</span><span></span><span>μ</span><span>)</span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>则</p><span><span><span>E(Y)=μ,Var⁡(Y)=Σ=(σij).E(Y)=\mu,
\qquad
\operatorname{Var}(Y)=\Sigma=(\sigma_{ij}).</span><span><span><span></span><span>E</span><span>(</span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>μ</span><span>,</span><span></span><span></span><span><span>Var</span></span><span>(</span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span><p>对每个分量，</p><span><span><span>E(yi)=μi,E(y_i)=\mu_i,</span><span><span><span></span><span>E</span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span></span></span></span></span><span><span><span>Var⁡(yi)=σii,\operatorname{Var}(y_i)=\sigma_{ii},</span><span><span><span></span><span><span>Var</span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span></span></span></span></span><p>且</p><span><span><span>Cov⁡(yi,yj)=σij.\operatorname{Cov}(y_i,y_j)=\sigma_{ij}.</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>同时，</p><span><span><span>yi∼N(μi,σii).y_i\sim N(\mu_i,\sigma_{ii}).</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span><span>μ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span><section><h4>样本的似然<a href="#样本的似然"><span>#</span></a></h4><p>对于容量为 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的样本</p><span><span><span>y1,…,yn,y_1,\ldots,y_n,</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span></span></span></span></span><p>似然函数为</p><span><span><span>L(μ,Σ∣y1,…,yn)=∏i=1nϕ(yi)L(\mu,\Sigma\mid y_1,\ldots,y_n)
=
\prod_{i=1}^{n}
\phi(y_i)</span><span><span><span></span><span>L</span><span>(</span><span>μ</span><span>,</span><span></span><span>Σ</span><span></span><span>∣</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∏</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>ϕ</span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>或</p><span><span><span>L(μ,Σ∣y1,…,yn)=∏i=1n1(2π)p/2∣Σ∣1/2exp⁡[−12(yi−μ)TΣ−1(yi−μ)].L(\mu,\Sigma\mid y_1,\ldots,y_n)
=
\prod_{i=1}^{n}
\frac{1}
{(2\pi)^{p/2}|\Sigma|^{1/2}}
\exp\left[
-\frac12
(y_i-\mu)^T
\Sigma^{-1}
(y_i-\mu)
\right].</span><span><span><span></span><span>L</span><span>(</span><span>μ</span><span>,</span><span></span><span>Σ</span><span></span><span>∣</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∏</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>2</span><span>π</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>p</span><span>/2</span></span></span></span></span></span></span></span></span><span>∣Σ</span><span><span>∣</span><span><span><span><span><span><span></span><span><span><span>1/2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>exp</span><span></span><span><span><span>[</span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span>μ</span><span>)</span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p>对数似然为</p><span><span><span>ℓ=log⁡L=−np2log⁡(2π)−n2log⁡∣Σ∣−12∑i=1n(yi−μ)TΣ−1(yi−μ).\ell
=
\log L
=
-\frac{np}{2}\log(2\pi)
-\frac{n}{2}\log|\Sigma|
-\frac12
\sum_{i=1}^{n}
(y_i-\mu)^T
\Sigma^{-1}
(y_i-\mu).</span><span><span><span></span><span>ℓ</span><span></span><span>=</span><span></span></span><span><span></span><span>lo<span>g</span></span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>n</span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span>(</span><span>2</span><span>π</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>lo<span>g</span></span><span></span><span>∣Σ∣</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span><span>.</span></span></span></span></span></section><section><h4>线性变换<a href="#线性变换"><span>#</span></a></h4><p>对于常数矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和常数向量 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span>，</p><span><span><span>AY+c∼N(Aμ+c,  AΣA′).AY+c
\sim
N(A\mu+c,\;A\Sigma A').</span><span><span><span></span><span>A</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>A</span><span>μ</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span>,</span><span></span><span></span><span>A</span><span>Σ</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span><p>等价地，</p><span><span><span>AY+c∼N(Aμ+c,  AΣAT).AY+c
\sim
N(A\mu+c,\;A\Sigma A^T).</span><span><span><span></span><span>A</span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>A</span><span>μ</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span>,</span><span></span><span></span><span>A</span><span>Σ</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span></section><section><h4>分块的多元正态向量<a href="#分块的多元正态向量"><span>#</span></a></h4><p>设 <span><span>YY</span><span><span><span></span><span>Y</span></span></span></span> 被分块为 <span><span>Y1Y_1</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>Y2Y_2</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Y=[Y1Y2]∼N([μ1μ2],[Σ11Σ12Σ21Σ22]).Y=
\begin{bmatrix}
Y_1\\
Y_2
\end{bmatrix}
\sim
N\left(
\begin{bmatrix}
\mu_1\\
\mu_2
\end{bmatrix},
\begin{bmatrix}
\Sigma_{11} &amp; \Sigma_{12}\\
\Sigma_{21} &amp; \Sigma_{22}
\end{bmatrix}
\right).</span><span><span><span></span><span>Y</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>Y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>Y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span></span><span><span><span>(</span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span>)</span></span></span><span></span><span>.</span></span></span></span></span><p>则</p><span><span><span>Σ11=Var⁡(Y1),\Sigma_{11}=\operatorname{Var}(Y_1),</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Var</span></span><span>(</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span></span></span></span></span><span><span><span>Σ22=Var⁡(Y2),\Sigma_{22}=\operatorname{Var}(Y_2),</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Var</span></span><span>(</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span></span></span></span></span><p>且</p><span><span><span>Σ12=Cov⁡(Y1,Y2).\Sigma_{12}=\operatorname{Cov}(Y_1,Y_2).</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Cov</span></span><span>(</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span><p>讲义指出，<span><span>Y1Y_1</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>Y2Y_2</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 相互独立当且仅当</p><span><span><span>Σ12=0.\Sigma_{12}=0.</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.</span></span></span></span></span></section></section><section><h3>2.6. 样本统计量<a href="#26-样本统计量"><span>#</span></a></h3><p>假设有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个观测、<span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 个变量。</p><section><h4>观测<a href="#观测"><span>#</span></a></h4><p>第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个观测为</p><span><span><span>Xi=[xi1⋮xip],i=1,…,n.X_i=
\begin{bmatrix}
x_{i1}\\
\vdots\\
x_{ip}
\end{bmatrix},
\qquad
i=1,\ldots,n.</span><span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>i</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>i</span><span>p</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>n</span><span>.</span></span></span></span></span></section><section><h4>样本均值<a href="#样本均值"><span>#</span></a></h4><p>样本均值向量为</p><span><span><span>Xˉ=[xˉ1⋮xˉp],\bar X=
\begin{bmatrix}
\bar x_1\\
\vdots\\
\bar x_p
\end{bmatrix},</span><span><span><span></span><span><span><span><span><span><span></span><span>X</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span></span></span></span></span><p>其中</p><span><span><span>xˉj=1n∑i=1nxij.\bar x_j
=
\frac1n
\sum_{i=1}^{n}x_{ij}.</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>.</span></span></span></span></span></section><section><h4>样本协方差矩阵<a href="#样本协方差矩阵"><span>#</span></a></h4><p>样本协方差矩阵为</p><span><span><span>S=1n−1∑i=1n(Xi−Xˉ)(Xi−Xˉ)T.S
=
\frac{1}{n-1}
\sum_{i=1}^{n}
(X_i-\bar X)(X_i-\bar X)^T.</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>n</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>X</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>X</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>X</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>变量 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 的样本方差为</p><span><span><span>sjj=1n−1∑i=1n(xij−xˉj)2.s_{jj}
=
\frac{1}{n-1}
\sum_{i=1}^{n}
(x_{ij}-\bar x_j)^2.</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span><span>j</span><span>j</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>n</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>变量 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 与 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 之间的样本协方差为</p><span><span><span>sjk=1n−1∑i=1n(xij−xˉj)(xik−xˉk).s_{jk}
=
\frac{1}{n-1}
\sum_{i=1}^{n}
(x_{ij}-\bar x_j)(x_{ik}-\bar x_k).</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span><span>j</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>n</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>ik</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>.</span></span></span></span></span></section><section><h4>样本相关矩阵<a href="#样本相关矩阵"><span>#</span></a></h4><p>样本相关矩阵为</p><span><span><span>R=diag⁡(S)−1/2Sdiag⁡(S)−1/2.R
=
\operatorname{diag}(S)^{-1/2}
S
\operatorname{diag}(S)^{-1/2}.</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>S</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1/2</span></span></span></span></span></span></span></span></span><span>S</span><span></span><span><span>diag</span></span><span>(</span><span>S</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1/2</span></span></span></span></span></span></span></span></span><span>.</span></span></span></span></span><p>变量 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 与 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 之间的样本相关系数为</p><span><span><span>rjk=sjksjjskk.r_{jk}
=
\frac{s_{jk}}
{\sqrt{s_{jj}s_{kk}}}.</span><span><span><span></span><span><span>r</span><span><span><span><span><span><span></span><span><span><span>j</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span><span>j</span><span>j</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span><span>k</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span><span>j</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>.</span></span></span></span></span></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/agent-01/</id>
      <title type="text">步骤并行化</title>
      <published>2026-08-28T00:00:00.000Z</published>
      <updated>2026-08-28T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/agent-01/"/>
      <summary type="text">介绍如何解决 Context 并发写入与 SSE 乱序问题</summary>
      <content type="html"><![CDATA[<p>在 Agent 系统中并行化步骤时，错误隔离是关键设计。我们在<a href="https://mp.weixin.qq.com/s/xH4cyBJJJlG9cfcmSU5ztA" target="_blank">从 Prompt 到 Harness：企业级 Agent 工程的完整演进之路</a>中讨论了两种错误隔离机制，针对两种不同的并发混乱：</p>
<blockquote><p>错误隔离是关键设计：<code>Promise.allSettled</code> 保证一个子任务失败不中断其他子任务，失败结果标记 <code>__error</code> 传递给下游步骤由 LLM 自行判断处理。Context 并发写入安全通过浅拷贝 sharedContext 确保只读前序数据。SSE 事件乱序问题通过 <code>taskId</code> 分组渲染解决。</p></blockquote>
<p>下面我们来简单介绍一下原文提到的两种隔离机制，以及它们对应的并发混乱问题。</p>
<section><h1>1. Context 并发写入<a href="#1-context-并发写入"><span>#</span></a></h1><p>假设我们有两个步骤并行执行，并且共享同一个 <code>sharedContext</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>sharedContext</span><span> </span></span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>userInput</span><span>:</span><span> </span><span>"分析销售数据"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>previousResult</span><span>:</span><span> </span><span>"……"</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果两个并行步骤都直接修改它的话：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>步骤 A：写入 sharedContext.result = A</span></div></div><div><div><div>2</div></div><div><span>步骤 B：写入 sharedContext.result = B</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>显然，由于 A，B 的执行顺序不固定，所以可能出现：</p><ul>
<li>A 覆盖 B；</li>
<li>B 覆盖 A；</li>
<li>一个步骤读到了另一个步骤写到一半的数据。</li>
</ul><p>这就是 Context 的并发写入问题。</p><section><h2>1.1 解决方案：软拷贝<a href="#11-解决方案软拷贝"><span>#</span></a></h2><p>文中的解决方式是软拷贝。也就是说，在启动每个并行步骤前，我们给它一份独立的浅拷贝：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>contextA</span><span> </span><span>=</span><span> { </span><span>...</span><span><span>sharedContext</span><span> };</span></span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>contextB</span><span> </span><span>=</span><span> { </span><span>...</span><span><span>sharedContext</span><span> };</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>于是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>步骤 A 只读取 contextA，并把结果单独返回</span></div></div><div><div><div>2</div></div><div><span>步骤 B 只读取 contextB，并把结果单独返回</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可见，二者都不直接修改原始的 <code>sharedContext</code>，而是等它们都完成后，再由调度器统一合并结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> [</span><span>resultA</span><span>, </span><span>resultB</span><span>] </span><span>=</span><span> </span><span>await</span><span> </span><span>Promise</span><span>.</span><span>all</span><span>([</span></div></div><div><div><div>2</div></div><div><span>  </span><span>runStepA</span><span><span>(</span><span>contextA</span><span>),</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>runStepB</span><span><span>(</span><span>contextB</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span>]);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>sharedContext</span><span>.</span></span><span>stepAResult</span><span> </span><span>=</span><span><span> </span><span>resultA</span><span>;</span></span></div></div><div><div><div>7</div></div><div><span><span>sharedContext</span><span>.</span></span><span>stepBResult</span><span> </span><span>=</span><span><span> </span><span>resultB</span><span>;</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p>浅拷贝的局限性在于，它只复制最外层。</p><p>换句话说，如果 Context 里有嵌套对象，比如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>sharedContext</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>user</span><span>:</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>name</span><span>:</span><span> </span><span>"Alice"</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>const</span><span> </span><span>contextA</span><span> </span><span>=</span><span> { </span><span>...</span><span><span>sharedContext</span><span> };</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>那么 <code>contextA.user</code> 和 <code>sharedContext.user</code> 仍指向同一个对象，不同的步骤依然会修改共享数据。</p><p>因此，这种方案真正安全的前提是：</p><ul>
<li>并行步骤严格只读<a href="/posts/agent-02/">前序数据</a>；</li>
<li>或冻结对象，禁止修改；</li>
<li>或使用<a href="/posts/agent-03/">深拷贝</a>；</li>
<li>或采用不可变数据结构。</li>
</ul><p>在<a href="https://mp.weixin.qq.com/s/xH4cyBJJJlG9cfcmSU5ztA" target="_blank">原文</a>中，<strong>浅拷贝恰恰是配合只读约束，防止并行步骤直接争抢同一个顶层 Context</strong>.</p></section></section>
<section><h1>2. SSE 事件乱序问题<a href="#2-sse-事件乱序问题"><span>#</span></a></h1><p>同样，假设两个 Agent 任务同时执行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>任务 A：生成报告</span></div></div><div><div><div>2</div></div><div><span>任务 B：分析代码</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们产生的 SSE 事件就可能交错到达前端：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A：开始搜索</span></div></div><div><div><div>2</div></div><div><span>B：开始读取代码</span></div></div><div><div><div>3</div></div><div><span>B：读取完成</span></div></div><div><div><div>4</div></div><div><span>A：搜索完成</span></div></div><div><div><div>5</div></div><div><span>B：开始分析</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果前端把所有事件直接放进同一个列表，用户看到的就是两个任务混在一起，容易误以为事件顺序错乱。</p><section><h2>2.1 解决方案：分组<a href="#21-解决方案分组"><span>#</span></a></h2><p>原文中的解决办法是：每条 SSE 事件都携带所属任务的 ID，按照 <code>taskID</code> 进行分组：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"taskId"</span><span>: </span><span>"task-A"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"type"</span><span>: </span><span>"search_started"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"message"</span><span>: </span><span>"开始搜索"</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>{</span></div></div><div><div><div>8</div></div><div><span>  </span><span>"taskId"</span><span>: </span><span>"task-B"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>"type"</span><span>: </span><span>"code_read"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>"message"</span><span>: </span><span>"读取代码完成"</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样，前端收到事件后，不是全部追加到同一个列表，而是根据 <code>taskId</code> 分组：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>eventsByTask</span><span>[</span><span>event</span><span>.</span></span><span>taskId</span><span>].</span><span>push</span><span><span>(</span><span>event</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最终就会分别显示：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>任务 A</span></div></div><div><div><div>2</div></div><div><span>1. 开始搜索</span></div></div><div><div><div>3</div></div><div><span>2. 搜索完成</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>任务 B</span></div></div><div><div><div>6</div></div><div><span>1. 开始读取代码</span></div></div><div><div><div>7</div></div><div><span>2. 读取完成</span></div></div><div><div><div>8</div></div><div><span>3. 开始分析</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样，即使不同任务的事件在网络中交错到达，界面也不会把它们混在一起。</p><hr /><p>需要注意的是，<code>taskID</code> 并不能解决同一任务内部的乱序，因为它们的 <code>taskID</code> 完全相同。此时，我们还需要给事件增加递增序号：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"taskId"</span><span>: </span><span>"task-A"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"sequence"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"message"</span><span>: </span><span>"工具完成"</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后前端就按下面的规则处理：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>先按 taskId 分组</span></div></div><div><div><div>2</div></div><div><span>再按 sequence 排序</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/agent-02/</id>
      <title type="text">什么是前序数据</title>
      <published>2026-08-28T00:00:00.000Z</published>
      <updated>2026-08-28T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/agent-02/"/>
      <summary type="text">介绍前序数据的基本概念</summary>
      <content type="html"><![CDATA[<p><strong>前序数据</strong>是指：在当前并行步骤开始之前，上游步骤已经产生并确定下来的数据。</p>
<p>例如有这样一个工作流：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>步骤 1：读取用户需求</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>步骤 2：搜索网页</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>┌────┴────┐</span></div></div><div><div><div>6</div></div><div><span>步骤 3A：生成摘要</span></div></div><div><div><div>7</div></div><div><span>步骤 3B：提取关键词</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>步骤 3A 和 3B 并行执行。对它们来说，步骤 1、步骤 2 已经产生的数据就是“前序数据”：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>sharedContext</span><span> </span></span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>userRequest</span><span>:</span><span> </span><span>"分析新能源汽车市场"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>searchResults</span><span>:</span><span> [</span><span>"资料 A"</span><span>, </span><span>"资料 B"</span><span>]</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>在并行执行期间：</p>
<ul>
<li>3A 可以读取 <code>userRequest</code> 和 <code>searchResults</code>；</li>
<li>3B 也可以读取 <code>userRequest</code> 和 <code>searchResults</code>；</li>
<li>但 3A 不应修改 <code>searchResults</code>；</li>
<li>3B 也不应修改它；</li>
<li>3A 和 3B 不应依赖对方尚未完成的结果。</li>
</ul>
<p>因此，“只读前序数据”的意思是：</p>
<blockquote><p>并行步骤可以把上游已经确定的数据当作参考资料，但不能修改这些共享资料。</p></blockquote>
<p>完成后，3A 和 3B 分别返回自己的新结果，再由调度器统一合并：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>sharedContext</span><span> </span></span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>userRequest</span><span>:</span><span> </span><span>"分析新能源汽车市场"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>searchResults</span><span>:</span><span> [</span><span>"资料 A"</span><span>, </span><span>"资料 B"</span><span>],</span></div></div><div><div><div>4</div></div><div><span>  </span><span>summary</span><span>:</span><span><span> </span><span>resultA</span><span>,</span></span></div></div><div><div><div>5</div></div><div><span>  </span><span>keywords</span><span>:</span><span><span> </span><span>resultB</span></span></div></div><div><div><div>6</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/agent-03/</id>
      <title type="text">什么是深拷贝</title>
      <published>2026-08-28T00:00:00.000Z</published>
      <updated>2026-08-28T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/agent-03/"/>
      <summary type="text">介绍深拷贝的基本概念</summary>
      <content type="html"><![CDATA[<section><h2>1. 什么是浅拷贝？<a href="#1-什么是浅拷贝"><span>#</span></a></h2><p>浅拷贝只创建一个新的<strong>最外层对象</strong>，内部嵌套对象仍然与原对象共用。</p><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>original</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>taskName</span><span>:</span><span> </span><span>"市场分析"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>user</span><span>:</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>name</span><span>:</span><span> </span><span>"Alice"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>};</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>const</span><span> </span><span>copy</span><span> </span><span>=</span><span> { </span><span>...</span><span><span>original</span><span> };</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>此时：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>original          copy</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>│                │</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>├─ taskName      ├─ taskName</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>│                │</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>└──────┐   ┌─────┘</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>↓   ↓</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>同一个 user 对象</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修改最外层字段通常互不影响：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>copy</span><span>.</span></span><span>taskName</span><span> </span><span>=</span><span> </span><span>"行业分析"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>original</span><span>.</span></span><span>taskName</span><span>);</span></div></div><div><div><div>4</div></div><div><span>// "市场分析"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但是修改嵌套对象会影响原对象：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>copy</span><span>.</span></span><span>user</span><span>.</span><span>name</span><span> </span><span>=</span><span> </span><span>"Bob"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>original</span><span>.</span></span><span>user</span><span>.</span><span>name</span><span>);</span></div></div><div><div><div>4</div></div><div><span>// "Bob"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>原因是 <code>copy.user</code> 和 <code>original.user</code> 指向同一份内存数据。</p><hr /></section>
<section><h2>2. 什么是深拷贝？<a href="#2-什么是深拷贝"><span>#</span></a></h2><p><strong>深拷贝</strong>不仅复制最外层对象，还递归复制内部所有嵌套对象和数组。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>deepCopy</span><span> </span><span>=</span><span> </span><span>structuredClone</span><span><span>(</span><span>original</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以理解为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>original                    deepCopy</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>│                           │</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>└─ user 对象 A              └─ user 对象 B</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>对象 A 和对象 B 内容相同，但彼此独立</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现在修改副本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>deepCopy</span><span>.</span></span><span>user</span><span>.</span><span>name</span><span> </span><span>=</span><span> </span><span>"Carol"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>original</span><span>.</span></span><span>user</span><span>.</span><span>name</span><span>);</span></div></div><div><div><div>4</div></div><div><span>// 不会变成 "Carol"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因此，并行任务各自拿到深拷贝后，即使修改嵌套内容，也不会直接影响原始 Context 或其他任务的副本。</p><hr /></section>
<section><h2>3. 浅拷贝和深拷贝的区别<a href="#3-浅拷贝和深拷贝的区别"><span>#</span></a></h2><p>假设原数据是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>context</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>step</span><span>:</span><span> </span><span>1</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>data</span><span>:</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>items</span><span>:</span><span> [</span><span>"A"</span><span>, </span><span>"B"</span><span>]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>3.1 浅拷贝<a href="#31-浅拷贝"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>copy</span><span> </span><span>=</span><span> { </span><span>...</span><span><span>context</span><span> };</span></span></div></div><div><div><div>2</div></div><div><span><span>copy</span><span>.</span></span><span>data</span><span>.</span><span>items</span><span>.</span><span>push</span><span>(</span><span>"C"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果可能是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>context</span><span>.</span></span><span>data</span><span>.</span><span>items</span></div></div><div><div><div>2</div></div><div><span>// ["A", "B", "C"]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>虽然修改的是 <code>copy</code>，但原数据也变了，因为嵌套的 <code>data</code> 和 <code>items</code> 仍然共享。</p></section><section><h3>3.2 深拷贝<a href="#32-深拷贝"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>copy</span><span> </span><span>=</span><span> </span><span>structuredClone</span><span><span>(</span><span>context</span><span>);</span></span></div></div><div><div><div>2</div></div><div><span><span>copy</span><span>.</span></span><span>data</span><span>.</span><span>items</span><span>.</span><span>push</span><span>(</span><span>"C"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>context</span><span>.</span></span><span>data</span><span>.</span><span>items</span></div></div><div><div><div>2</div></div><div><span>// ["A", "B"]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>copy</span><span>.</span></span><span>data</span><span>.</span><span>items</span></div></div><div><div><div>5</div></div><div><span>// ["A", "B", "C"]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>二者已经完全独立。</p>

<table><thead><tr><th>对比</th><th>浅拷贝</th><th>深拷贝</th></tr></thead><tbody><tr><td>最外层对象</td><td>独立</td><td>独立</td></tr><tr><td>嵌套对象和数组</td><td>通常共享</td><td>独立</td></tr><tr><td>修改嵌套数据</td><td>可能影响原数据</td><td>不影响原数据</td></tr><tr><td>性能和内存开销</td><td>较小</td><td>较大</td></tr><tr><td>适合情况</td><td>数据严格只读或结构简单</td><td>任务确实需要修改副本</td></tr></tbody></table></section></section>
<section><h2>4. 为什么“浅拷贝 + 只读”也可以安全？<a href="#4-为什么浅拷贝--只读也可以安全"><span>#</span></a></h2><p>这是我们在<a href="/posts/agent-01/">步骤并行化</a>里提到过的做法。虽然浅拷贝仍然共享嵌套对象，但只要所有并行任务都遵守：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>可以读取，不可以修改</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>就不会发生并发覆盖。例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 安全：只读取</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>count</span><span> </span><span>=</span><span><span> </span><span>localContext</span><span>.</span></span><span>searchResults</span><span>.</span><span>length</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// 不安全：修改了共享的嵌套数组</span></div></div><div><div><div>5</div></div><div><span><span>localContext</span><span>.</span></span><span>searchResults</span><span>.</span><span>push</span><span><span>(</span><span>newResult</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以常见设计是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>前序数据：不可变，只允许读取</span></div></div><div><div><div>2</div></div><div><span>并行步骤的新结果：通过 return 单独返回</span></div></div><div><div><div>3</div></div><div><span>调度器：等待步骤结束后统一合并</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这通常比给每个任务深拷贝一份大型 Context 更节省内存和时间。</p><p>总之：</p><blockquote><p><strong>前序数据是并行步骤开始前，上游已经确定的共享输入；浅拷贝只复制外壳，内部数据仍可能共享；深拷贝连内部嵌套数据也复制，因此副本之间更加独立，但成本也更高。</strong></p></blockquote></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/sdd-02/</id>
      <title type="text">SDD 工作流</title>
      <published>2026-08-23T00:00:00.000Z</published>
      <updated>2026-08-23T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/sdd-02/"/>
      <summary type="text">简要概括一下 sdd-riper 的工作流程</summary>
      <content type="html"><![CDATA[<p>这是 <code>sdd-riper-one</code> 中给出的推荐工作流：</p>
<ul>
<li>标准流（中大型任务）：
<ul>
<li><code>create_codemap -&gt; build_context_bundle -&gt; sdd_bootstrap -&gt; Research -&gt; (Innovate, 可选) -&gt; Plan -&gt; Execute -&gt; Review</code></li>
</ul>
</li>
<li>快速流（小任务/需求模糊）：
<ul>
<li><code>sdd_bootstrap -&gt; （按需补）create_codemap/build_context_bundle -&gt; Research -&gt; Plan -&gt; Execute -&gt; Review</code></li>
</ul>
</li>
<li>关键门禁：
<ul>
<li>首版 spec 落盘前，不进入后续实现阶段。</li>
<li>最终目标和当前任务单元未写清前，不进入 Plan。</li>
<li>未收到精确字样 <code>Plan Approved</code>，禁止进入 <code>Execute</code>。</li>
<li><code>Review</code> 不通过，必须回到 <code>Research/Plan</code> 闭环修正。</li>
</ul>
</li>
</ul>
<blockquote><p>这里的工作流是指我需要按这个流程去和 Agent 对话，还是已经内置到了这套skills中？</p></blockquote>
<section><h2>1. 关于该工作流的角色<a href="#1-关于该工作流的角色"><span>#</span></a></h2><p><strong>那条推荐流程，既是「内置在 skill 里的行为协议」，也需要你在对话里「启用并配合」。</strong></p><p>它不是像 CI pipeline 那样自动跑的后台程序，而是一套写进 <code>SKILL.md</code> 的 <strong>Agent 行为约束 + 人机协作节奏</strong>。Agent 读到 skill 后会按阶段推进；你在关键节点要 <strong>批准、纠偏、说 <code>Plan Approved</code></strong>，流程才会继续。</p><p>README 里也写得很直白：</p><blockquote><p>更推荐把 <code>sdd-riper-one</code> 当成一种<strong>标准控盘工作方式</strong>来使用，而不是当成一组必须死记硬背的显式命令。<br />
显式命令是能力接口，不是唯一入口。</p></blockquote><p>所以：我们<strong>不必逐字背 <code>create_codemap -&gt; build_context_bundle -&gt; ...</code> 这条链</strong>；更重要的是理解「先 spec、先批准、再改代码」这套习惯。命令名只是 skill 内部给 Agent 用的「动作标签」。</p><hr /></section>
<section><h2>2. 这套 skills 到底是什么？<a href="#2-这套-skills-到底是什么"><span>#</span></a></h2><p>可以把它理解成 <strong>给 Agent 看的操作手册</strong>，不是可执行程序：</p>

<table><thead><tr><th>组件</th><th>作用</th></tr></thead><tbody><tr><td><code>SKILL.md</code></td><td>运行内核：硬规则、阶段、门禁</td></tr><tr><td><code>references/</code></td><td>模板、诊断、按需加载的长规则</td></tr><tr><td><code>examples/</code></td><td>脱敏后的 spec / codemap 样例</td></tr><tr><td><code>AGENTS.md</code></td><td>项目级路由：默认用哪个 skill</td></tr></tbody></table><p>Agent 被 <strong>显式点名</strong>（如「请使用 sdd-riper-one」）或通过 <code>AGENTS.md</code> 路由后，会读这些 Markdown，并按其中规则行动。</p><hr /></section>
<section><h2>3. 初学者怎么用？（最短路径）<a href="#3-初学者怎么用最短路径"><span>#</span></a></h2><section><h3>第一步：选 skill<a href="#第一步选-skill"><span>#</span></a></h3>

<table><thead><tr><th>面临的情况</th><th>用哪个</th></tr></thead><tbody><tr><td>日常开发、任务已基本清楚、想低打扰</td><td><code>sdd-riper-one-light</code>（<strong>默认</strong>）</td></tr><tr><td>需求乱、新手、高风险、要审计留痕</td><td><code>sdd-riper-one</code></td></tr><tr><td>陌生/大型代码库</td><td>先 <code>codemap</code>，再进上面两个之一</td></tr><tr><td>长对话要交接</td><td><code>new-chat-ready</code></td></tr></tbody></table></section><section><h3>第二步：用「工作方式模板」开对话<a href="#第二步用工作方式模板开对话"><span>#</span></a></h3><p>不必先记 RIPER 各阶段名。直接这样开：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请使用 sdd-riper-one-light 处理这个任务，先不要直接改文件。</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>请先给我：</span></div></div><div><div><div>4</div></div><div><span>- 你对任务的理解</span></div></div><div><div><div>5</div></div><div><span>- 本轮核心目标</span></div></div><div><div><div>6</div></div><div><span>- 最小 spec / summary</span></div></div><div><div><div>7</div></div><div><span>- Done Contract：什么算完成，由什么证明</span></div></div><div><div><div>8</div></div><div><span>- 下一步动作</span></div></div><div><div><div>9</div></div><div><span>- 风险</span></div></div><div><div><div>10</div></div><div><span>- 验证方式</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>我批准后再执行。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果用重型版，把 <code>sdd-riper-one-light</code> 换成 <code>sdd-riper-one</code> 即可。</p></section><section><h3>第三步：你只需做三件事<a href="#第三步你只需做三件事"><span>#</span></a></h3><ol>
<li><strong>给任务时带上</strong>：目标、范围、约束、验证方式（哪怕很粗）</li>
<li><strong>在 checkpoint 处审批</strong>：Agent 给出 spec/plan 后，你看懂再批准；重型版进 Execute 前需回复 <strong><code>Plan Approved</code></strong></li>
<li><strong>用证据验收</strong>：测试、日志、手测通过才算 done，不靠 Agent 自己说「完成了」</li>
</ol><hr /></section></section>
<section><h2>4. 那条工作流具体指什么？<a href="#4-那条工作流具体指什么"><span>#</span></a></h2><p>这段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>标准流：create_codemap -&gt; build_context_bundle -&gt; sdd_bootstrap</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>-&gt; Research -&gt; (Innovate) -&gt; Plan -&gt; Execute -&gt; Review</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以拆成两层理解：</p><section><h3>4.1 对 Agent：内置的行为协议<a href="#41-对-agent内置的行为协议"><span>#</span></a></h3><ul>
<li><strong><code>create_codemap</code> / <code>build_context_bundle</code> / <code>sdd_bootstrap</code></strong>：skill 里定义的 <strong>Pre-Research 动作名</strong>，Agent 在 Research 前按需执行（建代码索引、汇总需求、产出首版 spec）</li>
<li><strong><code>Research → Plan → Execute → Review</code></strong>：RIPER 阶段状态机，写在 <code>SKILL.md</code> 里，Agent 应遵守</li>
<li><strong>门禁</strong>（如「没 <code>Plan Approved</code> 不改代码」）也在 skill 里，Agent 读到就应阻塞</li>
</ul></section><section><h3>4.2 对你：协作节奏，不是你要手动执行的脚本<a href="#42-对你协作节奏不是你要手动执行的脚本"><span>#</span></a></h3><p>你 <strong>不需要</strong> 每轮输入 <code>create_codemap</code> 或 <code>sdd_bootstrap</code>。口述需求、贴 PRD、说「先收敛任务别改代码」就够了，Agent 会按 skill 自行决定何时建 codemap、何时写 spec。</p><p>你 <strong>需要</strong> 参与的是人类侧的控制点：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>首版 spec 落盘前     → 不进入实现（Agent 应停，你审 spec）</span></div></div><div><div><div>2</div></div><div><span>目标/任务单元未写清 → 不进入 Plan（你补充或纠正）</span></div></div><div><div><div>3</div></div><div><span>未收到 Plan Approved → 禁止 Execute（你明确批准）</span></div></div><div><div><div>4</div></div><div><span>Review 不通过        → 回到 Research/Plan（你决定是否继续）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section></section>
<section><h2>5. Light vs One：流程差异<a href="#5-light-vs-one流程差异"><span>#</span></a></h2>

<table><thead><tr><th></th><th><code>sdd-riper-one-light</code></th><th><code>sdd-riper-one</code></th></tr></thead><tbody><tr><td>流程形态</td><td>checkpoint 驱动，不强制展开 RIPER 文本</td><td>显式 RIPER 阶段 + 更完整 spec</td></tr><tr><td>你的负担</td><td>较低：批准 + 验收即可</td><td>较高：更多 checkpoint、<code>Plan Approved</code> 字面批准</td></tr><tr><td>适合</td><td>熟练用户、任务已较清楚</td><td>SDD 初学者、复杂/高风险任务</td></tr></tbody></table><p>作为 SDD 初学者，若任务不小或边界不清，建议<strong>先用 <code>sdd-riper-one</code> 练习惯</strong>；熟练后可切 <code>light</code> 减负。</p><hr /></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><ul>
<li><strong>Skills = Agent 的行为协议文档</strong>，不是自动运行的引擎</li>
<li><strong>工作流 = Agent 内置应遵循的阶段 + 你配合的审批节奏</strong></li>
<li><strong>你不必背命令链</strong>；用启动模板开对话，在 spec/plan 处批准，用证据验收即可</li>
<li><strong>重型版 Execute 前必须你回复 <code>Plan Approved</code></strong>，这是少数需要你「精确字样」的门禁之一</li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/sdd-03/</id>
      <title type="text">sdd-riper 概览</title>
      <published>2026-08-21T00:00:00.000Z</published>
      <updated>2026-08-21T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/sdd-03/"/>
      <summary type="text">介绍 sdd-riper 项目</summary>
      <content type="html"><![CDATA[<section><h1>1. 项目概览<a href="#1-项目概览"><span>#</span></a></h1><p><code>sdd-riper</code> 是一套给 AI coding agent 使用的 <strong>Harness / 工作流技能包</strong>。它的核心目标是：让模型负责探索、实现和推进任务，让人类控制目标、边界、权限、checkpoint、验证证据和最终验收。</p><p>该项目主要包含 4 个 skills：</p><ul>
<li>
<p><code>sdd-riper-one-light</code><br />
日常默认入口。适合普通 coding、文档、bugfix、轻量重构。它强调低打扰控盘：先复述目标、形成 micro-spec、checkpoint、获批后执行、验证后 reverse sync。</p>
</li>
<li>
<p><code>sdd-riper-one</code><br />
重型版本。适合高风险、多文件、跨模块、审计、训练、交接密集任务。它会更严格地走 <code>Research -&gt; Plan -&gt; Execute -&gt; Review</code>，并要求更完整的 spec、checkpoint、验证和留痕。</p>
</li>
<li>
<p><code>codemap</code><br />
代码地形索引 skill。用于陌生代码库、大模块、遗留系统或跨仓任务。它不负责直接实现功能，而是帮 agent 找到入口、调用链、风险点、验证入口，让后续任务不用把整个代码库塞进上下文。</p>
</li>
<li>
<p><code>new-chat-ready</code><br />
交接与恢复 skill。用于新对话、上下文压缩、handoff、长任务暂停、恢复旧会话，以及把可复用知识同步到项目文档候选中。</p>
</li>
</ul><p>工作流概览：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>日常任务</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>-&gt; sdd-riper-one-light</span></div></div><div><div><div>3</div></div><div><span><span>      </span></span><span>-&gt; 必要时引用 new-chat-ready 做 handoff / resume</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>-&gt; 陌生代码时可先让用户或 agent 切到 codemap</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>复杂/高风险任务</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>-&gt; sdd-riper-one</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>-&gt; Pre-Research 阶段可调用/采用 codemap 建代码地图</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>-&gt; 执行闭环后可调用 new-chat-ready 做交接或恢复包</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>-&gt; 任务清晰后，风格上可降噪接近 light，但门禁仍更严格</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>陌生代码任务</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>-&gt; codemap</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>-&gt; 产出代码地形索引</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>-&gt; 供 sdd-riper-one-light 或 sdd-riper-one 后续读取</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>长对话/换会话/暂停</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>-&gt; new-chat-ready</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>-&gt; 读取当前 spec、codemap、验证结果、git 状态等</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>-&gt; 产出 handoff 和下一轮 prompt</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h1>2. <code>sdd-riper-one</code> 概览<a href="#2-sdd-riper-one-概览"><span>#</span></a></h1><p><code>sdd-riper-one</code> 的完整工作流分为两个阶段：</p><p><strong>1. Pre-research：进入正式开发前的上下文准备</strong></p><p>这一阶段主要是降低混沌，不着急改代码。主要环节包括：</p><ul>
<li><code>create_codemap</code>：
<ul>
<li>建立代码地形图，搞清楚入口、模块边界、调用链、风险点、验证入口。</li>
</ul>
</li>
<li><code>build_context_bundle</code>：
<ul>
<li>整理需求文档、聊天记录、日志、截图等输入，压缩成可研究上下文。</li>
</ul>
</li>
<li><code>sdd_bootstrap</code>：
<ul>
<li>启动 SDD/RIPER 流程，形成第一版 spec，明确目标、边界、当前任务单元和验证方式。</li>
</ul>
</li>
</ul><hr /><p><strong>2. RIPER：正式执行闭环</strong></p><p>核心阶段为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Research -&gt; Innovate (可选) -&gt; Plan -&gt; Execute -&gt; Review</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>Research</code>：
<ul>
<li>研究需求、代码、约束和风险，补齐事实，不急着定方案。</li>
</ul>
</li>
<li><code>Innovate</code>：
<ul>
<li>当方案不唯一或风险较高时，提出 2-3 个可选方案并比较取舍。</li>
</ul>
</li>
<li><code>Plan</code>：
<ul>
<li>生成可执行计划，通常要写清文件路径、改动点、检查清单、验证方式。</li>
</ul>
</li>
<li><code>Execute</code>：
<ul>
<li>只有用户明确批准后才进入执行。项目里强调精确门禁：没有 <code>Plan Approved</code>，不进入实现。</li>
</ul>
</li>
<li><code>Review</code>：
<ul>
<li>执行后对照 spec、plan、实际改动和验证结果做复核；如果不通过，要回到 Research 或 Plan 修正。</li>
</ul>
</li>
</ul><hr /><p>整体的流程就是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>澄清最终目标</span></div></div><div><div><div>2</div></div><div><span>-&gt; 判断当前任务是否是最小可执行单元</span></div></div><div><div><div>3</div></div><div><span>-&gt; 建 codemap / context bundle（按需）</span></div></div><div><div><div>4</div></div><div><span>-&gt; 生成或更新 spec</span></div></div><div><div><div>5</div></div><div><span>-&gt; Research 补事实</span></div></div><div><div><div>6</div></div><div><span>-&gt; 可选 Innovate 比方案</span></div></div><div><div><div>7</div></div><div><span>-&gt; Plan 写执行清单</span></div></div><div><div><div>8</div></div><div><span>-&gt; 等用户 Plan Approved</span></div></div><div><div><div>9</div></div><div><span>-&gt; Execute 修改</span></div></div><div><div><div>10</div></div><div><span>-&gt; Validation 验证</span></div></div><div><div><div>11</div></div><div><span>-&gt; Review 复核</span></div></div><div><div><div>12</div></div><div><span>-&gt; Reverse Sync 回写 spec</span></div></div><div><div><div>13</div></div><div><span>-&gt; 必要时 handoff / archive / new-chat-ready</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>2.1 spec 和 plan 的区别<a href="#21-spec-和-plan-的区别"><span>#</span></a></h2><p>两者的区别可以简单理解为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>spec = 任务真相源</span></div></div><div><div><div>2</div></div><div><span>plan = 本轮执行清单</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>spec 回答的是：<strong>我们到底要做什么，为什么做，边界在哪里，什么算完成</strong>。</p><p>也就是说，spec 是一个偏“持续状态”的文档，它会随着任务推进不断更新，是长对话、换人、换 chat、复盘时的真相源。</p><hr /><p>而 plan 回答的是：<strong>接下来这一轮具体怎么改</strong>。</p><p>所以，plan 是偏“执行动作”的清单，它服务于即将发生的这一次实现。</p><p>下面是一个简单的示例，展示二者面对同一任务时记录内容的区别：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Spec:</span></div></div><div><div><div>2</div></div><div><span>用户登录失败时，需要区分密码错误、账号冻结、验证码过期。</span></div></div><div><div><div>3</div></div><div><span>不能改变现有成功登录流程。</span></div></div><div><div><div>4</div></div><div><span>完成证据是相关单测通过，并手测三种失败场景。</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Plan:</span></div></div><div><div><div>7</div></div><div><span>1. 修改 auth/error.ts，新增错误码映射。</span></div></div><div><div><div>8</div></div><div><span>2. 修改 login/service.ts，让冻结账号分支返回 ACCOUNT_FROZEN。</span></div></div><div><div><div>9</div></div><div><span>3. 修改 login/ui.tsx，按错误码展示提示文案。</span></div></div><div><div><div>10</div></div><div><span>4. 补 login.service.test.ts 三个失败场景。</span></div></div><div><div><div>11</div></div><div><span>5. 运行 auth/login 相关测试。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h1>3. <code>sdd-riper-one-light</code> 概览<a href="#3-sdd-riper-one-light-概览"><span>#</span></a></h1><p>作为 <code>sdd-riper-one</code> 的轻量实现，它不强调完整的 <code>Research -&gt; Plan -&gt; Execute -&gt; Review</code> 阶段展开，而是强调 <strong>checkpoint-driven</strong>：用少量关键校准点保证 Agent 不跑偏。</p><p>下面是 <code>sdd-riper-one-light</code> 大致的完整工作流：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>理解任务</span></div></div><div><div><div>2</div></div><div><span>-&gt; 轻量风险判断</span></div></div><div><div><div>3</div></div><div><span>-&gt; 建立 micro-spec / 最小 spec</span></div></div><div><div><div>4</div></div><div><span>-&gt; 写清 Done Contract</span></div></div><div><div><div>5</div></div><div><span>-&gt; 输出 checkpoint</span></div></div><div><div><div>6</div></div><div><span>-&gt; 等待批准</span></div></div><div><div><div>7</div></div><div><span>-&gt; 执行</span></div></div><div><div><div>8</div></div><div><span>-&gt; 验证</span></div></div><div><div><div>9</div></div><div><span>-&gt; Reverse Sync</span></div></div><div><div><div>10</div></div><div><span>-&gt; 必要时留下 resume / handoff</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>并且内置了几个档位：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>zero      纯机械小改动，可跳过正式 spec</span></div></div><div><div><div>2</div></div><div><span>fast      1-3 句 micro-spec</span></div></div><div><div><div>3</div></div><div><span>standard  默认模式，适合普通多文件任务</span></div></div><div><div><div>4</div></div><div><span>deep      复杂但还没重到必须用 sdd-riper-one</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>1. 理解任务</strong></p><p>Agent 先用自己的话复述任务，确认目标、范围和已知约束。</p><p>官方参考 prompt：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请使用 sdd-riper-one-light 处理这个任务。</span></div></div><div><div><div>2</div></div><div><span>先不要直接改文件。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>请先给我：</span></div></div><div><div><div>5</div></div><div><span>- 你对任务的理解</span></div></div><div><div><div>6</div></div><div><span>- 本轮核心目标</span></div></div><div><div><div>7</div></div><div><span>- 最小 spec / summary</span></div></div><div><div><div>8</div></div><div><span>- Done Contract：什么算完成，由什么证明</span></div></div><div><div><div>9</div></div><div><span>- 下一步动作</span></div></div><div><div><div>10</div></div><div><span>- 风险</span></div></div><div><div><div>11</div></div><div><span>- 验证方式</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>我批准后再执行。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>遇到陌生代码时，先建 codemap：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请使用 codemap。</span></div></div><div><div><div>2</div></div><div><span>先创建 feature 级或 project 级代码地形索引，再进入计划。</span></div></div><div><div><div>3</div></div><div><span>重点关注入口、调用链、风险点、验证入口，以及下一步最小回读代码片段。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>2. 轻量风险判断</strong></p><p>判断任务是否：</p><ul>
<li>过大</li>
<li>边界不清</li>
<li>高风险</li>
</ul><p>如果明显复杂，就建议升级到 <code>deep</code> 或切换到 <code>sdd-riper-one</code>.</p><p><strong>3. 建立 <code>mico-spec</code> 或最小 <code>spec</code></strong></p><p>一般包含：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Goal</span></div></div><div><div><div>2</div></div><div><span>Context</span></div></div><div><div><div>3</div></div><div><span>Boundary</span></div></div><div><div><div>4</div></div><div><span>Validation</span></div></div><div><div><div>5</div></div><div><span>Risk</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>4. 写清 Done Contract</strong></p><p>明确什么算完成，以及由什么证据证明完成。比如测试通过、日志符合预期、截图、人工验收等。</p><p><strong>5. 输出 <code>Checkpoint</code></strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>当前目标是什么</span></div></div><div><div><div>2</div></div><div><span>已确认什么</span></div></div><div><div><div>3</div></div><div><span>下一步做什么</span></div></div><div><div><div>4</div></div><div><span>风险是什么</span></div></div><div><div><div>5</div></div><div><span>怎么验证</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>6. 等待批准</strong></p><p>硬门禁，未获用户明确批准前，不进入实现或高影响变更。</p><p><strong>7. 执行</strong></p><p>获批后执行改动。light 默认相信强模型能自行拆分局部步骤，所以不会强制写很重的阶段计划。</p><p><strong>8. 验证</strong></p><p>跑最小有意义的验证，完成必须有证据。</p><p><strong>9. Reverse Sync</strong></p><p>把实际改动、偏差、验证结果、剩余风险回写到 spec / summary / handoff 中。</p><p><strong>10. <code>Project Sync Scan</code></strong></p><p>收尾时判断有没有稳定、可复用的项目知识需要沉淀。没有就说明没有。</p><p><strong>11. <code>Resume Ready</code></strong></p><p>如果任务长、要暂停或换对话，留下最小恢复锚点；必要时转给 <code>new-chat-ready</code>.</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/sdd-01/</id>
      <title type="text">SDD 初探</title>
      <published>2026-08-14T00:00:00.000Z</published>
      <updated>2026-08-14T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/sdd-01/"/>
      <summary type="text">介绍什么是 SDD</summary>
      <content type="html"><![CDATA[<blockquote><p>介绍 SDD 的基本概念。</p></blockquote>
<section><h1>1. 什么是 SDD<a href="#1-什么是-sdd"><span>#</span></a></h1><p>Spec-Driven Development (SDD) ，规范驱动开发，是一种以 “规范文档” 为核心的工程方法论。它指的是在利用 AI 编写代码前，首先写一份规范（<code>spec.md</code>），即文档先行，作为开发者与 AI 共同的事实来源。</p><p>而关于具体的定义，GitHub 和 Tessel 似乎有些许不同的看法：</p><p>GitHub:</p><blockquote><p>在这个新世界中，维护软件意味着演进规格。…… 开发的通用语言（lingua franca）转移到了更高的层次，代码是最后一英里（last-mile）的方法。</p></blockquote><p>Tessl:</p><blockquote><p>一种开发方法，其中规格（specs）——而非代码——是主要产物（artifact）。规格用结构化、可测试的语言描述意图，AI 代理（agents）生成代码来匹配它们。</p></blockquote><p>Birgitta 认为，实际上 SDD 存在多个实现层次：</p><ul>
<li>Spec-first（规格优先）：先编写一个经过深思熟虑的 spec，然后在 AI 辅助开发工作流中用于当前任务。</li>
<li>Spec-anchored（规格锚定）：任务完成后仍保留 spec，继续用于相应功能的演进和维护。</li>
<li>Spec-as-source（规格即源码）：随着时间的推移，spec 是主要的源文件，只有 spec 由人类编辑，人类从不触碰代码。</li>
</ul><p><img src="https://i-blog.csdnimg.cn/direct/667b1691dd2a44fdac26b4407228ad1f.png" alt="" /></p><p>如今，就像我们开头说的那样，大多数的 SDD 方法和定义都是 Spec-first 的模式。不过，并非所有都力求成为 spec-anchored 或 spec-as-source. 根据 Birgitta 的观察，随着时间的推移，spec 维护策略应该是什么，往往被模糊处理或完全开放。</p><section><h2>1.1 什么是 spec<a href="#11-什么是-spec"><span>#</span></a></h2><p>关于究竟什么是 spec，目前也并没有一个通用的定义。一个可能被大多数人接受的共识是：spec 类似一个产品需求文档（Product Requirements Document）。</p><p>Birgitta 试图给出一个明确的定义。在她看来：</p><blockquote><p>spec 是一个结构化的、面向行为的产物（artifact）——或一组相关的产物——用自然语言编写，表达软件功能并作为 AI 编码代理（AI coding agents）的指导。每种 spec-driven development 的变体都定义了它们对 spec 结构、详细程度以及这些产物如何在项目中组织的方法。</p></blockquote><p>从这个角度来看，spec 与我们的 codebase 中其他更通用的上下文文档之间存在区别。它们的区别在于：</p><ul>
<li>通用的上下文文档与代码库中的所有 AI 编码会话都相关；</li>
<li>而 specs 只与实际上创建或更改特定功能的任务相关。</li>
</ul></section></section>
<section><h1>2. SDD 的困境<a href="#2-sdd-的困境"><span>#</span></a></h1><p>Birgitta 尝试了三个工具：Kiro, Spec-kit 以及 Tessl Framework. 她结合自己的使用体验，提出了一些观察与问题。</p><section><h2>2.1 工作流与规模<a href="#21-工作流与规模"><span>#</span></a></h2><p>Kiro 和 spec-kit 各自提供一个有明确观点的工作流，但 Birgitta 认为，它们并不适合大多数现实的 coding 问题，尤其是面对不同的问题规模，这些工作流是否具有普遍性要打上一个大大的问号。</p><p>例如，如果用 Kiro 去修复一个小 bug，一个形象的比喻是“用大锤砸坚果”。Specs 将这个小 bug 变成了 4 个 user stories 与 16 个验收标准。</p><p>在使用 spec-kit 时，从 spec-kit 采取的步骤数量，以及为开发者创建的要审查的 markdown 文件数量来看，也明显出现了“过度杀伤”的问题。Spec-kit 是比 Kiro 还要复杂的一个工作流。</p></section><section><h2>2.2 审查 markdown<a href="#22-审查-markdown"><span>#</span></a></h2><blockquote><p>正如刚才提到的，正如你在上面的工具描述中看到的那样，spec-kit 为我创建了大量的 markdown 文件来审查。它们是重复的，彼此之间以及与已经存在的代码之间都是如此。有些已经包含了代码。总的来说，它们非常冗长，审查起来很乏味。</p></blockquote><blockquote><p>老实说，我宁愿审查代码也不愿审查所有这些 markdown 文件。一个有效的 SDD 工具必须提供非常好的 spec 审查体验。</p></blockquote></section><section><h2>2.3 虚假的控制感<a href="#23-虚假的控制感"><span>#</span></a></h2><p>Birgitta 观察到，即使有我们上面提到的生成的所有这些文件、模板、提示、工作流和检查清单，她也经常看到 Agent 最终没有遵循所有指令。更大的上下文窗口并没有理所应当地带来 AI 理解其中所有内容的正确性。</p><p>这不仅仅体现在 Agent 没能遵循所有指令，也体现在它们因为过于热切地遵循指令而做得过火。</p><p>在过去，对于保持对所构建内容控制，我们的通常做法是使用小的、迭代的步骤。但小的工作步骤似乎又与 SDD 的理念相违背：</p><blockquote><p>我非常怀疑大量的前期 spec 设计是否是一个好主意，特别是当它过于冗长时。</p></blockquote></section><section><h2>2.4 有效分离功能与技术<a href="#24-有效分离功能与技术"><span>#</span></a></h2><p>在 SDD 中，有意分离功能规格（functional spec）和技术实现（technical implementation）是一个常见的想法。一个潜在的追求是，最终我们可以让 AI 填充所有的解决方案和细节，并使用相同的规格切换到不同的技术栈。</p><p>但在实践中，一个问题在于我们常常困惑何时应该保持在功能级别，何时应该添加技术细节，尤其是教程与文档在这方面也不完全一致，似乎对 ”纯功能（purely functional）“ 的真正含义有不同的解释。</p></section><section><h2>2.5 目标用户<a href="#25-目标用户"><span>#</span></a></h2><p>许多 spec-driven development 工具的演示和教程包括定义产品和功能目标之类的内容，它们甚至纳入了”用户故事（user story）“等术语。这里的想法可能是将 AI 作为跨技能（cross-skilling）的推动者，让开发人员更多地参与需求分析？或者让开发人员与产品人员配对当他们处理这个工作流时？然而，这些都没有明确说明，它被呈现为理所当然的，即开发人员将完成所有这些分析。</p><p>在这种情况下，Birgitta 问道，SDD 旨在解决什么规模和问题类型？可能不是针对仍然非常不明确的大型功能，因为那肯定需要更专业的产品和需求技能，以及许多其他步骤，如研究和利益相关者参与？</p></section><section><h2>2.6 出路<a href="#26-出路"><span>#</span></a></h2><p><a href="https://zhuanlan.zhihu.com/p/1993009461451831150" target="_blank">这篇长文</a>讲述了原作者从踩坑规范驱动工具，到借鉴 Anthropic 多 Agent 协作架构、融合上下文工程与复合工程理念，最终实现边际成本递减、知识持续复利的完整历程。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-07/</id>
      <title type="text">为 Agent 添加 skill 支持</title>
      <published>2026-08-11T00:00:00.000Z</published>
      <updated>2026-08-11T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-07/"/>
      <summary type="text">介绍如何对你的 Agent 或开发工具添加 skill 支持</summary>
      <content type="html"><![CDATA[<blockquote><p>为 AI 代理或开发工具添加 Agent Skills 支持的指南。</p></blockquote>
<p>如今的主流 Agent 都已集成了 skill 支持。对于大部分开发者而言，掌握如何创建一个 skill 便以足够。虽然如此，了解 Agent 如何调用 skill 有助于我们更好地理解 skill 的相关机制。</p>
<p>本指南将逐步介绍如何为 AI 代理或开发工具添加 Agent Skills 支持。它涵盖了完整的生命周期：发现技能、向模型告知技能、将技能内容加载到上下文中，以及长期保持这些内容的有效性。</p>
<p>无论你的代理架构如何，核心集成方式都是相同的。实现细节因以下两个因素而异：</p>
<ul>
<li><strong>技能存放在哪里？</strong> 本地运行的代理可以扫描用户文件系统中的技能目录。云托管或沙箱化的代理则需要替代的发现机制——API、远程注册表或捆绑的资源。</li>
<li><strong>模型如何访问技能内容？</strong> 如果模型具有文件读取能力，它可以直接读取 <code>SKILL.md</code> 文件。否则，你需要提供一个专用工具，或以编程方式将技能内容注入提示中。</li>
</ul>
<p>本指南会在这些差异出现时加以说明。你不需要支持所有场景——请遵循适合你代理的路径。</p>
<p><strong>前置条件</strong>：熟悉 <a href="/posts/skill-02/">agentskills.io 规范</a> ，该规范定义了 <code>SKILL.md</code> 文件格式、前置元数据字段和目录约定。</p>
<section><h2>核心原则：渐进式披露<a href="#核心原则渐进式披露"><span>#</span></a></h2><p>每个兼容技能的代理都遵循相同的三层加载策略：</p>

<table><thead><tr><th>层级</th><th>加载内容</th><th>时机</th><th>Token 成本</th></tr></thead><tbody><tr><td>1. 目录</td><td>名称 + 描述</td><td>会话启动</td><td>每个技能约 50-100 token</td></tr><tr><td>2. 指令</td><td>完整 <code>SKILL.md</code> 正文</td><td>技能被激活时</td><td>&lt; 5000 token（推荐）</td></tr><tr><td>3. 资源</td><td>脚本、参考文档、资源文件</td><td>指令引用它们时</td><td>不定</td></tr></tbody></table><p>模型从一开始就看到目录，因此它知道有哪些技能可用。当它判断某个技能相关时，就会加载完整的指令。如果这些指令引用了支持文件，模型会按需逐个加载它们。</p><p>这使得基础上下文保持小巧，同时让模型能在需要时访问专业知识。一个安装了 20 个技能的代理，不需要预先支付 20 套完整指令集的 token 成本——只需为在给定对话中实际使用的那些支付。</p></section>
<section><h2>步骤 1：发现技能<a href="#步骤-1发现技能"><span>#</span></a></h2><p>在会话启动时，找到所有可用技能并加载其元数据。</p><section><h3>1.1 扫描哪些位置<a href="#11-扫描哪些位置"><span>#</span></a></h3><p>扫描哪些目录取决于你的代理环境。大多数本地运行的代理至少扫描两个范围：</p><ul>
<li><strong>项目级</strong>（相对于工作目录）：特定于项目或仓库的技能。</li>
<li><strong>用户级</strong>（相对于主目录）：对给定用户在所有项目中可用的技能。</li>
</ul><p>其他范围也是可能的——例如，由管理员部署的组织级技能，或与代理本身捆绑的技能。正确的范围集合取决于你的代理部署模型。</p><p>在每个范围内，考虑同时扫描<strong>客户端特定目录</strong>和 <strong><code>.agents/skills/</code> 约定</strong>：</p>

<table><thead><tr><th>范围</th><th>路径</th><th>用途</th></tr></thead><tbody><tr><td>项目</td><td><code>&lt;project&gt;/.&lt;your-client&gt;/skills/</code></td><td>你的客户端的原生位置</td></tr><tr><td>项目</td><td><code>&lt;project&gt;/.agents/skills/</code></td><td>跨客户端互操作性</td></tr><tr><td>用户</td><td><code>~/.&lt;your-client&gt;/skills/</code></td><td>你的客户端的原生位置</td></tr><tr><td>用户</td><td><code>~/.agents/skills/</code></td><td>跨客户端互操作性</td></tr></tbody></table><p><code>.agents/skills/</code> 路径已成为跨客户端技能共享的广泛采用的约定。虽然 Agent Skills 规范没有强制规定技能目录必须放在哪里（它只定义了目录内部的内容），但扫描 <code>.agents/skills/</code> 意味着其他兼容客户端安装的技能会自动对你的客户端可见，反之亦然。</p><div><div><div></div><div>Info</div></div><div><p>一些实现还会扫描 <code>.claude/skills/</code>（项目级和用户级）以实现实用的兼容性，因为许多现有技能都安装在那里。其他可能的位置还包括 git 根目录的祖先目录（对 monorepo 有用）、<a href="https://specifications.freedesktop.org/basedir-spec/latest/" target="_blank">XDG</a> 配置目录，以及用户配置的路径。</p></div></div></section><section><h3>1.2 扫描什么<a href="#12-扫描什么"><span>#</span></a></h3><p>在每个技能目录中，查找<strong>包含名为 <code>SKILL.md</code> 文件的子目录</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>~/.agents/skills/</span></div></div><div><div><div>2</div></div><div><span>├── pdf-processing/</span></div></div><div><div><div>3</div></div><div><span>│   ├── SKILL.md          ← 被发现</span></div></div><div><div><div>4</div></div><div><span>│   └── scripts/</span></div></div><div><div><div>5</div></div><div><span>│       └── extract.py</span></div></div><div><div><div>6</div></div><div><span>├── data-analysis/</span></div></div><div><div><div>7</div></div><div><span>│   └── SKILL.md          ← 被发现</span></div></div><div><div><div>8</div></div><div><span>└── README.md             ← 被忽略（不是技能目录）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实用的扫描规则：</p><ul>
<li>跳过不会包含技能的目录，如 <code>.git/</code> 和 <code>node_modules/</code></li>
<li>可选择遵循 <code>.gitignore</code> 以避免扫描构建产物</li>
<li>设置合理的边界（例如，最大深度 4-6 层，最多 2000 个目录），以防止在大型目录树中失控扫描</li>
</ul></section><section><h3>1.3 处理名称冲突<a href="#13-处理名称冲突"><span>#</span></a></h3><p>当两个技能共享相同的 <code>name</code> 时，应用确定的优先级规则。</p><p>现有实现中的通用约定：<strong>项目级技能覆盖用户级技能。</strong></p><p>在同一范围内（例如，在 <code>&lt;project&gt;/.agents/skills/</code> 和 <code>&lt;project&gt;/.&lt;your-client&gt;/skills/</code> 下都找到了名为 <code>code-review</code> 的技能），选择先找到或后找到都可以——选定一种并保持一致。当发生冲突时记录警告，以便用户知道某个技能被遮蔽了。</p></section><section><h3>1.4 信任考量<a href="#14-信任考量"><span>#</span></a></h3><p>项目级技能来自正在处理的仓库，该仓库可能是不可信的（例如，新克隆的开源项目）。考虑在信任检查的基础上加载项目级技能——仅当用户已将项目文件夹标记为受信任时才加载它们。这可以防止不可信的仓库在代理上下文中静默注入指令。</p></section><section><h3>1.5 云托管和沙箱化代理<a href="#15-云托管和沙箱化代理"><span>#</span></a></h3><p>如果你的代理在容器中或远程服务器上运行，它将无法访问用户的本地文件系统。根据技能范围，发现需要以不同的方式进行：</p><ul>
<li><strong>项目级技能</strong>通常是最简单的情况。如果代理在克隆的仓库上操作（即使在沙箱内），项目级技能会随代码一起传输，并且可以从仓库的目录树中扫描。</li>
<li><strong>用户级和组织级技能</strong>在沙箱中不存在。你需要从外部源配置它们——例如，克隆配置仓库、通过代理设置接受技能 URL 或包，或让用户通过 Web UI 上传技能目录。</li>
<li><strong>内置技能</strong>可以作为静态资源打包在代理的部署产物中，使其在每个会话中都可用，无需外部获取。</li>
</ul><p>一旦技能对代理可用，生命周期的其余部分——解析、披露、激活——就以相同的方式工作。</p></section></section>
<section><h2>步骤 2：解析 <code>SKILL.md</code> 文件<a href="#步骤-2解析-skillmd-文件"><span>#</span></a></h2><p>对于每个被发现的 <code>SKILL.md</code>，提取元数据和正文内容。</p><section><h3>2.1 前置元数据提取<a href="#21-前置元数据提取"><span>#</span></a></h3><p>一个 <code>SKILL.md</code> 文件有两个部分：<code>---</code> 分隔符之间的 YAML 前置元数据，以及结束分隔符之后的 Markdown 正文。解析方法：</p><ol>
<li>找到文件开头的 <code>---</code> 及其后的结束 <code>---</code>。</li>
<li>解析两者之间的 YAML 块。提取 <code>name</code> 和 <code>description</code>（必需），以及任何可选字段。</li>
<li>结束 <code>---</code> 之后的所有内容（去除首尾空白）即为技能的正文内容。</li>
</ol><p>请参阅 <a href="agentskills.io%20%E8%A7%84%E8%8C%83.md">agentskills.io 规范</a> 了解完整的前置元数据字段列表及其约束。</p></section><section><h3>2.2 处理格式错误的 YAML<a href="#22-处理格式错误的-yaml"><span>#</span></a></h3><p>为其他客户端编写的技能文件可能包含技术上无效的 YAML，但其解析器恰好能接受。最常见的问题是包含冒号的未加引号的值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 技术上无效的 YAML——冒号破坏了解析</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>Use this skill when</span><span>: </span><span>the user asks about PDFs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>考虑一种回退机制，在重试前用引号包裹此类值或将其转换为 YAML 块标量。这能以最小成本提高跨客户端兼容性。</p></section><section><h3>2.3 宽松验证<a href="#23-宽松验证"><span>#</span></a></h3><p>对问题发出警告，但在可能的情况下仍然加载技能：</p><ul>
<li>名称与父目录名称不匹配 → 警告，仍然加载</li>
<li>名称超过 64 个字符 → 警告，仍然加载</li>
<li>描述缺失或为空 → 跳过该技能（描述对于披露至关重要），记录错误</li>
<li>YAML 完全无法解析 → 跳过该技能，记录错误</li>
</ul><p>记录诊断信息，以便向用户展示（在调试命令、日志文件或 UI 中），但不要因外观问题而阻止技能加载。</p><div><div><div></div><div>Info</div></div><div><p><a href="/posts/skill-02/">agentskills.io 规范</a> 对 <code>name</code> 字段定义了严格的约束（与父目录匹配、字符集、最大长度）。上面的宽松方法有意放宽了这些约束，以提高与为其他客户端编写的技能的兼容性。</p></div></div></section><section><h3>2.4 存储什么<a href="#24-存储什么"><span>#</span></a></h3><p>至少，每个技能记录需要三个字段：</p>

<table><thead><tr><th>字段</th><th>描述</th></tr></thead><tbody><tr><td><code>name</code></td><td>来自前置元数据</td></tr><tr><td><code>description</code></td><td>来自前置元数据</td></tr><tr><td><code>location</code></td><td><code>SKILL.md</code> 文件的绝对路径</td></tr></tbody></table><p>将这些存储在以 <code>name</code> 为键的内存映射中，以便在激活时快速查找。</p><p>你也可以在发现时存储<strong>正文</strong>（前置元数据之后的 Markdown 内容），或在激活时从 <code>location</code> 读取。存储它会使激活更快；在激活时读取它在总体上使用更少内存，并且能捕获两次激活之间技能文件的更改。</p><p>技能的<strong>基础目录</strong>（<code>location</code> 的父目录）稍后需要用于解析相对路径和枚举捆绑资源——需要时从 <code>location</code> 派生即可。</p></section></section>
<section><h2>步骤 3：向模型披露可用技能<a href="#步骤-3向模型披露可用技能"><span>#</span></a></h2><p>告诉模型存在哪些技能，但不加载其完整内容。这是渐进式披露的第 1 层。</p><section><h3>3.1 构建技能目录<a href="#31-构建技能目录"><span>#</span></a></h3><p>对于每个发现的技能，以适合你栈的任何结构化格式——XML、JSON 或项目符号列表——包含 <code>name</code>、<code>description</code>，以及可选的 <code>location</code>（<code>SKILL.md</code> 文件的路径）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>available_skills</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>&lt;</span><span>skill</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>name</span><span>&gt;pdf-processing&lt;/</span><span>name</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>description</span><span>&gt;Extract PDF text, fill forms, merge files. Use when handling PDFs.&lt;/</span><span>description</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>&lt;</span><span>location</span><span>&gt;/home/user/.agents/skills/pdf-processing/SKILL.md&lt;/</span><span>location</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>&lt;/</span><span>skill</span><span>&gt;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>&lt;</span><span>skill</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>&lt;</span><span>name</span><span>&gt;data-analysis&lt;/</span><span>name</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>&lt;</span><span>description</span><span>&gt;Analyze datasets, generate charts, and create summary reports.&lt;/</span><span>description</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>&lt;</span><span>location</span><span>&gt;/home/user/project/.agents/skills/data-analysis/SKILL.md&lt;/</span><span>location</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>&lt;/</span><span>skill</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span>&lt;/</span><span>available_skills</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>location</code> 字段有两个用途：它支持文件读取激活（参见步骤 4），并为模型提供解析技能正文中相对引用（如 <code>scripts/evaluate.py</code>）的基础路径。如果你的专用激活工具在其结果中提供了技能目录路径（参见步骤 4 中的结构化包装），则可以从目录中省略 <code>location</code>。否则，请包含它。</p><p>每个技能为目录增加大约 50-100 个 token。即使安装了数十个技能，目录仍然保持紧凑。</p></section><section><h3>3.2 将目录放在哪里<a href="#32-将目录放在哪里"><span>#</span></a></h3><p>两种常见方法：</p><p><strong>系统提示部分</strong>：将目录作为系统提示中的一个带标签部分添加，前面加上关于如何使用技能的简要说明。这是最简单的方法，适用于任何能访问文件读取工具的模型。</p><p><strong>工具描述</strong>：将目录嵌入专用技能激活工具的描述中（参见步骤 4）。这使系统提示保持干净，并将发现与激活自然耦合。</p><p>两种方法都有效。系统提示放置更简单且兼容性更广；当你拥有专用激活工具时，工具描述嵌入更干净。</p></section><section><h3>3.3 行为指令<a href="#33-行为指令"><span>#</span></a></h3><p>在目录旁边包含一个简短的指令块，告诉模型如何以及何时使用技能。措辞取决于你支持的激活机制（参见步骤 4）：</p><p><strong>如果模型通过读取文件来激活技能：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>以下技能为特定任务提供专门指令。</span></div></div><div><div><div>2</div></div><div><span>当任务匹配技能描述时，请先使用文件读取工具</span></div></div><div><div><div>3</div></div><div><span>加载所列位置的 SKILL.md，然后再继续。</span></div></div><div><div><div>4</div></div><div><span>当技能引用相对路径时，请相对于技能目录</span></div></div><div><div><div>5</div></div><div><span>（SKILL.md 的父目录）解析它们，并在工具调用中使用绝对路径。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>如果模型通过专用工具激活技能：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>以下技能为特定任务提供专门指令。</span></div></div><div><div><div>2</div></div><div><span>当任务匹配技能描述时，请调用 activate_skill 工具</span></div></div><div><div><div>3</div></div><div><span>并传入技能名称以加载其完整指令。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保持这些指令简洁。目标是告诉模型技能存在以及如何加载它们——技能内容本身在加载后会提供详细的指令。</p></section><section><h3>3.4 过滤<a href="#34-过滤"><span>#</span></a></h3><p>某些技能应从目录中排除。常见原因：</p><ul>
<li>用户在设置中禁用了该技能</li>
<li>权限系统拒绝访问该技能</li>
<li>该技能选择退出模型驱动的激活（例如，通过 <code>disable-model-invocation</code> 标志）</li>
</ul><p><strong>从目录中完全隐藏</strong>被过滤的技能，而不是列出它们并在激活时阻止。这可以防止模型浪费轮次尝试加载它无法使用的技能。</p></section><section><h3>3.5 当没有技能可用时<a href="#35-当没有技能可用时"><span>#</span></a></h3><p>如果没有发现技能，请完全省略目录和行为指令。不要显示空的 <code>&lt;available_skills/&gt;</code> 块，也不要注册没有有效选项的技能工具——这会混淆模型。</p></section></section>
<section><h2>步骤 4：激活技能<a href="#步骤-4激活技能"><span>#</span></a></h2><p>当模型或用户选择某个技能时，将完整指令传递到对话上下文中。这是渐进式披露的第 2 层。</p><section><h3>4.1 模型驱动的激活<a href="#41-模型驱动的激活"><span>#</span></a></h3><p>大多数实现依赖模型自身的判断作为激活机制，而不是实现代理侧的触发器匹配或关键词检测。模型读取目录（来自步骤 3），判断某个技能与当前任务相关，然后加载它。</p><p>两种实现模式：</p><p><strong>文件读取激活</strong>：模型使用其标准文件读取工具，并传入目录中的 <code>SKILL.md</code> 路径。无需特殊基础设施——代理现有的文件读取能力就足够了。模型将文件内容作为工具结果接收。当模型具有文件访问权限时，这是最简单的方法。</p><p><strong>专用工具激活</strong>：注册一个工具（例如 <code>activate_skill</code>），该工具接受技能名称并返回内容。当模型无法直接读取文件时，这是必需的；即使模型能读取，这也是可选的（但有用的）。相比原始文件读取的优势：</p><ul>
<li>控制返回的内容——例如，去除 YAML 前置元数据或保留它（参见下文模型接收什么）</li>
<li>将内容包装在结构化标签中，以便在上下文管理期间识别</li>
<li>在指令旁边列出捆绑资源（例如 <code>references/*</code>）</li>
<li>强制执行权限或提示用户同意</li>
<li>跟踪激活情况以进行分析</li>
</ul><div><div><div></div><div>Tip</div></div><div><p>如果你使用专用激活工具，请将 <code>name</code> 参数限制为有效技能名称的集合（例如，在工具模式中作为枚举）。这可以防止模型虚构不存在的技能名称。如果没有可用技能，则根本不要注册该工具。</p></div></div></section><section><h3>4.2 用户显式激活<a href="#42-用户显式激活"><span>#</span></a></h3><p>用户也应能直接激活技能，而无需等待模型决定。最常见的模式是<strong>斜杠命令或提及语法</strong>（<code>/skill-name</code> 或 <code>$skill-name</code>），由代理拦截。具体语法由你决定——关键思想是代理处理查找和注入，因此模型无需自行采取激活操作即可接收技能内容。</p><p>自动补全小部件（在用户输入时列出可用技能）也能使其可被发现。</p></section><section><h3>4.3 模型接收什么<a href="#43-模型接收什么"><span>#</span></a></h3><p>当技能被激活时，模型会接收技能的指令。关于具体内容，有两种选择：</p><p><strong>完整文件</strong>：模型看到完整的 <code>SKILL.md</code>，包括 YAML 前置元数据。这是文件读取激活的自然结果，模型读取原始文件。对于专用工具，这也是一个有效的选择。前置元数据可能包含在激活时有用的字段——例如，<code>compatibility</code> 记录了环境要求，可以指导模型如何执行技能的指令。</p><p><strong>仅正文（去除前置元数据）</strong>：代理解析并移除 YAML 前置元数据，仅返回 Markdown 指令。在拥有专用激活工具的现有实现中，大多数采用这种方法——在发现期间提取 <code>name</code> 和 <code>description</code> 后去除前置元数据。</p><p>两种方法在实践中都有效。</p></section><section><h3>4.4 结构化包装<a href="#44-结构化包装"><span>#</span></a></h3><p>如果你使用专用激活工具，请考虑将技能内容包装在识别标签中。例如：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>skill_content</span><span> </span><span>name</span><span>=</span><span>"pdf-processing"</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span># PDF 处理</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>## 何时使用此技能</span></div></div><div><div><div>5</div></div><div><span>当用户需要处理 PDF 文件时使用此技能...</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>[SKILL.md 正文的其余部分]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>技能目录：/home/user/.agents/skills/pdf-processing</span></div></div><div><div><div>10</div></div><div><span>此技能中的相对路径相对于技能目录。</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>&lt;</span><span>skill_resources</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>&lt;</span><span>file</span><span>&gt;scripts/extract.py&lt;/</span><span>file</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>&lt;</span><span>file</span><span>&gt;scripts/merge.py&lt;/</span><span>file</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>&lt;</span><span>file</span><span>&gt;references/pdf-spec-summary.md&lt;/</span><span>file</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span>&lt;/</span><span>skill_resources</span><span>&gt;</span></div></div><div><div><div>17</div></div><div><span>&lt;/</span><span>skill_content</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这具有实际好处：</p><ul>
<li>模型可以清楚地将技能指令与其他对话内容区分开</li>
<li>代理可以在上下文压缩期间识别技能内容（步骤 5）</li>
<li>捆绑资源被展示给模型，而无需急切加载</li>
</ul></section><section><h3>4.5 列出捆绑资源<a href="#45-列出捆绑资源"><span>#</span></a></h3><p>当专用激活工具返回技能内容时，它还可以枚举技能目录中的支持文件（脚本、参考文档、资源文件）——但<strong>不应急切地读取它们</strong>。当技能的指令引用特定文件时，模型会使用其文件读取工具按需加载。</p><p>对于大型技能目录，考虑限制列表大小，并注明可能不完整。</p></section><section><h3>4.6 权限允许列表<a href="#46-权限允许列表"><span>#</span></a></h3><p>如果你的代理具有控制文件访问的权限系统，请将<strong>技能目录加入允许列表</strong>，以便模型可以读取捆绑资源而无需触发用户确认提示。如果没有这个，每次引用捆绑脚本或参考文件都会导致权限对话框，从而破坏包含 <code>SKILL.md</code> 以外资源的技能的流程。</p></section></section>
<section><h2>步骤 5：随时间管理技能上下文<a href="#步骤-5随时间管理技能上下文"><span>#</span></a></h2><p>一旦技能指令进入对话上下文，请在整个会话期间保持其有效性。</p><section><h3>5.1 保护技能内容不被上下文压缩<a href="#51-保护技能内容不被上下文压缩"><span>#</span></a></h3><p>如果你的代理在上下文窗口填满时截断或总结较旧的消息，请<strong>将技能内容免于修剪</strong>。技能指令是持久的行为指导——在对话中途丢失它们会静默降低代理的性能，而不会出现任何可见错误。模型继续运行，但失去了技能提供的专门指令。</p><p>常见方法：</p><ul>
<li>将技能工具输出标记为受保护，以便修剪算法跳过它们</li>
<li>使用步骤 4 中的结构化标签来识别技能内容，并在压缩期间保留它</li>
</ul></section><section><h3>5.2 重复激活去重<a href="#52-重复激活去重"><span>#</span></a></h3><p>考虑跟踪当前会话中已激活了哪些技能。如果模型（或用户）尝试加载已在上下文中的技能，你可以跳过重新注入，以避免相同指令在对话中多次出现。</p></section><section><h3>5.3 子代理委派（可选）<a href="#53-子代理委派可选"><span>#</span></a></h3><p>这是一种高级模式，仅部分客户端支持。技能指令不是注入到主对话中，而是在<strong>单独的子代理会话</strong>中运行。子代理接收技能指令，执行任务，并将工作摘要返回给主对话。</p><p>当技能的工作流足够复杂，值得一个专用的、专注的会话时，此模式很有用。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/bert-t5-gpt-04/</id>
      <title type="text">04_ffn_layer_norm_skip_conn</title>
      <published>2026-07-26T00:00:00.000Z</published>
      <updated>2026-07-26T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/bert-t5-gpt-04/"/>
      <summary type="text">Transformer 与 GPT2 系列第四节</summary>
      <content type="html"><![CDATA[<p>本节的大部分内容都在 08 和 09 节中介绍得比较具体了，但那是放在 BERT 的环境下讨论的。本节我们将从 Transformer 架构的角度，重新审视 FFN 层归一化与残差连接。</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>import</span><span> torch.nn.functional </span><span>as</span><span> F</span></div></div><div><div><div>4</div></div><div><span>import</span><span> transformers</span></div></div><div><div><div>5</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoModel, AutoTokenizer, AutoConfig</span></div></div><div><div><div>6</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>7</div></div><div><span>import</span><span> pandas </span><span>as</span><span> pd</span></div></div><div><div><div>8</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>9</div></div><div><span>import</span><span> matplotlib </span><span>as</span><span> mpl</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>mpl.rcParams[</span><span>'figure.dpi'</span><span><span>] </span><span>=</span><span> </span></span><span>150</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>1. summary<a href="#1-summary"><span>#</span></a></h1><ul>
<li>两层的 MLP</li>
<li>两个细节
<ul>
<li>h -&gt; 4h -&gt; h
<ul>
<li>768 -&gt; 3072 -&gt; 768</li>
</ul>
</li>
<li>Gelu</li>
</ul>
</li>
<li>模型的 capacity 以及 memorization (world knowledge)</li>
<li>Layer Normalization &amp; skip connections
<ul>
<li>LN 在整个模型拓扑结构中的位置</li>
</ul>
</li>
</ul><section><h2>1.1 多层感知机<a href="#11-多层感知机"><span>#</span></a></h2><p>在 Transformer 的每个 encoder 层中，除了 Multi-Head Attention 之外，还有一个<strong>前馈神经网络</strong>（FFN, Feed-Forward Network）。它本质上就是一个两层的全连接网络（MLP）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入 → 线性层1 → 激活函数 → 线性层2 → 输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个细节：</p><ol>
<li>768 -&gt; 3072 -&gt; 768：这是 FFN 的维度变化规律：
<ul>
<li>h 是模型的隐藏层维度，在 BERT-base 模型中，<code>h=768</code>；</li>
<li>第一层将维度扩大到 4 倍，即 768 -&gt; 3072；</li>
<li>第二层再压缩回原维度，即 3072 -&gt; 768；</li>
</ul>
</li>
</ol><p>这样设计的好处是，先”膨胀”再”压缩”给了模型更大的表达能力：中间层有 4 倍的空间来学习更丰富的特征组合，然后再把学到的信息压缩回标准维度，方便和下一层对接。</p><ol>
<li>GELU（Gaussian Error Linear Unit）是 FFN 使用的激活函数。相比早期的 ReLU，GELU 更平滑，在 Transformer/BERT 中效果更好。简单理解：它就是一个”升级版的 ReLU”。</li>
</ol></section><section><h2>1.2 Capacity &amp; Memorization<a href="#12-capacity--memorization"><span>#</span></a></h2><ul>
<li>Capacity：指模型的容量。FN 的参数量非常大（总参数为 <span><span>768×3072+3072×768≈4.7M768\times 3072+3072\times 768\approx 4.7M</span><span><span><span></span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>3072</span><span></span><span>+</span><span></span></span><span><span></span><span>3072</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span><span></span><span>≈</span><span></span></span><span><span></span><span>4.7</span><span>M</span></span></span></span>，注意这里只算了权重没有算 bias），占了 Transformer 总参数的大部分。这让模型有足够的”脑容量”去学习复杂模式。</li>
<li>Memorization：研究表明，Transformer 中的 FFN 层主要负责存储<strong>事实性知识</strong>（比如”巴黎是法国的首都”这类世界知识），而 Attention 层负责从上下文中提取关系。可以粗略理解为：Attention 负责”推理”，FFN 负责”记忆”。</li>
</ul></section><section><h2>1.3 Layer Normalization &amp; Skip Connections<a href="#13-layer-normalization--skip-connections"><span>#</span></a></h2><ul>
<li>Layer Normalization：层归一化。训练深层网络时，各层的输出数值分布可能越来越不稳定（梯度消失/爆炸）。Layer Normalization 对每个样本的特征维度做标准化，让数值保持稳定，训练更顺畅。</li>
<li>Skip Connections：就是把输入直接”短路”加到输出上：<code>输出 = 子层(x) + x</code>. 这样做的好处是：即使子层（如 FFN 或 Attention）学得不好，梯度也能通过”短路路径”直接传回去，有效防止深层网络的梯度消失问题。</li>
</ul><p>关于 LN 在整个模型拓扑结构中的位置，这里指的是 Post-LN 与 Pre-LN 的区别：</p>

<table><thead><tr><th>类型</th><th>结构</th><th>说明</th></tr></thead><tbody><tr><td><strong>Post-LN</strong>（原始 Transformer）</td><td><code>x → 子层 → LN(x + 子层(x))</code></td><td>LN 放在残差连接<strong>之后</strong></td></tr><tr><td><strong>Pre-LN</strong>（现代主流）</td><td><code>x → LN(x) → 子层 → x + 子层(LN(x))</code></td><td>LN 放在子层<strong>之前</strong></td></tr></tbody></table><p>现代大多数模型（包括 GPT 系列）都采用 <strong>Pre-LN</strong>，因为训练更稳定。</p></section></section>
<section><h1>2. review multihead attention<a href="#2-review-multihead-attention"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_skpt </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>model </span><span>=</span><span> AutoModel.</span><span>from_pretrained</span><span>(model_skpt)</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>print</span><span>(model)</span></div></div><div><div><div>5</div></div><div><span>print</span><span>(model.embeddings)</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(model.encoder)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 只显示了 layer 的定义，至于层与层之间的拓扑连接关系，是在 module 的 forward 中定义</span></div></div><div><div><div>9</div></div><div><span>print</span><span>(model.encoder.layer[</span><span>0</span><span>])</span></div></div><div><div><div>10</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_skpt)</span></span></div></div><div><div><div>11</div></div><div><span><span>config </span><span>=</span><span> AutoConfig.</span><span>from_pretrained</span><span>(model_skpt)</span></span></div></div><div><div><div>12</div></div><div><span>config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>scaled_dot_product_attention</span><span>(</span><span>query</span><span>,</span><span><span> </span><span>key</span></span><span>,</span><span><span> </span><span>value</span></span><span>,</span><span><span> </span><span>mask</span></span><span>=</span><span>None</span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>计算缩放点积注意力</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>query: 查询张量，形状为 (..., seq_len_q, depth)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>key: 键张量，形状为 (..., seq_len_k, depth)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>value: 值张量，形状为 (..., seq_len_v, depth_v)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>mask: 可选的掩码张量，形状为 (..., seq_len_q, seq_len_k)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>9</div></div><div><span>    </span><span># hidden size</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>dim_k </span><span>=</span><span> key.</span><span>size</span><span>(</span><span>-</span><span>1</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span># batch_size, seq_len_q, seq_len_k</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>attn_scores </span><span>=</span><span> torch.</span><span>bmm</span><span>(query, key.</span><span>transpose</span><span>(</span><span>1</span><span>, </span><span>2</span><span><span>)) </span><span>/</span><span> np.</span><span>sqrt</span><span>(dim_k)</span></span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>attn_weights </span><span>=</span><span> F.</span><span>softmax</span><span>(attn_scores, </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span><span> torch.</span><span>bmm</span><span>(attn_weights, value)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>AttentionHead</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>embedding_dim</span></span><span>,</span><span><span> </span><span>head_dim</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.query </span><span>=</span><span> nn.</span><span>Linear</span><span>(embedding_dim, head_dim)</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.key </span><span>=</span><span> nn.</span><span>Linear</span><span>(embedding_dim, head_dim)</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.value </span><span>=</span><span> nn.</span><span>Linear</span><span>(embedding_dim, head_dim)</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>query</span><span>(hidden_states)</span></span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>key</span><span>(hidden_states)</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>value</span><span>(hidden_states)</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>attn_output </span><span>=</span><span> </span><span>scaled_dot_product_attention</span><span>(q, k, v)</span></div></div><div><div><div>13</div></div><div><span>        </span><span>return</span><span> attn_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>MultiHeadAttention</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>config</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>embedding_dim </span><span>=</span><span> config.hidden_size</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>num_heads </span><span>=</span><span> config.num_attention_heads</span></div></div><div><div><div>6</div></div><div><span>        </span><span># 768 // 12 = 64</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>head_dim </span><span>=</span><span> embedding_dim </span><span>//</span><span> num_heads</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span><span>.heads </span><span>=</span><span> nn.</span><span>ModuleList</span><span>([</span><span>AttentionHead</span><span>(embedding_dim, head_dim) </span></span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(num_heads)])</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span><span>.output_layer </span><span>=</span><span> nn.</span><span>Linear</span><span>(embedding_dim, embedding_dim)</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"input hidden_states shape: </span><span>{</span><span>hidden_states.shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"head (hidden_states) shape: </span><span>{</span><span>self</span><span>.heads[</span><span>11</span><span>](hidden_states).shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.</span><span>cat</span><span>([</span><span>head</span><span>(hidden_states) </span><span>for</span><span> head </span><span>in</span><span> </span><span>self</span><span>.heads], </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>16</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"concat head shape: </span><span>{</span><span>x.shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>output_layer</span><span>(x)</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>return</span><span> x</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mha </span><span>=</span><span> </span><span>MultiHeadAttention</span><span>(config)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># input</span></div></div><div><div><div>2</div></div><div><span># config.vocab_size = 30522</span></div></div><div><div><div>3</div></div><div><span># config.hidden_size = 768 = 12 * 64</span></div></div><div><div><div>4</div></div><div><span># lookup-table, learnable</span></div></div><div><div><div>5</div></div><div><span><span>token_embeddings </span><span>=</span><span> nn.</span><span>Embedding</span><span>(config.vocab_size, config.hidden_size)</span></span></div></div><div><div><div>6</div></div><div><span># token_embeddings</span></div></div><div><div><div>7</div></div><div><span><span>sample_text </span><span>=</span><span> </span></span><span>'time flies like an arrow'</span></div></div><div><div><div>8</div></div><div><span><span>model_inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(sample_text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>, </span><span>add_special_tokens</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>9</div></div><div><span># forward of embedding module</span></div></div><div><div><div>10</div></div><div><span><span>input_embeddings </span><span>=</span><span> </span><span>token_embeddings</span><span>(model_inputs[</span></span><span>'input_ids'</span><span>])</span></div></div><div><div><div>11</div></div><div><span># batch_size, seq_len, hidden_size</span></div></div><div><div><div>12</div></div><div><span>input_embeddings.shape </span><span># torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>attention_output </span><span>=</span><span> </span><span>mha</span><span>(input_embeddings)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>input hidden_states shape: torch.Size([1, 5, 768])</span></div></div><div><div><div>2</div></div><div><span>head (hidden_states) shape: torch.Size([1, 5, 64])</span></div></div><div><div><div>3</div></div><div><span>concat head shape: torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h1>3. FFN<a href="#3-ffn"><span>#</span></a></h1><p>以上是对之前的简单 review，下面正式进入本节的正题——FFN.</p><ul>
<li>RELU: <span><span>x⋅1x&gt;0x\cdot\mathbf{1}_{x&gt;0}</span><span><span><span></span><span>x</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span><span>x</span><span>&gt;</span><span>0</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></li>
<li>GELU: <span><span>xΦ(x)x\mathbf{\Phi}(x)</span><span><span><span></span><span>x</span><span>Φ</span><span>(</span><span>x</span><span>)</span></span></span></span></li>
</ul><span><span><span>Φ(x)=1+Erf(x/2)2\mathbf{\Phi}(x)=\frac{1+\text{Erf}(x/\sqrt{2})}{2}</span><span><span><span></span><span>Φ</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span><span>Erf</span></span><span>(</span><span>x</span><span>/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><section><h2>3.1 ReLU<a href="#31-relu"><span>#</span></a></h2><p>ReLU (Rectified Linear Unit)，公式为：</p><span><span><span>ReLU(x)=x⋅1x&gt;0=max⁡(0,x)\text{ReLU}(x) = x\cdot\mathbf{1}_{x&gt;0} = \max(0,x)</span><span><span><span></span><span><span>ReLU</span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span><span>x</span><span>&gt;</span><span>0</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>(</span><span>0</span><span>,</span><span></span><span>x</span><span>)</span></span></span></span></span><p>含义非常简单：如果输入是正数，原样输出；如果是负数，直接输出 0.</p><ul>
<li>优点：计算极快，没有复杂的指数运算；正区间梯度恒为 1，缓解了梯度消失。</li>
<li>缺点：负区间梯度为 0，导致某些神经元可能永远”死掉”（dead ReLU），再也无法激活。</li>
</ul></section><section><h2>3.2 LeakyRELU<a href="#32-leakyrelu"><span>#</span></a></h2><p>LeakyRELU 是 ReLU 的一个改进版本，公式为：</p><span><span><span>LeakyRELU(x)=max⁡(0,x)+α⋅min⁡(0,x)\text{LeakyRELU}(x)=\max(0,x)+\alpha\cdot \min(0,x)</span><span><span><span></span><span><span>LeakyRELU</span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span>(</span><span>0</span><span>,</span><span></span><span>x</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>α</span><span></span><span>⋅</span><span></span></span><span><span></span><span>min</span><span>(</span><span>0</span><span>,</span><span></span><span>x</span><span>)</span></span></span></span></span><p>其中 <span><span>α\alpha</span><span><span><span></span><span>α</span></span></span></span> 是一个很小的常数，例如 0.01 或 0.1.</p><p>ReLU 对负数直接抹零，而 LeakyReLU 对负数<strong>留一条缝</strong>——负值不是变成 0，而是乘上一个很小的系数 <span><span>α\alpha</span><span><span><span></span><span>α</span></span></span></span> 后”漏”一点过去。所以，即使 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 为负，它也依然有斜率。</p><p>LeakyRELU 主要是为了解决ReLU 的<strong>死神经元问题</strong>：一旦某个神经元输出的加权和始终为负，它的梯度恒为 0，这个神经元就永远无法更新了。</p><p>因此，LeakyReLU 的解决思路是：在负区间给一个极小的非零梯度 <span><span>α\alpha</span><span><span><span></span><span>α</span></span></span></span>，这样即使神经元落到负区间，仍有微弱的梯度信号传回去，神经元就有机会”复活”。</p></section><section><h2>3.3 GELU<a href="#33-gelu"><span>#</span></a></h2><p>GELU (Gaussian Error Linear Unit)，公式为：</p><span><span><span>GELU(x)=x⋅Φ(x)\text{GELU}(x) = x\cdot\mathbf{\Phi}(x)</span><span><span><span></span><span><span>GELU</span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>⋅</span><span></span></span><span><span></span><span>Φ</span><span>(</span><span>x</span><span>)</span></span></span></span></span><p>其中 <span><span>Φ(x)\mathbf{\Phi}(x)</span><span><span><span></span><span>Φ</span><span>(</span><span>x</span><span>)</span></span></span></span> 是标准正态分布的累积分布函数：</p><span><span><span>Φ(x)=1+Erf(x/2)2\mathbf{\Phi}(x)=\frac{1+\text{Erf}(x/\sqrt{2})}{2}</span><span><span><span></span><span>Φ</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span><span>Erf</span></span><span>(</span><span>x</span><span>/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>ReLU 用硬性开关（正/负一刀切），而 GELU 用概率来决定要不要输出这个值：</p><ul>
<li>
<p>输入的 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 越大，越有可能被保留；</p>
</li>
<li>
<p><span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 越小，越可能被抑制。</p>
</li>
<li>
<p>优点：处处可导、曲线平滑，对小负值不是直接抹零而是”打折”输出，保留了更多信息。在 Transformer/BERT 等大模型中效果明显优于 ReLU。</p>
</li>
<li>
<p>缺点：计算量比 ReLU 大（涉及 erf 误差函数或 tanh 近似），但在实际工程中用近似公式已经足够快。</p>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>x </span><span>=</span><span> np.</span><span>arange</span><span>(</span><span>-</span></span><span>5</span><span>, </span><span>5</span><span>, </span><span>0.01</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>plt.</span><span>plot</span><span>(x, nn.</span><span>ReLU</span><span>()(torch.</span><span>from_numpy</span><span>(x)))</span></span></div></div><div><div><div>3</div></div><div><span><span>plt.</span><span>plot</span><span>(x, nn.</span><span>LeakyReLU</span><span>(</span></span><span>negative_slope</span><span>=</span><span>0.1</span><span><span>)(torch.</span><span>from_numpy</span><span>(x)))</span></span></div></div><div><div><div>4</div></div><div><span><span>plt.</span><span>plot</span><span>(x, nn.</span><span>GELU</span><span>()(torch.</span><span>from_numpy</span><span>(x)))</span></span></div></div><div><div><div>5</div></div><div><span><span>plt.</span><span>legend</span><span>([</span></span><span>'ReLU'</span><span>, </span><span>'LeakyReLU'</span><span>, </span><span>'GELU'</span><span>])</span></div></div><div><div><div>6</div></div><div><span><span>plt.</span><span>show</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260705162214067.png" alt="" /></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>FeedForward</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>config</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.linear1 </span><span>=</span><span> nn.</span><span>Linear</span><span>(config.hidden_size, config.intermediate_size)</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.activation </span><span>=</span><span> nn.</span><span>GELU</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.linear2 </span><span>=</span><span> nn.</span><span>Linear</span><span>(config.intermediate_size, config.hidden_size)</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.dropout </span><span>=</span><span> nn.</span><span>Dropout</span><span>(config.hidden_dropout_prob)</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>linear1</span><span>(hidden_states)</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>activation</span><span>(x)</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>linear2</span><span>(x)</span></span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>dropout</span><span>(x)</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>return</span><span> x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>ffn </span><span>=</span><span> </span><span>FeedForward</span><span>(config)</span></span></div></div><div><div><div>2</div></div><div><span><span>ffn</span><span>(attention_output).</span><span>size</span><span>() </span></span><span># torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h1>4. layer norm &amp; skip connection<a href="#4-layer-norm--skip-connection"><span>#</span></a></h1><ul>
<li>layer norm (<code>config, "layer_norm_eps": 1e-12</code>, )
<ul>
<li>zero mean &amp; one variant</li>
<li>与 batch norm 相对，是样本层面</li>
<li>对于 NLP 问题，一般是每一个 token 对应的 embedding（即 768 维的 hidden states 进行 0 均值 1 标准差的归一化）</li>
</ul>
</li>
<li>Transformer 中主要是两处用到了 layer norm：
<ul>
<li>embedding：对于最终的 token_embedding</li>
<li>构建 BERT layer（即 Transformer encoder layer）</li>
</ul>
</li>
</ul><section><h2>4.1 layer norm<a href="#41-layer-norm"><span>#</span></a></h2><p>Layer Norm 的核心操作很简单，就是对一组数值做两件事：</p><span><span><span>x^i=xi−μσ\hat{x}_i=\frac{x_i-\mu}{\sigma}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>σ</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span>μ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><ul>
<li>减均值 <span><span>μ\mu</span><span><span><span></span><span>μ</span></span></span></span>：让数据中心平移到 0（零均值）；</li>
<li>除以标准差 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span>：让数据缩放成标准差为 1（单位方差）；</li>
</ul><p>除此之外，还有一个可学习的缩放和偏移参数 <span><span>γ\gamma</span><span><span><span></span><span>γ</span></span></span></span> 和 <span><span>β\beta</span><span><span><span></span><span>β</span></span></span></span>：<span><span>yi=γx^i+βy_i=\gamma \hat{x}_i+\beta</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>γ</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>β</span></span></span></span>，让模型自己决定要不要“还原”一些信息。</p><p><code>"layer_norm_eps": 1e-12</code> 就是为了防止除以 0 而加的一个极小值，在之前的 <code>config</code> 中可以看到。</p><hr /><p>与 Batch Norm 的区别：layer norm 是”样本层面”的：</p>

<table><thead><tr><th></th><th>Batch Norm</th><th>Layer Norm</th></tr></thead><tbody><tr><td>归一化的维度</td><td>同一个特征，<strong>跨样本</strong>（跨 batch）</td><td>同一个样本，<strong>跨特征</strong>（跨 hidden dims）</td></tr><tr><td>依赖 batch 大小</td><td>✅ 依赖，batch 太小就不稳定</td><td>❌ 不依赖，batch=1 也能用</td></tr><tr><td>适用场景</td><td>CNN（图像）</td><td>RNN / Transformer（NLP）</td></tr></tbody></table><p>用一个例子来直观理解：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>假设一个 batch 有 2 个句子，各 5 个 token，hidden_size=768</span></div></div><div><div><div>2</div></div><div><span>张量形状: [batch=2, seq=5, hidden=768]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Batch Norm:  对每个 hidden 维度，跨 batch×seq 求均值（跨样本）</span></div></div><div><div><div>5</div></div><div><span>Layer Norm:  对每个 token，跨 768 维求均值（样本内部）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以，Layer Norm 是<strong>每个样本自己跟自己比</strong>，不依赖 batch 里其他样本。</p><hr /><p>对 NLP 问题的具体做法：</p><blockquote><p>每一个 token 对应的 embedding（即 768 维的 hidden states）进行 0 均值 1 标准差的归一化</p></blockquote><p>具体来说，对于形状为 <code>[batch, seq_len, 768]</code> 的张量：</p><ul>
<li><strong>归一化对象</strong>：每个 token 的 768 维向量</li>
<li><strong>统计范围</strong>：这 768 个数（不是跨 token，也不是跨 batch）</li>
<li><strong>结果</strong>：每个 token 的 768 维向量变成均值 0、标准差 1</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Token "time" 的 768 维向量 → 均值 0, 标准差 1（独立归一化）</span></div></div><div><div><div>2</div></div><div><span>Token "flies" 的 768 维向量 → 均值 0, 标准差 1（独立归一化）</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个 token 独立归一化，互不影响。这天然适合 NLP——因为句子长度不固定，batch norm 很难处理变长序列。</p><hr /><p>Transformer 中使用 Layer Norm 的两处位置：</p><section><h5>第一处：Embedding 层之后<a href="#第一处embedding-层之后"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>token → Embedding → Layer Norm → 进入 encoder</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对最终的 token embedding 先做一次归一化，让输入数值稳定后再送入 encoder 层。</p></section><section><h5>第二处：每个 Encoder Layer 内部<a href="#第二处每个-encoder-layer-内部"><span>#</span></a></h5><p>每个 Transformer encoder layer 内部有<strong>两个子层</strong>，各配一个 Layer Norm：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>x → Attention → Skip Connection → Layer Norm → FFN → Skip Connection → Layer Norm → 输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>即我们之前讨论的 Pre-LN 或 Post-LN 的位置。BERT 用的是 <strong>Post-LN</strong>（LN 在残差连接之后），示意图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260705175702452.png" alt="" /></p></section></section></section>
<section><h1>5. EncoderLayer<a href="#5-encoderlayer"><span>#</span></a></h1><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/2d3dc8c29d142805229860bf5ec80fa1.png" alt="" /></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>TransformerEncoderLayer</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>config</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.self_attention </span><span>=</span><span> </span><span>MultiHeadAttention</span><span>(config)</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.layer_norm1 </span><span>=</span><span> nn.</span><span>LayerNorm</span><span>(config.hidden_size, </span></span><span>eps</span><span><span>=</span><span>config.layer_norm_eps)</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.feed_forward </span><span>=</span><span> </span><span>FeedForward</span><span>(config)</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.layer_norm2 </span><span>=</span><span> nn.</span><span>LayerNorm</span><span>(config.hidden_size, </span></span><span>eps</span><span><span>=</span><span>config.layer_norm_eps)</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>10</div></div><div><span>        </span><span># Self-Attention</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>attention_output </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>self_attention</span><span>(hidden_states)</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span># Skip Connection + Layer Norm</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>hidden_states </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>layer_norm1</span><span>(hidden_states </span><span>+</span><span> attention_output)</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>        </span><span># Feed Forward</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>ffn_output </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>feed_forward</span><span>(hidden_states)</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span># Skip Connection + Layer Norm</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>hidden_states </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>layer_norm2</span><span>(hidden_states </span><span>+</span><span> ffn_output)</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>        </span><span>return</span><span> hidden_states</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>encoder_layer </span><span>=</span><span> </span><span>TransformerEncoderLayer</span><span>(config)</span></span></div></div><div><div><div>2</div></div><div><span><span>encoder_layer</span><span>(input_embeddings).shape </span></span><span># torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>input hidden_states shape: torch.Size([1, 5, 768])</span></div></div><div><div><div>2</div></div><div><span>head (hidden_states) shape: torch.Size([1, 5, 64])</span></div></div><div><div><div>3</div></div><div><span>concat head shape: torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/bert-t5-gpt-03/</id>
      <title type="text">03_transformer_architecture_multi_head_attention</title>
      <published>2026-07-21T00:00:00.000Z</published>
      <updated>2026-07-21T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/bert-t5-gpt-03/"/>
      <summary type="text">Transformer 与 GPT2 系列第三节</summary>
      <content type="html"><![CDATA[<section><h1>1. Multi-head (Self/Cross) Attention<a href="#1-multi-head-selfcross-attention"><span>#</span></a></h1><p>本节我们继续介绍 Transformer 架构的多头（自/交叉）注意力。下面是 Multi-head Attention 的架构图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260627152938280.png" alt="" /></p><p>多个头的 Scaled dot product attention 拼接起来，送给 concat，然后交给 Linear 就得到了 multi-head attention 的输出。</p><p>我们首先承接上一节的内容：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>import</span><span> transformers</span></div></div><div><div><div>4</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>5</div></div><div><span>import</span><span> pandas </span><span>as</span><span> pd</span></div></div><div><div><div>6</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>7</div></div><div><span>import</span><span> matplotlib </span><span>as</span><span> mpl</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>mpl.rcParams[</span><span>'figure.dpi'</span><span><span>] </span><span>=</span><span> </span></span><span>200</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(torch.</span><span>__version__</span><span>) </span><span># 2.6.0+cu124</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(transformers.</span><span>__version__</span><span>) </span><span># 5.9.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> bertviz.transformers_neuron_view </span><span>import</span><span> BertModel</span></div></div><div><div><div>2</div></div><div><span>from</span><span> bertviz.neuron_view </span><span>import</span><span> show</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>sample_text </span><span>=</span><span> </span></span><span>'time flies like an arrow'</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>show</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>model,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>model_type</span><span>=</span><span>'bert'</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>tokenizer</span><span><span>=</span><span>tokenizer,</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>sentence_a</span><span><span>=</span><span>sample_text,</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>display_mode</span><span>=</span><span>'light'</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>layer</span><span>=</span><span>0</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>head</span><span>=</span><span>8</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(sample_text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>, </span><span>add_special_tokens</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>2</div></div><div><span>model_inputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoConfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>config </span><span>=</span><span> AutoConfig.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>2</div></div><div><span>config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># config.vocab_size: 30522</span></div></div><div><div><div>2</div></div><div><span># config.hidden_size: 768=64*12</span></div></div><div><div><div>3</div></div><div><span><span>token_embedding </span><span>=</span><span> nn.</span><span>Embedding</span><span>(config.vocab_size, config.hidden_size)</span></span></div></div><div><div><div>4</div></div><div><span>token_embedding </span><span># Embedding(30522, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># forward of embedding module</span></div></div><div><div><div>2</div></div><div><span><span>input_embeddings </span><span>=</span><span> </span><span>token_embedding</span><span>(model_inputs[</span></span><span>'input_ids'</span><span>])</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># batch_size, seq_len, hidden_size</span></div></div><div><div><div>5</div></div><div><span>input_embeddings.shape </span><span># torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 先不考虑 position encoding</span></div></div><div><div><div>2</div></div><div><span># query: [1, 5, 768], key: [1, 5, 768], value: [1, 5, 768]</span></div></div><div><div><div>3</div></div><div><span># W_q, W_k, W_v: nn.linear, 作用在 token_embedding 上，依然通过 learing 最终决定</span></div></div><div><div><div>4</div></div><div><span><span>query </span><span>=</span><span> key </span><span>=</span><span> value </span><span>=</span><span> input_embeddings</span></span></div></div><div><div><div>5</div></div><div><span><span>dim_k </span><span>=</span><span> key.</span><span>size</span><span>(</span><span>-</span></span><span>1</span><span>) </span><span># 768</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(dim_k)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># /np.sqrt(dim_k) 是为了缩放，防止 dot product 过大导致 softmax 后梯度消失</span></div></div><div><div><div>9</div></div><div><span><span>attention_scores </span><span>=</span><span> torch.</span><span>matmul</span><span>(query, key.</span><span>transpose</span><span>(</span></span><span>1</span><span>, </span><span>2</span><span><span>)) </span><span>/</span><span> np.</span><span>sqrt</span><span>(dim_k)</span></span></div></div><div><div><div>10</div></div><div><span>attention_scores.shape </span><span># torch.Size([1, 5, 5])</span></div></div><div><div><div>11</div></div><div><span>attention_scores</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># from scores -&gt; weights, softmax</span></div></div><div><div><div>2</div></div><div><span>import</span><span> torch.nn.functional </span><span>as</span><span> F</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>attention_weights </span><span>=</span><span> F.</span><span>softmax</span><span>(attention_scores, </span></span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>5</div></div><div><span>attention_weights.shape </span><span># torch.Size([1, 5, 5])</span></div></div><div><div><div>6</div></div><div><span>attention_weights</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>attention_weights.</span><span>sum</span><span>(</span></span><span>dim</span><span>=-</span><span>1</span><span>) </span><span># 每行加起来等于 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 5*5, 5*768 -&gt; 5*768</span></div></div><div><div><div>2</div></div><div><span><span>attention_output </span><span>=</span><span> torch.</span><span>bmm</span><span>(attention_weights, value)</span></span></div></div><div><div><div>3</div></div><div><span>attention_output.shape </span><span># torch.Size([1, 5, 768])</span></div></div><div><div><div>4</div></div><div><span>attention_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># batch_size, seq_len, hidden_size</span></div></div><div><div><div>2</div></div><div><span>def</span><span> </span><span>scaled_dot_product_attention</span><span>(</span><span>query</span><span>,</span><span><span> </span><span>key</span></span><span>,</span><span><span> </span><span>value</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>dim_k </span><span>=</span><span> key.</span><span>size</span><span>(</span><span>-</span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>attention_scores </span><span>=</span><span> torch.</span><span>matmul</span><span>(query, key.</span><span>transpose</span><span>(</span><span>1</span><span>, </span><span>2</span><span><span>)) </span><span>/</span><span> np.</span><span>sqrt</span><span>(dim_k)</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>attention_weights </span><span>=</span><span> F.</span><span>softmax</span><span>(attention_scores, </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>attention_output </span><span>=</span><span> torch.</span><span>bmm</span><span>(attention_weights, value)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> attention_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scaled_dot_product_attention</span><span>(query, key, value)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>以上就是上一节的内容，这里我们已经得到了 Scaled dot product attention，也就是之前示意图中的红色部分。</p><p>下面正式进入本节的部分。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>AttentionHead</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>embed_dim</span></span><span>,</span><span><span> </span><span>head_dim</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span>        </span><span># 三个线性层，用 W 把输入投影成 query, key, value</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.W_q </span><span>=</span><span> nn.</span><span>Linear</span><span>(embed_dim, head_dim)</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.W_k </span><span>=</span><span> nn.</span><span>Linear</span><span>(embed_dim, head_dim)</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.W_v </span><span>=</span><span> nn.</span><span>Linear</span><span>(embed_dim, head_dim)</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>W_q</span><span>(hidden_states)</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>W_k</span><span>(hidden_states)</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>W_v</span><span>(hidden_states)</span></span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>attention_output </span><span>=</span><span> </span><span>scaled_dot_product_attention</span><span>(q, k, v)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>return</span><span> attention_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们这里简单定义了一个注意力头从输入到输出的完整 pipline. 这里的 <code>W_q</code>, <code>W_k</code>, <code>W_v</code> 是 <code>nn.Linear</code> 模块，每个模块内部存储着：</p><ul>
<li>一个可学习的权重矩阵 weight，形状为 (<code>head_dim</code>, <code>embed_dim</code>)</li>
<li>一个可学习的 bias，形状为 (<code>head_dim</code>)，默认开启</li>
</ul><p>那么我们在上一节中提到的，数学上的 <span><span>WQW^Q</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span></span></span></span> 对应的就是 <code>self.W_q.weight</code>，再加上可选的 bias.</p><hr /><p>关于具体得到 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 的过程，我们知道 <code>hidden_states</code> 的形状为 <code>[batch_size, seq_len, embed_dim]</code>. 对于每个 token 位置 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>，它的向量为 <span><span>xix_i</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（长度 768），那么 <code>nn.Linear</code> 对每个 <span><span>xix_i</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做的就是：</p><span><span><span>qi=WQxi+bQq_i=W^Qx_i+b^Q</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span></span></span></span></span><p>在 Pytorch 中，<code>nn.Linear(in_features, out_features)</code> 的 <code>forward</code> 本质是</p><span><span><span>y=xWT+by=xW^T+b</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span></span></span></span></span><p>其中 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 的形状是 <code>(out_features, in_features)</code>，也就是 (<code>head_dim</code>, <code>embed_dim</code>)，差别主要是 PyTorch 把权重存成 <code>(out, in)</code>，关于这一点我们在 08 节中探讨过。</p><p>对整段序列，PyTorch 会在 <code>batch</code> 和 <code>seq_len</code> 维上批量做同样的线性变换，所以一次调用就得到所有位置的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hidden_states: [batch, seq_len, embed_dim]</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>↓  W_q（对每个 token 的向量做同一线性变换）</span></div></div><div><div><div>3</div></div><div><span>q:             [batch, seq_len, head_dim]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>K,VK,V</span><span><span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 同理。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>MultiHeadAttention</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># config 见前文，存储 BERT 的超参数</span></div></div><div><div><div>3</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>config</span></span><span>):</span></div></div><div><div><div>4</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>embed_dim </span><span>=</span><span> config.hidden_size          </span><span># 例如 BERT-base: 768</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>num_heads </span><span>=</span><span> config.num_attention_heads  </span><span># 例如 12</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>head_dim </span><span>=</span><span> embed_dim </span><span>//</span><span> num_heads       </span><span># 768 // 12 = 64</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.heads </span><span>=</span><span> nn.</span><span>ModuleList</span><span>([</span></span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>AttentionHead</span><span>(embed_dim, head_dim) </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(num_heads)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>])</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.output_layer </span><span>=</span><span> nn.</span><span>Linear</span><span>(embed_dim, embed_dim)</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 定义前向计算过程</span></div></div><div><div><div>14</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>15</div></div><div><span>        </span><span># hidden_states: [batch_size, seq_len, embed_dim]</span></div></div><div><div><div>16</div></div><div><span>        </span><span># 按头分割</span></div></div><div><div><div>17</div></div><div><span>        </span><span># 每个头得到的结果是 [batch_size, seq_len, head_dim]</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> torch.</span><span>cat</span><span>([</span><span>head</span><span>(hidden_states) </span><span>for</span><span> head </span><span>in</span><span> </span><span>self</span><span>.heads], </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>output_layer</span><span>(x)</span></span></div></div><div><div><div>20</div></div><div><span>        </span><span>return</span><span> x</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里我们就定义了多头注意力，它的核心之前也介绍过：不要只算一次注意力，而是并行算<strong>多个独立的注意力头</strong>，每个头用自己的 <span><span>WQ,WK,WVW^Q, W^K, W^V</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>K</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span></span></span></span></span></span></span></span>，从不同子空间看同一段序列；再把各头结果拼起来，最后过一个线性层得到输出。</p><ul>
<li><code>nn.ModuleList</code> ：把多个子模块放进列表，PyTorch 能正确注册参数。
<ul>
<li>列表里有 12 个 <code>AttentionHead</code>，彼此参数不共享。</li>
<li>每个 <code>AttentionHead</code> 内部有 3 个 <code>nn.Linear(embed_dim, head_dim)</code>，即各自的 <span><span>WQ,WK,WVW^Q, W^K, W^V</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>K</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span></span></span></span></span></span></span></span>.</li>
</ul>
</li>
<li><code>torch.cat</code> ：对得到的 12 个 tensors 进行拼接，<code>dim=-1</code> 表示在 <strong>特征维</strong> 上拼接：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>head_0:  [B, L, 64]</span></div></div><div><div><div>2</div></div><div><span>head_1:  [B, L, 64]</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div><div><div><div>4</div></div><div><span>head_11: [B, L, 64]</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>↓ concat</span></div></div><div><div><div>6</div></div><div><span>x:       [B, L, 768]    # 64 × 12 = 768</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>x = self.output_layer(x)</code> ：最后输出线性层，对每个 token 的 768 维向量做 <span><span>y=xWO+by=xW^O+b</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span><span>W</span><span><span><span><span><span><span></span><span><span>O</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span></span></span></span>，最终返回的 <code>x</code> 就是 Multi-head Attention 的输出。</li>
</ul><p>整体的数据流：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hidden_states                    [B, L, 768]</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>├─ AttentionHead 0 ──→ [B, L, 64]</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>├─ AttentionHead 1 ──→ [B, L, 64]</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>├─ ...</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>└─ AttentionHead 11 ─→ [B, L, 64]</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>▼ torch.cat(dim=-1)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>x                            [B, L, 768]</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>▼ output_layer (W^O)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>输出                           [B, L, 768]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoConfig, AutoTokenizer, AutoModel</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>5</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># hyperparameters</span></div></div><div><div><div>8</div></div><div><span><span>config </span><span>=</span><span> AutoConfig.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>9</div></div><div><span>config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mha </span><span>=</span><span> </span><span>MultiHeadAttention</span><span>(config)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># input</span></div></div><div><div><div>2</div></div><div><span># config.vocab_size: 30522</span></div></div><div><div><div>3</div></div><div><span># config.hidden_state: 768=64*12</span></div></div><div><div><div>4</div></div><div><span># lookup-table, learnable</span></div></div><div><div><div>5</div></div><div><span><span>token_embedding </span><span>=</span><span> nn.</span><span>Embedding</span><span>(config.vocab_size, config.hidden_size)</span></span></div></div><div><div><div>6</div></div><div><span># token_embedding</span></div></div><div><div><div>7</div></div><div><span><span>sample_text </span><span>=</span><span> </span></span><span>'time flies like an arrow'</span></div></div><div><div><div>8</div></div><div><span><span>model_inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(sample_text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>, </span><span>add_special_tokens</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>9</div></div><div><span># foward of embedding module</span></div></div><div><div><div>10</div></div><div><span><span>input_embeddings </span><span>=</span><span> </span><span>token_embedding</span><span>(model_inputs[</span></span><span>'input_ids'</span><span>])</span></div></div><div><div><div>11</div></div><div><span># batch_size, seq_len, hidden_size</span></div></div><div><div><div>12</div></div><div><span>input_embeddings.shape </span><span># torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mha</span><span>(input_embeddings)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.3316, -0.1109, -0.2945,  ...,  0.2417, -0.0272,  0.0901],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[ 0.3491, -0.1779, -0.2362,  ...,  0.1970,  0.1125,  0.0460],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[ 0.2233, -0.0979, -0.1581,  ...,  0.1129,  0.1000,  0.0995],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>[ 0.3722, -0.1765, -0.2382,  ...,  0.1719, -0.0061,  0.0895],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.3137, -0.2047, -0.2799,  ...,  0.2113,  0.0330, -0.0451]]],</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>grad_fn=&lt;ViewBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> bertviz </span><span>import</span><span> head_view</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoModel</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>model </span><span>=</span><span> AutoModel.</span><span>from_pretrained</span><span>(model_ckpt, </span></span><span>output_attentions</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>sentence_a </span><span>=</span><span> </span></span><span>'time flies like an arrow'</span></div></div><div><div><div>6</div></div><div><span><span>sentence_b </span><span>=</span><span> </span></span><span>'fruit flies like a banana'</span></div></div><div><div><div>7</div></div><div><span><span>viz_inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(sentence_a, sentence_b, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># nsp (next sentence prediction)</span></div></div><div><div><div>10</div></div><div><span>print</span><span>(viz_inputs)</span></div></div><div><div><div>11</div></div><div><span><span>attention </span><span>=</span><span> </span><span>model</span><span>(**viz_inputs).attentions</span></span></div></div><div><div><div>12</div></div><div><span><span>sentence_b_start </span><span>=</span><span> (viz_inputs.token_type_ids </span><span>==</span><span> </span></span><span>0</span><span><span>).</span><span>sum</span><span>(</span></span><span>dim</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>13</div></div><div><span>print</span><span>(sentence_b_start)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>tokens </span><span>=</span><span> tokenizer.</span><span>convert_ids_to_tokens</span><span>(viz_inputs.input_ids[</span></span><span>0</span><span>]) </span><span># tensor([...])</span></div></div><div><div><div>16</div></div><div><span><span>head_view</span><span>(attention, tokens, sentence_b_start, </span></span><span>heads</span><span><span>=</span><span>[</span></span><span>8</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>部分输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[  101,  2051, 10029,  2066,  2019,  8612,   102,  5909, 10029,  2066,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>1037, 15212,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}</span></div></div><div><div><div>3</div></div><div><span>tensor([7])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看出，我们只有一个 tensor，也就是说两句话实际上被展平了，我们识别第一句话和第二句话的方法就是看 <code>token_type_ids</code>，为 0 的就是第一句话，为 1 的就是第二句话。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-06/</id>
      <title type="text">skill 脚本</title>
      <published>2026-07-16T00:00:00.000Z</published>
      <updated>2026-07-16T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-06/"/>
      <summary type="text">介绍如何在 skill 中捆绑并运行脚本</summary>
      <content type="html"><![CDATA[<blockquote><p>如何在你的技能中运行命令并捆绑可执行脚本。</p></blockquote>
<p>技能可以指示代理运行 shell 命令，并将可复用脚本捆绑在 <code>scripts/</code> 目录中。本指南涵盖一次性命令、自带依赖的自包含脚本，以及如何为代理使用设计脚本接口。</p>
<section><h2>1. 一次性命令<a href="#1-一次性命令"><span>#</span></a></h2><p>当现有包已经能满足你的需求时，你可以直接在 <code>SKILL.md</code> 指令中引用它，而无需 <code>scripts/</code> 目录。许多生态系统提供了在运行时自动解析依赖的工具，下面仅列举常用的三种，更多类别参见<a href="https://agentskills.io/" target="_blank">官网</a>。</p><ol>
<li><a href="https://docs.astral.sh/uv/guides/tools/" target="_blank">uvx</a> 在隔离环境中运行 Python 包，并具有激进的缓存。它随 <a href="https://docs.astral.sh/uv/" target="_blank">uv</a> 一起提供。</li>
</ol><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>uvx</span><span> </span><span>ruff@0.8.0</span><span> </span><span>check</span><span> </span><span>.</span></div></div><div><div><div>2</div></div><div><span>uvx</span><span> </span><span>black@24.10.0</span><span> </span><span>.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>不捆绑在 Python 中——需要单独安装。</li>
<li>速度快。积极缓存，重复运行几乎即时。</li>
</ul><ol>
<li><a href="https://pipx.pypa.io/" target="_blank">pipx</a> 在隔离环境中运行 Python 包。可通过操作系统包管理器获取（<code>apt install pipx</code>，<code>brew install pipx</code>）。</li>
</ol><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pipx</span><span> </span><span>run</span><span> </span><span>'black==24.10.0'</span><span> </span><span>.</span></div></div><div><div><div>2</div></div><div><span>pipx</span><span> </span><span>run</span><span> </span><span>'ruff==0.8.0'</span><span> </span><span>check</span><span> </span><span>.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>不捆绑在 Python 中——需要单独安装。</li>
<li>是 <code>uvx</code> 的成熟替代品。虽然 <code>uvx</code> 已成为标准推荐，但 <code>pipx</code> 仍是一个可靠的选择，在操作系统包管理器中的可用性更广。</li>
</ul><ol>
<li><a href="https://docs.npmjs.com/cli/commands/npx" target="_blank">npx</a> 运行 npm 包，按需下载。它随 npm（随 Node.js 提供）一起提供。</li>
</ol><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>npx</span><span> </span><span>eslint@9</span><span> </span><span>--fix</span><span> </span><span>.</span></div></div><div><div><div>2</div></div><div><span>npx</span><span> </span><span>create-vite@6</span><span> </span><span>my-app</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>捆绑在 Node.js 中——无需额外安装。</li>
<li>下载包、运行它，并缓存以供将来使用。</li>
<li>使用 <code>npx package@version</code> 锁定版本以确保可重现性。</li>
</ul><p><strong>在技能中使用一次性命令的技巧：</strong></p><ul>
<li><strong>锁定版本</strong>（例如 <code>npx eslint@9.0.0</code>），使命令在不同时间行为一致。</li>
<li><strong>在 <code>SKILL.md</code> 中说明前提条件</strong>（例如“需要 Node.js 18+”），而不是假定代理环境已具备。对于运行时级别的要求，使用 <code>compatibility</code> 前置元数据字段。</li>
<li><strong>将复杂命令移至脚本中。</strong> 当你调用带有几个标志的工具时，一次性命令效果很好。当命令变得复杂到难以在第一次尝试时正确执行时，<code>scripts/</code> 中经过测试的脚本更加可靠。</li>
</ul></section>
<section><h2>2. 从 <code>SKILL.md</code> 引用脚本<a href="#2-从-skillmd-引用脚本"><span>#</span></a></h2><p>使用<strong>从技能目录根目录开始的相对路径</strong>来引用捆绑文件。代理会自动解析这些路径——无需绝对路径。</p><p>在 <code>SKILL.md</code> 中列出可用脚本，以便代理知道它们的存在：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 可用脚本</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>-</span><span> </span></span><span>**</span><span><span>`</span><span>scripts/validate.sh</span><span>`</span></span><span>**</span><span> —— 验证配置文件</span></div></div><div><div><div>4</div></div><div><span><span>-</span><span> </span></span><span>**</span><span><span>`</span><span>scripts/process.py</span><span>`</span></span><span>**</span><span> —— 处理输入数据</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后指示代理运行它们：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 工作流</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>1.</span><span> 运行验证脚本：</span></span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>```bash</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>bash scripts/validate.sh </span><span>"</span><span>$INPUT_FILE</span><span>"</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>```</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>2.</span><span> 处理结果：</span></span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>```bash</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>python3 scripts/process.py --input results.json</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Info</div></div><div><p>相同的相对路径约定也适用于 <code>references/*.md</code> 等支持文件——脚本执行路径（在代码块中）是相对于<strong>技能目录根目录</strong>的，因为代理从那里运行命令。</p></div></div></section>
<section><h2>3. 自包含脚本<a href="#3-自包含脚本"><span>#</span></a></h2><p>当你需要可复用逻辑时，在 <code>scripts/</code> 中捆绑一个脚本，并在脚本内联声明其依赖项。代理可以用单个命令运行该脚本——无需单独的清单文件或安装步骤。</p><p>多种语言支持内联依赖声明，以 Python 为例：</p><p><a href="https://peps.python.org/pep-0723/" target="_blank">PEP 723</a> 定义了内联脚本元数据的标准格式。在 <code># ///</code> 标记内的 TOML 块中声明依赖项：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span># /// script</span></div></div><div><div><div>2</div></div><div><span>    </span><span># dependencies = [</span></div></div><div><div><div>3</div></div><div><span>    </span><span>#   "beautifulsoup4",</span></div></div><div><div><div>4</div></div><div><span>    </span><span># ]</span></div></div><div><div><div>5</div></div><div><span>    </span><span># ///</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>from</span><span> bs4 </span><span>import</span><span> BeautifulSoup</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>html </span><span>=</span><span> </span><span>'&lt;html&gt;&lt;body&gt;&lt;h1&gt;Welcome&lt;/h1&gt;&lt;p class="info"&gt;This is a test.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;'</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span><span>(</span><span>BeautifulSoup</span><span>(html, </span></span><span>"html.parser"</span><span><span>).</span><span>select_one</span><span>(</span></span><span>"p.info"</span><span><span>).</span><span>get_text</span><span>())</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用 <a href="https://docs.astral.sh/uv/" target="_blank">uv</a>（推荐）运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>uv</span><span> </span><span>run</span><span> </span><span>scripts/extract.py</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>uv run</code> 会创建一个隔离环境，安装声明的依赖项，并运行脚本。<a href="https://pipx.pypa.io/" target="_blank">pipx</a>（<code>pipx run scripts/extract.py</code>）也支持 PEP 723。</p><ul>
<li>使用 <a href="https://peps.python.org/pep-0508/" target="_blank">PEP 508</a> 说明符锁定版本：<code>"beautifulsoup4&gt;=4.12,&lt;5"</code>。</li>
<li>使用 <code>requires-python</code> 约束 Python 版本。</li>
<li>使用 <code>uv lock --script</code> 创建锁文件以实现完全可重现性。</li>
</ul></section>
<section><h2>4. 为代理使用设计脚本<a href="#4-为代理使用设计脚本"><span>#</span></a></h2><p>当代理运行你的脚本时，它通过读取 stdout 和 stderr 来决定下一步做什么。一些设计选择能让脚本对代理来说更易于使用。</p><section><h3>4.1 避免交互式提示<a href="#41-避免交互式提示"><span>#</span></a></h3><p>这是代理执行环境的硬性要求。代理在非交互式 shell 中操作——它们无法响应 TTY 提示、密码对话框或确认菜单。阻塞在交互式输入上的脚本会无限挂起。</p><p>通过命令行标志、环境变量或 stdin 接受所有输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 错误：挂起等待输入</span></div></div><div><div><div>2</div></div><div><span>$ python scripts/deploy.py</span></div></div><div><div><div>3</div></div><div><span>目标环境：_</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 正确：清晰的错误并附带指导</span></div></div><div><div><div>6</div></div><div><span>$ python scripts/deploy.py</span></div></div><div><div><div>7</div></div><div><span>错误：--env 是必需的。选项：development, staging, production。</span></div></div><div><div><div>8</div></div><div><span>用法：python scripts/deploy.py --env staging --tag v1.2.3</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4.2 使用 <code>--help</code> 文档化用法<a href="#42-使用---help-文档化用法"><span>#</span></a></h3><p><code>--help</code> 输出是代理学习脚本接口的主要方式。包含简要描述、可用标志和使用示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Usage: scripts/process.py [OPTIONS] INPUT_FILE</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>处理输入数据并生成摘要报告。</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>Options:</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>--format FORMAT    输出格式：json, csv, table（默认：json）</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>--output FILE      将输出写入 FILE 而不是 stdout</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>--verbose          将进度信息打印到 stderr</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Examples:</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>scripts/process.py data.csv</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>scripts/process.py --format csv --output report.csv data.csv</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保持简洁——输出会进入代理的上下文窗口，与它正在处理的其他内容一起。</p><section><h4>4.2.1 <code>--help</code> 文档化用法与 Docstring 的不同<a href="#421---help-文档化用法与-docstring-的不同"><span>#</span></a></h4><p>问得很好！这个问题触及了 agent skill 设计的核心思维。虽然两者看起来都是”给脚本写说明”，但服务对象和使用方式完全不同：</p><p>核心区别：运行时 vs 源码级</p>

<table><thead><tr><th></th><th><code>--help</code> 文档化用法</th><th>Docstring（<code>"""……"""</code>）</th></tr></thead><tbody><tr><td><strong>获取方式</strong></td><td>运行命令：<code>python script.py --help</code></td><td>读取源码文件内容</td></tr><tr><td><strong>面向谁</strong></td><td><strong>使用者</strong>（人类终端用户 + AI agent）</td><td><strong>开发者</strong>（读代码的人）</td></tr><tr><td><strong>要什么能力</strong></td><td>只需要能执行命令</td><td>需要能读文件</td></tr><tr><td><strong>内容聚焦</strong></td><td>接口契约：参数、选项、示例</td><td>实现说明：算法逻辑、设计意图</td></tr></tbody></table><p>我们可以用一个 agent skill 场景来理解：</p><p>假设你的 vault 里有一个数据处理脚本，AI agent 需要调用它。两种方式的过程对比：</p><p><strong>方式一：Agent 读 Docstring（读源码）</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1. Agent 找到脚本文件路径</span></div></div><div><div><div>2</div></div><div><span>2. Agent 用 read 工具读取整个文件</span></div></div><div><div><div>3</div></div><div><span>3. Agent 在一堆 import、函数定义中翻找 """...""" 注释</span></div></div><div><div><div>4</div></div><div><span>4. Agent 自己推断怎么调用</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>→ 消耗大量上下文窗口，还要”猜”用法。</p><p><strong>方式二：Agent 读 —help（运行时查询）</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1. Agent 直接运行: python script.py --help</span></div></div><div><div><div>2</div></div><div><span>2. 得到清晰简洁的：</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Usage: script.py [OPTIONS] INPUT</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Options:</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>--format json|csv</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>Examples:</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>script.py data.csv</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>→ 零猜测，所见即所得，上下文窗口高效。</p><p>一个直观的类比是：</p><blockquote><p><strong>Docstring</strong> 像产品说明书印在机器外壳背面——你得走到机器跟前仔细看。</p><p><strong><code>--help</code></strong> 像按一下机器上的”帮助”按钮——机器直接告诉你它能做什么。</p></blockquote><p>对于 agent 来说，<strong>按按钮的成本远低于绕到背后读说明书</strong>。Agent 的默认行为是执行命令获取帮助，而不是打开源文件阅读。这就是为什么 skill 设计规范强调 <code>--help</code> 作为”主要接口发现机制”。</p><hr /><p><strong>那 Docstring 扮演的角色</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""数据预处理模块 —— 负责清洗和标准化原始传感器数据。    ← Docstring：给读代码的人看</span></div></div><div><div><div>2</div></div><div><span>实现基于滑动窗口的异常值检测算法（详见 RFC-42）。</span></div></div><div><div><div>3</div></div><div><span>"""</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>import</span><span> argparse</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>def</span><span> </span><span>main</span><span>():</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>parser </span><span>=</span><span> argparse.</span><span>ArgumentParser</span><span>(</span></div></div><div><div><div>9</div></div><div><span>        </span><span>description</span><span>=</span><span>"处理输入数据并生成摘要报告。  ← --help：给使用者看</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>简单原则</strong>：Docstring 写”为什么这么设计”，<code>--help</code> 写”怎么用”。Agent 只关心后者。</p></section></section><section><h3>4.3 编写有用的错误消息<a href="#43-编写有用的错误消息"><span>#</span></a></h3><p>当代理收到错误时，消息内容直接影响其下一步尝试。含糊的 “错误：无效输入” 会浪费一次尝试。相反，说明哪里出错、期望什么以及可以尝试什么：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>错误：--format 必须是以下之一：json, csv, table。</span></div></div><div><div><div>2</div></div><div><span><span>       </span></span><span>收到："xml"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4.4 使用结构化输出<a href="#44-使用结构化输出"><span>#</span></a></h3><p>优先使用结构化格式——JSON、CSV、TSV——而非自由格式文本。结构化格式可被代理和标准工具（<code>jq</code>、<code>cut</code>、<code>awk</code>）消费，使你的脚本在管道中具有可组合性。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 空格对齐——难以程序化解析</span></div></div><div><div><div>2</div></div><div><span>NAME          STATUS    CREATED</span></div></div><div><div><div>3</div></div><div><span>my-service    running   2025-01-15</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 带分隔符——明确的字段边界</span></div></div><div><div><div>6</div></div><div><span>{"name": "my-service", "status": "running", "created": "2025-01-15"}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>将数据与诊断信息分离：</strong> 将结构化数据发送到 stdout，将进度消息、警告和其他诊断信息发送到 stderr。这让代理既能捕获干净、可解析的输出，又能在需要时访问诊断信息。</p></section><section><h3>4.5 其他考虑<a href="#45-其他考虑"><span>#</span></a></h3><ul>
<li><strong>幂等性。</strong> 代理可能会重试命令。“如果不存在则创建”比“创建并在重复时失败”更安全。</li>
<li><strong>输入约束。</strong> 以清晰的错误拒绝模糊输入，而不是猜测。尽可能使用枚举和封闭集合。</li>
<li><strong>试运行支持。</strong> 对于破坏性或有状态的操作，<code>--dry-run</code> 标志让代理预览将要发生的事情。</li>
<li><strong>有意义的退出码。</strong> 为不同的失败类型（未找到、无效参数、身份验证失败）使用不同的退出码，并在 <code>--help</code> 输出中记录它们，以便代理了解每个代码的含义。</li>
<li><strong>安全的默认值。</strong> 考虑破坏性操作是否需要明确的确认标志（<code>--confirm</code>、<code>--force</code>）或其他适合风险级别的防护措施。</li>
<li><strong>可预测的输出大小。</strong> 许多代理框架会自动截断超过阈值（例如 10-30K 字符）的工具输出，可能丢失关键信息。如果脚本可能产生大量输出，默认使用摘要或合理限制，并支持如 <code>--offset</code> 之类的标志，以便代理在需要时可以请求更多信息。或者，如果输出较大且不适合分页，要求代理传递 <code>--output</code> 标志，指定输出文件或 <code>-</code> 以明确选择 stdout。</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-05/</id>
      <title type="text">skill 评估</title>
      <published>2026-07-14T00:00:00.000Z</published>
      <updated>2026-07-14T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-05/"/>
      <summary type="text">介绍 skill 的评估流程、原则与技巧</summary>
      <content type="html"><![CDATA[<blockquote><p>如何使用基于评估的迭代来测试你的技能是否能产生良好的输出。</p></blockquote>
<p>你编写了一个技能，在一个提示上尝试了一下，似乎有效。但它在各种不同的提示下、在边缘情况下，是否都能可靠地工作？是否比没有技能时更好？运行结构化评估（evals）可以回答这些问题，并为你提供一个系统性地改进技能的反馈循环。</p>
<section><h2>1. 设计测试用例<a href="#1-设计测试用例"><span>#</span></a></h2><p>一个测试用例包含三个部分：</p><ul>
<li><strong>提示</strong>：一条真实的用户消息——类似用户实际会输入的内容。</li>
<li><strong>预期输出</strong>：对成功结果的人工可读描述。</li>
<li><strong>输入文件</strong>（可选）：技能需要处理的文件。</li>
</ul><p>将测试用例存储在技能目录内的 <code>evals/evals.json</code> 中：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"skill_name"</span><span>: </span><span>"csv-analyzer"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"evals"</span><span>: [</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"id"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"prompt"</span><span>: </span><span>"我有一份月度销售数据的 CSV 文件，在 data/sales_2025.csv。你能找出收入最高的 3 个月并生成一个柱状图吗？"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>"expected_output"</span><span>: </span><span>"一张柱状图图片，显示收入最高的 3 个月，带有标注的坐标轴和数值。"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>      </span><span>"files"</span><span>: [</span><span>"evals/files/sales_2025.csv"</span><span>]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>11</div></div><div><span>      </span><span>"id"</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>12</div></div><div><span>      </span><span>"prompt"</span><span>: </span><span>"我的下载文件夹里有个叫 customers.csv 的 CSV 文件，有些行缺少邮箱——你能清理一下并告诉我有多少行缺失吗？"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>      </span><span>"expected_output"</span><span>: </span><span>"一份处理了缺失邮箱的清理后 CSV，以及缺失数量的计数。"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>      </span><span>"files"</span><span>: [</span><span>"evals/files/customers.csv"</span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>编写良好测试提示的技巧：</strong></p><ul>
<li><strong>从 2-3 个测试用例开始。</strong> 在看到第一轮结果之前不要过度投入。后续可以扩充测试集。</li>
<li><strong>变化提示风格。</strong> 使用不同的措辞、详细程度和正式程度。一些提示应该随意（“嘿，能清理一下这个 csv 吗”），另一些则应精确（“解析 data/input.csv 中的 CSV，删除 B 列为空的行，并将结果写入 data/output.csv”）。</li>
<li><strong>覆盖边缘情况。</strong> 至少包含一个测试边界条件的提示——格式错误的输入、不寻常的请求，或技能指令可能存在歧义的情况。</li>
<li><strong>使用真实的上下文。</strong> 真实用户会提及文件路径、列名和个人背景。像 “处理这些数据” 这样的提示过于模糊，无法测试任何有用的东西。</li>
</ul><p>暂时不要担心定义具体的通过/失败检查——只需提供提示和预期输出。你将在看到第一轮运行产生的结果后，再添加详细的检查（称为 assertions）。</p></section>
<section><h2>2. 运行评估<a href="#2-运行评估"><span>#</span></a></h2><p>核心模式是每个测试用例运行两次：一次<strong>携带技能</strong>，一次<strong>不带技能</strong>（或使用先前版本）。这样你就有了一个可对比的基准。</p><section><h3>2.1 工作区结构<a href="#21-工作区结构"><span>#</span></a></h3><p>在技能目录旁边的工作区目录中组织评估结果。每完成一轮完整的评估循环，都获得自己的 <code>iteration-N/</code> 目录。在该目录内，每个测试用例都有一个评估目录，内含 <code>with_skill/</code> 和 <code>without_skill/</code> 子目录：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>csv-analyzer/</span></div></div><div><div><div>2</div></div><div><span>├── SKILL.md</span></div></div><div><div><div>3</div></div><div><span>└── evals/</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>└── evals.json</span></div></div><div><div><div>5</div></div><div><span>csv-analyzer-workspace/</span></div></div><div><div><div>6</div></div><div><span>└── iteration-1/</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>├── eval-top-months-chart/</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>│   ├── with_skill/</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>│   │   ├── outputs/       # 运行产生的文件</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>│   │   ├── timing.json    # Token 数和耗时</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>│   │   └── grading.json   # 断言结果</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>│   └── without_skill/</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>│       ├── outputs/</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>│       ├── timing.json</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>│       └── grading.json</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>├── eval-clean-missing-emails/</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>│   ├── with_skill/</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>│   │   ├── outputs/</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>│   │   ├── timing.json</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>│   │   └── grading.json</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>│   └── without_skill/</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>│       ├── outputs/</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>│       ├── timing.json</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>│       └── grading.json</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>└── benchmark.json         # 聚合统计</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>你手工编写的主要文件是 <code>evals/evals.json</code>。其他 JSON 文件（<code>grading.json</code>、<code>timing.json</code>、<code>benchmark.json</code>）是在评估过程中生成的——由代理、脚本或你本人生成。</p></section><section><h3>2.2 启动运行<a href="#22-启动运行"><span>#</span></a></h3><p>每次评估运行都应从干净上下文开始——不保留之前运行或技能开发过程中的任何残留状态。这能确保代理只遵循 <code>SKILL.md</code> 告诉它的内容。在支持子代理的环境（例如 Claude Code）中，这种隔离是天然的：每个子任务都从全新状态开始。如果没有子代理，可以为每次运行使用单独的会话。</p><p>对于每次运行，提供：</p><ul>
<li>技能路径（基线运行则不提供）</li>
<li>测试提示</li>
<li>任何输入文件</li>
<li>输出目录</li>
</ul><p>以下是针对单次带技能运行，你需要提供给代理的指令示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>执行此任务：</span></div></div><div><div><div>2</div></div><div><span>- 技能路径：/path/to/csv-analyzer</span></div></div><div><div><div>3</div></div><div><span>- 任务：我有一份月度销售数据的 CSV 文件，在 data/sales_2025.csv。</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>你能找出收入最高的 3 个月并生成一个柱状图吗？</span></div></div><div><div><div>5</div></div><div><span>- 输入文件：evals/files/sales_2025.csv</span></div></div><div><div><div>6</div></div><div><span>- 将输出保存到：csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对于基线，使用相同的提示但不提供技能路径，并保存到 <code>without_skill/outputs/</code>。</p><p>当改进现有技能时，使用先前版本作为基线。在编辑之前对其进行快照（<code>cp -r &lt;skill-path&gt; &lt;workspace&gt;/skill-snapshot/</code>），将基线运行指向快照，并保存到 <code>old_skill/outputs/</code> 而不是 <code>without_skill/</code>。</p></section><section><h3>2.3 捕获计时数据<a href="#23-捕获计时数据"><span>#</span></a></h3><p>计时数据让你能对比技能相对于基线消耗的时间和 token 数——一个能显著提升输出质量但使 token 使用量增加两倍的技能，与一个既更好又更便宜的技能，是不同性质的取舍。每次运行完成后，记录 token 数和耗时：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"total_tokens"</span><span>: </span><span>84852</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"duration_ms"</span><span>: </span><span>23332</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Tip</div></div><div><p>在 Claude Code 中，当子代理任务完成时，<a href="https://platform.claude.com/docs/en/agent-sdk/typescript#sdk-task-notification-message" target="_blank">任务完成通知</a>会包含 <code>total_tokens</code> 和 <code>duration_ms</code>。请立即保存这些值——它们不会在其他地方持久化。</p></div></div></section></section>
<section><h2>3. 编写断言<a href="#3-编写断言"><span>#</span></a></h2><p>断言是关于输出应包含或达成哪些内容的可验证陈述。<strong>在你看完第一轮输出后再添加它们</strong>——你通常需要在技能运行之后才知道“好”是什么样子。</p><p>好的断言：</p><ul>
<li><code>"输出文件是有效的 JSON"</code> —— 可通过编程验证。</li>
<li><code>"柱状图有标注的坐标轴"</code> —— 具体且可观察。</li>
<li><code>"报告包含至少 3 条建议"</code> —— 可计数。</li>
</ul><p>弱的断言：</p><ul>
<li><code>"输出很好"</code> —— 过于模糊，无法评分。</li>
<li><code>"输出精确使用短语 'Total Revenue: $X'"</code> —— 过于脆弱；措辞不同但正确的输出会失败。</li>
</ul><p>并非所有东西都需要断言。某些质量属性——写作风格、视觉设计、输出是否“感觉对了”——难以分解为通过/失败检查。这些更适合在人工评审中发现。请将断言保留给可以客观检查的内容。</p><p>将断言添加到 <code>evals/evals.json</code> 的每个测试用例中：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"skill_name"</span><span>: </span><span>"csv-analyzer"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"evals"</span><span>: [</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"id"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"prompt"</span><span>: </span><span>"我有一份月度销售数据的 CSV 文件，在 data/sales_2025.csv。你能找出收入最高的 3 个月并生成一个柱状图吗？"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>"expected_output"</span><span>: </span><span>"一张柱状图图片，显示收入最高的 3 个月，带有标注的坐标轴和数值。"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>      </span><span>"files"</span><span>: [</span><span>"evals/files/sales_2025.csv"</span><span>],</span></div></div><div><div><div>9</div></div><div><span>      </span><span>"assertions"</span><span>: [</span></div></div><div><div><div>10</div></div><div><span>        </span><span>"输出包含一张柱状图图片文件"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"图表正好显示 3 个月"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"两个坐标轴都有标签"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"图表标题或说明提到了收入"</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>4. 对输出进行评分<a href="#4-对输出进行评分"><span>#</span></a></h2><p>评分是指根据实际输出评估每个断言，并记录 <strong>通过（PASS）</strong> 或 <strong>失败（FAIL）</strong>，附上具体证据。证据应引用或参考输出，而不仅仅是陈述观点。</p><p>最简单的方法是将输出和断言交给 LLM，让其逐条评估。对于可通过代码检查的断言（有效 JSON、正确的行数、存在符合预期尺寸的文件），应使用验证脚本——对于机械性检查，脚本比 LLM 判断更可靠，并且可跨迭代复用。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"assertion_results"</span><span>: [</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"text"</span><span>: </span><span>"输出包含一张柱状图图片文件"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"passed"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"evidence"</span><span>: </span><span>"在 outputs 目录中找到 chart.png（45KB）"</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>9</div></div><div><span>      </span><span>"text"</span><span>: </span><span>"图表正好显示 3 个月"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>      </span><span>"passed"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>"evidence"</span><span>: </span><span>"图表显示三月、七月和十一月的柱状条"</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>14</div></div><div><span>      </span><span>"text"</span><span>: </span><span>"两个坐标轴都有标签"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>      </span><span>"passed"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>16</div></div><div><span>      </span><span>"evidence"</span><span>: </span><span>"Y 轴标注了 'Revenue ($)'，但 X 轴没有标签"</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>19</div></div><div><span>      </span><span>"text"</span><span>: </span><span>"图表标题或说明提到了收入"</span><span>,</span></div></div><div><div><div>20</div></div><div><span>      </span><span>"passed"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>21</div></div><div><span>      </span><span>"evidence"</span><span>: </span><span>"图表标题为 'Top 3 Months by Revenue'"</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>24</div></div><div><span>  </span><span>"summary"</span><span>: {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>"passed"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>26</div></div><div><span>    </span><span>"failed"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>"total"</span><span>: </span><span>4</span><span>,</span></div></div><div><div><div>28</div></div><div><span>    </span><span>"pass_rate"</span><span>: </span><span>0.75</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>4.1 评分原则<a href="#41-评分原则"><span>#</span></a></h3><ul>
<li><strong>通过必须有具体证据。</strong> 不要给予怀疑的好处。如果一个断言说“包含摘要”，而输出中有一个标题为“摘要”的部分但只有一句含糊的话，那就是 <strong>失败</strong>——标签存在，但实质内容不足。</li>
<li><strong>不仅检查结果，也要检查断言本身。</strong> 在评分过程中，注意断言是否过于容易（无论技能质量如何总是通过）、过于困难（即使输出很好也总是失败）或无法验证（无法仅从输出中检查）。在下一轮迭代中修正这些问题。</li>
</ul><div><div><div></div><div>Tip</div></div><div><p>对于比较两个技能版本，可以尝试<strong>盲测对比</strong>：将两份输出交给 LLM 评判者，但不透露哪个来自哪个版本。评判者根据自己设定的标准对整体质量——组织、格式、可用性、润色程度——打分，从而避免对哪个版本“应该”更好的偏见。这可以作为断言评分的补充：两份输出可能都通过了所有断言，但整体质量却存在显著差异。</p></div></div></section></section>
<section><h2>5. 聚合结果<a href="#5-聚合结果"><span>#</span></a></h2><p>在迭代中的每次运行都完成评分后，计算每个配置的汇总统计，并将结果保存到 <code>benchmark.json</code> 中，放在评估目录旁边（例如 <code>csv-analyzer-workspace/iteration-1/benchmark.json</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"run_summary"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"with_skill"</span><span>: {</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"pass_rate"</span><span>: { </span><span>"mean"</span><span>: </span><span>0.83</span><span>, </span><span>"stddev"</span><span>: </span><span>0.06</span><span> },</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"time_seconds"</span><span>: { </span><span>"mean"</span><span>: </span><span>45.0</span><span>, </span><span>"stddev"</span><span>: </span><span>12.0</span><span> },</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"tokens"</span><span>: { </span><span>"mean"</span><span>: </span><span>3800</span><span>, </span><span>"stddev"</span><span>: </span><span>400</span><span> }</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"without_skill"</span><span>: {</span></div></div><div><div><div>9</div></div><div><span>      </span><span>"pass_rate"</span><span>: { </span><span>"mean"</span><span>: </span><span>0.33</span><span>, </span><span>"stddev"</span><span>: </span><span>0.10</span><span> },</span></div></div><div><div><div>10</div></div><div><span>      </span><span>"time_seconds"</span><span>: { </span><span>"mean"</span><span>: </span><span>32.0</span><span>, </span><span>"stddev"</span><span>: </span><span>8.0</span><span> },</span></div></div><div><div><div>11</div></div><div><span>      </span><span>"tokens"</span><span>: { </span><span>"mean"</span><span>: </span><span>2100</span><span>, </span><span>"stddev"</span><span>: </span><span>300</span><span> }</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>13</div></div><div><span>    </span><span>"delta"</span><span>: {</span></div></div><div><div><div>14</div></div><div><span>      </span><span>"pass_rate"</span><span>: </span><span>0.50</span><span>,</span></div></div><div><div><div>15</div></div><div><span>      </span><span>"time_seconds"</span><span>: </span><span>13.0</span><span>,</span></div></div><div><div><div>16</div></div><div><span>      </span><span>"tokens"</span><span>: </span><span>1700</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>delta</code> 告诉你技能的成本（更多时间、更多 token）和收益（更高的通过率）。一个增加 13 秒但将通过率提升 50 个百分点的技能，很可能是值得的。一个 token 使用量翻倍但仅带来 2 个百分点提升的技能，可能就不值得了。</p><div><div><div></div><div>Tip</div></div><div><p>标准差（<code>stddev</code>）只有在每个评估有多次运行时才有意义。在早期迭代中，如果只有 2-3 个测试用例且各运行一次，应关注原始通过计数和 delta——当你扩展测试集并对每个评估进行多次运行后，统计指标才变得有用。</p></div></div></section>
<section><h2>6. 分析模式<a href="#6-分析模式"><span>#</span></a></h2><p>聚合统计可能会掩盖重要的模式。在计算基准后：</p><ul>
<li><strong>删除或替换在两个配置中总是通过的断言。</strong> 这些断言不会告诉你任何有用信息——模型在没有技能的情况下也能很好地处理它们。它们抬高了带技能的通过率，却没有反映实际技能价值。</li>
<li><strong>调查在两个配置中总是失败的断言。</strong> 要么断言本身有问题（要求模型无法做到的事情），要么测试用例太难，要么断言检查了错误的内容。在下一轮迭代前修复这些问题。</li>
<li><strong>研究在带技能时通过、不带技能时失败的断言。</strong> 这些是技能明显增加价值的地方。理解<em>为什么</em>——哪些指令或脚本起到了作用？</li>
<li><strong>当结果在不同运行之间不一致时，收紧指令。</strong> 如果同一个评估有时通过有时失败（在基准中体现为高 <code>stddev</code>），可能评估本身不稳定（对模型的随机性敏感），或者技能的指令可能过于模糊，导致模型每次理解不同。通过增加示例或更具体的指导来减少歧义。</li>
<li><strong>检查时间和 token 异常值。</strong> 如果某个评估耗时是其他评估的 3 倍，阅读其执行记录（运行期间模型所做操作的完整日志）以找到瓶颈。</li>
</ul></section>
<section><h2>7. 人工评审结果<a href="#7-人工评审结果"><span>#</span></a></h2><p>断言评分和模式分析能发现很多问题，但它们只能检查你想到要写断言的那些方面。人工评审者带来新的视角——发现你未预料到的问题，注意到输出在技术上正确但未切中要点，或发现难以用通过/失败检查表达的问题。对于每个测试用例，请评审实际输出及其评分。</p><p>为每个测试用例记录具体反馈，并保存到工作区中（例如，作为与评估目录并列的 <code>feedback.json</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"eval-top-months-chart"</span><span>: </span><span>"图表缺少坐标轴标签，且月份按字母顺序排列而非时间顺序。"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"eval-clean-missing-emails"</span><span>: </span><span>""</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>“图表缺少坐标轴标签”是可操作的；“看起来不好”则不是。空反馈意味着输出看起来没问题——该测试用例通过了你的评审。在迭代步骤中，将改进重点放在你有具体意见的测试用例上。</p></section>
<section><h2>8. 迭代技能<a href="#8-迭代技能"><span>#</span></a></h2><p>在完成评分和评审后，你拥有三个信号来源：</p><ul>
<li><strong>失败的断言</strong>指向具体的缺口——遗漏的步骤、不清晰的指令，或技能未处理的情况。</li>
<li><strong>人工反馈</strong>指向更广泛的质量问题——方法错误、输出结构不佳，或技能产生了技术上正确但无用的结果。</li>
<li><strong>执行记录</strong>揭示了<em>为什么</em>出错。如果代理忽略了一条指令，该指令可能模棱两可。如果代理在无效步骤上浪费时间，这些指令可能需要简化或删除。</li>
</ul><p>将这些信号转化为技能改进的最有效方式，是同时将这三个来源以及当前的 <code>SKILL.md</code> 交给 LLM，并要求它提出修改建议。LLM 可以综合失败断言、评审意见和记录行为中的模式，而这些模式手动连接会很繁琐。在提示 LLM 时，请包含以下指导原则：</p><ul>
<li><strong>从反馈中泛化。</strong> 技能将在许多不同的提示下使用，而不仅仅是测试用例。修复应广泛解决根本问题，而不是为特定示例添加窄补丁。</li>
<li><strong>保持技能精简。</strong> 更少、更好的指令往往胜过详尽的规则。如果记录显示有浪费的工作（不必要的验证、不需要的中间输出），请删除这些指令。如果尽管添加了更多规则，通过率仍停滞不前，可能技能约束过多了——尝试删除一些指令，看看结果是否保持不变或有所改善。</li>
<li><strong>解释原因。</strong> 基于推理的指令（“做 X，因为 Y 往往导致 Z”）比刚性指令（“总是做 X，绝不做 Y”）效果更好。当模型理解目的时，它们会更可靠地遵循指令。</li>
<li><strong>捆绑重复工作。</strong> 如果每次测试运行都独立编写了类似的辅助脚本（图表构建器、数据解析器），这说明应将脚本打包到技能的 <code>scripts/</code> 目录中。参见 <a href="/posts/skill-06/">skill 脚本</a> 了解具体做法。</li>
</ul><section><h3>8.1 循环<a href="#81-循环"><span>#</span></a></h3><ol>
<li>将评估信号和当前 <code>SKILL.md</code> 交给 LLM，要求其提出改进建议。</li>
<li>评审并应用修改。</li>
<li>在新的 <code>iteration-&lt;N+1&gt;/</code> 目录中重新运行所有测试用例。</li>
<li>对结果进行评分和聚合。</li>
<li>进行人工评审。重复。</li>
</ol><p>当结果令你满意、反馈始终为空，或你不再看到迭代之间有意义的改进时，即可停止。</p><div><div><div></div><div>Tip</div></div><div><p><a href="https://github.com/anthropics/skills/tree/main/skills/skill-creator" target="_blank"><code>skill-creator</code></a> 技能可以自动化该工作流的大部分——运行评估、评分断言、聚合基准，并呈现结果供人工评审。具体流程参见 <a href="/posts/skill-08/">skill-creator</a>.</p></div></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-04/</id>
      <title type="text">优化 skill 描述</title>
      <published>2026-07-13T00:00:00.000Z</published>
      <updated>2026-07-13T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-04/"/>
      <summary type="text">介绍如何优化 skill description</summary>
      <content type="html"><![CDATA[<blockquote><p>如何改进技能的描述，使其能在相关提示下可靠触发。</p></blockquote>
<p>技能只有在被激活时才有用。<code>SKILL.md</code> 前置元数据中的 <code>description</code> 字段是代理决定是否为给定任务加载技能的主要机制。描述过于简略会导致技能在该触发时不触发；描述过于宽泛则会导致它在不该触发时触发。</p>
<p>本指南介绍如何系统地测试和改进技能描述，以提高触发的准确性。</p>
<section><h2>1. 技能触发的工作原理<a href="#1-技能触发的工作原理"><span>#</span></a></h2><p>代理使用 <a href="/posts/skill-01/">渐进式披露</a> 来管理上下文。启动时，它们只加载每个可用技能的 <code>name</code> 和 <code>description</code>——足以判断技能何时可能相关。当用户任务与 <code>description</code> 匹配时，代理会将完整的 <code>SKILL.md</code> 读入上下文并遵循其指令。</p><p>这意味着 <code>description</code> 承担了触发的全部责任。如果 <code>description</code> 没有传达技能的适用场景，代理就不知道去调用它。</p><p>一个重要细节：<strong>代理通常只在任务需要它们自身无法处理的知识或能力时，才会考虑技能</strong>。像 “读取这个 PDF” 这样简单、一步到位的请求，即使描述完美匹配，也可能不会触发 PDF 技能，因为代理可以用基础工具处理。涉及专业知识——不熟悉的 API、领域特定工作流或非通用格式——的任务，才是精心编写的描述能发挥作用的场景。</p></section>
<section><h2>2. 编写有效的描述<a href="#2-编写有效的描述"><span>#</span></a></h2><p>在测试之前，了解好的描述是什么样的会很有帮助。以下是一些原则：</p><ul>
<li><strong>使用祈使语气</strong>。将描述表述为对代理的指令：“当……时使用此技能”，而不是 “此技能执行……”。代理需要决定是否行动，所以告诉它何时行动。</li>
<li><strong>关注用户意图，而非实现细节</strong>。描述用户试图达成什么，而不是技能的内部机制。代理是根据用户提出的需求进行匹配的。</li>
<li><strong>宁可“强势”一些</strong>。明确列出技能适用的上下文，包括用户没有直接提及领域的情况：“即使用户没有明确提到‘CSV’或‘分析’”。</li>
<li><strong>保持简洁</strong>。几句话到一小段通常就足够了——长度要足以涵盖技能的适用范围，又足够短，不至于在多个技能中膨胀代理的上下文。 <a href="/posts/skill-02/">agentskills.io 规范</a> 强制规定了 1024 个字符的硬限制。</li>
</ul></section>
<section><h2>3. 设计触发评估查询<a href="#3-设计触发评估查询"><span>#</span></a></h2><p>为了测试触发，你需要一组评估查询——这些查询是真实的用户提示，并标注了它们是否应触发你的技能。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>{ </span><span>"query"</span><span>: </span><span>"我有个电子表格在 ~/data/q4_results.xlsx，C 列是收入，D 列是支出——你能添加一个利润率列，并把低于 10% 的标出来吗？"</span><span>, </span><span>"should_trigger"</span><span>: </span><span>true</span><span> },</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>{ </span><span>"query"</span><span>: </span><span>"把这个 json 文件转成 yaml 最快的方法是什么"</span><span>, </span><span>"should_trigger"</span><span>: </span><span>false</span><span> }</span></div></div><div><div><div>4</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>目标大约 20 个查询：8-10 个应触发和 8-10 个不应触发。</p><section><h3>3.1 应触发查询<a href="#31-应触发查询"><span>#</span></a></h3><p>这些查询测试描述是否捕捉了技能的适用范围。请从多个维度变化：</p><ul>
<li><strong>措辞</strong>：一些正式，一些随意，一些包含拼写错误或缩写。</li>
<li><strong>明确性</strong>：一些直接命名技能的领域（“分析这个 CSV”），另一些描述需求而不命名它（“我老板想从这个数据文件里看个图”）。</li>
<li><strong>详细程度</strong>：混合简洁提示和富含上下文的提示——简短的“分析我的销售 CSV 并做图”与包含文件路径、列名和背景故事的较长消息。</li>
<li><strong>复杂度</strong>：变化步骤数和决策点的数量。包括单步任务和多步工作流，以测试当目标任务隐藏在更大链中时，代理能否识别技能的相关性。</li>
</ul><p>最有价值的应触发查询是那些技能会有帮助，但从查询本身来看关联并不明显的情况。这些是描述措辞起关键作用的案例——如果查询已经明确要求技能所做的，那么任何合理的描述都会触发。</p></section><section><h3>3.2 不应触发查询<a href="#32-不应触发查询"><span>#</span></a></h3><p>最有价值的负例测试是<strong>近似匹配（near-misses）</strong>——与技能共享关键词或概念，但实际需要不同内容的查询。这些测试描述是否精确，而不仅仅是宽泛。</p><p>对于 CSV 分析技能，弱的负例是：</p><ul>
<li><code>"写一个斐波那契函数"</code> —— 明显无关，无测试价值。</li>
<li><code>"今天天气怎么样？"</code> —— 无关键词重叠，太简单。</li>
</ul><p>强的负例：</p><ul>
<li><code>"我需要更新我的 Excel 预算电子表格中的公式"</code> —— 共享“电子表格”和“数据”概念，但需要的是 Excel 编辑，而非 CSV 分析。</li>
<li><code>"你能写一个 Python 脚本读取 CSV 并把每一行上传到我们的 PostgreSQL 数据库吗"</code> —— 涉及 CSV，但任务是数据库 ETL，而非分析。</li>
</ul></section><section><h3>3.3 真实性技巧<a href="#33-真实性技巧"><span>#</span></a></h3><p>真实用户提示包含通用测试查询所缺乏的上下文。请包含：</p><ul>
<li>文件路径（<code>~/Downloads/report_final_v2.xlsx</code>）</li>
<li>个人背景（<code>"我经理让我..."</code>）</li>
<li>具体细节（列名、公司名称、数据值）</li>
<li>口语化表达、缩写和偶尔的拼写错误</li>
</ul></section></section>
<section><h2>4. 测试描述是否触发<a href="#4-测试描述是否触发"><span>#</span></a></h2><p>基本方法：在安装了技能的代理中运行每个查询，观察代理是否调用该技能。确保技能已注册且可被代理发现——具体方式因客户端而异（例如，技能目录、配置文件或 CLI 标志）。</p><p>大多数代理客户端提供某种形式的可观测性——执行日志、工具调用历史或详细输出——让你能查看运行期间咨询了哪些技能。请查阅客户端文档了解详情。如果代理加载了你的 <code>SKILL.md</code>，则表示技能已触发；如果代理未咨询它就直接进行，则表示未触发。</p><p>一个查询“通过”的条件是：</p><ul>
<li><code>should_trigger</code> 为 <code>true</code> 且技能被调用，或</li>
<li><code>should_trigger</code> 为 <code>false</code> 且技能未被调用。</li>
</ul><section><h3>4.1 多次运行<a href="#41-多次运行"><span>#</span></a></h3><p>模型行为是非确定性的——同一个查询可能在某次运行中触发技能，但下一次不触发。每个查询运行多次（3 次是一个合理的起点），并计算<strong>触发率</strong>：技能被调用的运行比例。</p><p>一个应触发查询，如果其触发率高于阈值（0.5 是一个合理的默认值），则通过。一个不应触发查询，如果其触发率低于该阈值，则通过。</p><p>20 个查询，每个 3 次，就是 60 次调用。你需要将其脚本化。以下是通用结构——请将 <code>check_triggered</code> 中的 <code>claude</code> 调用和检测逻辑替换为你代理客户端提供的相应方式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/bin/bash</span></div></div><div><div><div>2</div></div><div><span>QUERIES_FILE</span><span>=</span><span>"</span><span><span>${1</span><span>:?</span></span><span>Usage</span><span>:</span><span> </span><span>$0</span><span> </span><span>&lt;queries</span><span>.</span><span>json&gt;</span><span>}</span><span>"</span></div></div><div><div><div>3</div></div><div><span>SKILL_NAME</span><span>=</span><span>"my-skill"</span></div></div><div><div><div>4</div></div><div><span>RUNS</span><span>=</span><span>3</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 此示例使用 Claude Code 的 JSON 输出来检查 Skill 工具调用。</span></div></div><div><div><div>7</div></div><div><span># 请将此函数替换为你代理客户端的检测逻辑。</span></div></div><div><div><div>8</div></div><div><span># 如果技能被调用，应返回 0（成功），否则返回 1。</span></div></div><div><div><div>9</div></div><div><span>check_triggered</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>  </span><span>local</span><span> </span><span>query</span><span>=</span><span>"</span><span>$1</span><span>"</span></div></div><div><div><div>11</div></div><div><span>  </span><span>claude</span><span> </span><span>-p</span><span> </span><span>"</span><span>$query</span><span>"</span><span> </span><span>--output-format</span><span> </span><span>json</span><span> 2&gt;</span><span>/dev/null</span><span> </span><span>\</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>| </span><span>jq</span><span> </span><span>-e</span><span> </span><span>--arg</span><span> </span><span>skill</span><span> </span><span>"</span><span>$SKILL_NAME</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>13</div></div><div><span>      </span><span>'any(.messages[].content[]; .type == "tool_use" and .name == "Skill" and .input.skill == $skill)'</span><span> </span><span>\</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>&gt; </span><span>/dev/null</span><span> 2&gt;&amp;1</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>count</span><span><span>=</span><span>$(</span></span><span>jq</span><span> </span><span>length</span><span> </span><span>"</span><span>$QUERIES_FILE</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> $(</span><span>seq</span><span> </span><span>0</span><span> $((</span><span>count</span><span> </span><span>-</span><span> </span><span>1</span><span>))); </span><span>do</span></div></div><div><div><div>19</div></div><div><span>  </span><span>query</span><span><span>=</span><span>$(</span></span><span>jq</span><span> </span><span>-r</span><span> </span><span>".[</span><span>$i</span><span>].query"</span><span> </span><span>"</span><span>$QUERIES_FILE</span><span>"</span><span>)</span></div></div><div><div><div>20</div></div><div><span>  </span><span>should_trigger</span><span><span>=</span><span>$(</span></span><span>jq</span><span> </span><span>-r</span><span> </span><span>".[</span><span>$i</span><span>].should_trigger"</span><span> </span><span>"</span><span>$QUERIES_FILE</span><span>"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>  </span><span>triggers</span><span>=</span><span>0</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>  </span><span>for</span><span> </span><span>run</span><span> </span><span>in</span><span> $(</span><span>seq</span><span> </span><span>1</span><span> </span><span>$RUNS</span><span>); </span><span>do</span></div></div><div><div><div>24</div></div><div><span>    </span><span>check_triggered</span><span> </span><span>"</span><span>$query</span><span>"</span><span> &amp;&amp; </span><span>triggers</span><span><span>=</span><span>$((</span></span><span>triggers</span><span> </span><span>+</span><span> </span><span>1</span><span>))</span></div></div><div><div><div>25</div></div><div><span>  </span><span>done</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>  </span><span>jq</span><span> </span><span>-n</span><span> </span><span>\</span></div></div><div><div><div>28</div></div><div><span>    </span><span>--arg</span><span> </span><span>query</span><span> </span><span>"</span><span>$query</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>29</div></div><div><span>    </span><span>--argjson</span><span> </span><span>should_trigger</span><span> </span><span>"</span><span>$should_trigger</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>30</div></div><div><span>    </span><span>--argjson</span><span> </span><span>triggers</span><span> </span><span>"</span><span>$triggers</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>31</div></div><div><span>    </span><span>--argjson</span><span> </span><span>runs</span><span> </span><span>"</span><span>$RUNS</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>32</div></div><div><span>    </span><span>'{query: $query, should_trigger: $should_trigger, triggers: $triggers, runs: $runs, trigger_rate: ($triggers / $runs)}'</span></div></div><div><div><div>33</div></div><div><span>done</span><span> | </span><span>jq</span><span> </span><span>-s</span><span> </span><span>'.'</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div></div><div>Tip</div></div><div><p>如果你的代理客户端支持，一旦结果明确（代理要么咨询了技能，要么开始工作而未使用它），你可以提前停止运行。这可以显著减少运行完整评估集所需的时间和成本。</p></div></div></section></section>
<section><h2>5. 使用训练/验证拆分避免过拟合<a href="#5-使用训练验证拆分避免过拟合"><span>#</span></a></h2><p>如果你针对所有查询优化描述，就有过拟合的风险——编写出的描述仅适用于这些特定措辞，而在新查询上失败。</p><p>解决方案是拆分查询集：</p><ul>
<li><strong>训练集（约 60%）</strong>：用于识别失败并指导改进的查询。</li>
<li><strong>验证集（约 40%）</strong>：留出的查询，仅用于检查改进是否具有泛化性。</li>
</ul><p>确保两个集合都包含应触发和不应触发查询的适当比例——不要不小心把所有正例都放在一个集合中。随机打乱，并在各迭代中保持拆分固定，以便进行同类比较。</p><p>如果你使用类似上述的脚本，可以将查询拆分为两个文件——<code>train_queries.json</code> 和 <code>validation_queries.json</code>——并分别针对每个文件运行脚本。</p></section>
<section><h2>6. 优化循环<a href="#6-优化循环"><span>#</span></a></h2><ol>
<li><strong>评估</strong>当前描述在 <em>训练集和验证集</em> 上的表现。训练结果指导你的修改；验证结果告诉你这些修改是否具有泛化性。</li>
<li><strong>识别失败</strong>在 <em>训练集</em> 中：哪些应触发查询没有触发？哪些不应触发查询触发了？
<ul>
<li>仅使用训练集的失败来指导修改——无论你自己修改描述还是提示 LLM，都要将验证集结果排除在流程之外。</li>
</ul>
</li>
<li><strong>修改描述</strong>。专注于泛化：
<ul>
<li>如果应触发查询失败，说明描述可能过于狭窄。扩大范围或添加关于技能适用场景的上下文。</li>
<li>如果不应触发查询误触发，说明描述可能过于宽泛。增加关于技能 <em>不</em> 做什么的特定说明，或明确此技能与相邻能力之间的边界。</li>
<li>避免添加失败查询中的具体关键词——那是过拟合。相反，找到这些查询所代表的一般类别或概念，并针对该类别进行调整。</li>
<li>如果经过几次迭代仍无进展，尝试从结构上重新构思描述，而不是增量调整。不同的表述方式或句子结构可能突破微调无法解决的问题。</li>
<li>检查描述是否保持在 1024 个字符的限制内——描述在优化过程中往往会变长。</li>
</ul>
</li>
<li><strong>重复</strong>步骤 1-3，直到所有 <em>训练集</em> 查询都通过，或你不再看到有意义的改进。</li>
<li><strong>选择最佳迭代</strong>根据其验证通过率——<em>验证集</em> 中通过查询的比例。请注意，最佳描述可能不是你生成的最后一个；早期的迭代可能比后期过拟合到训练集的迭代具有更高的验证通过率。</li>
</ol><p>五次迭代通常足够。如果性能没有提升，问题可能出在查询本身（太容易、太难或标签不佳），而非描述。</p><div><div><div></div><div>Tip</div></div><div><p><a href="https://github.com/anthropics/skills/tree/main/skills/skill-creator" target="_blank"><code>skill-creator</code></a> 技能可以端到端地自动化此循环：它拆分评估集、并行评估触发率、使用 Claude 提出描述改进建议，并生成一个实时 HTML 报告供你在运行时观看。</p></div></div></section>
<section><h2>7. 应用结果<a href="#7-应用结果"><span>#</span></a></h2><p>选定最佳描述后：</p><ol>
<li>更新 <code>SKILL.md</code> 前置元数据中的 <code>description</code> 字段。</li>
<li>确认描述不超过 1024 字符限制。</li>
<li>验证描述能按预期触发。手动尝试几个提示作为快速健全性检查。如需更严格的测试，编写 5-10 个新查询（混合应触发和不应触发），并通过评估脚本运行——由于这些查询从未参与优化过程，它们能真实地检验描述是否具有泛化性。</li>
</ol><p>修改前后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 修改前</span></div></div><div><div><div>2</div></div><div><span>description</span><span>: </span><span>Process CSV files.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 修改后</span></div></div><div><div><div>5</div></div><div><span>description</span><span>: </span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>分析 CSV 和表格数据文件——计算汇总统计量、</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>添加派生列、生成图表，并清理杂乱数据。当用户有 CSV、TSV 或 Excel 文件，并希望探索、转换或可视化数据时使用此技能，即使用户没有明确提到“CSV”或“分析”。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>改进后的描述在技能具体做什么（汇总统计、派生列、图表、清理）方面更明确，在适用场景（CSV、TSV、Excel；即使没有明确关键词）方面更宽泛。</p></section>
<section><h2>8. 后续步骤<a href="#8-后续步骤"><span>#</span></a></h2><p>一旦技能能可靠触发，你就需要评估它是否能产生良好的输出。请参阅 <a href="/posts/skill-05/">skill 评估</a> ，了解如何设置测试用例、对结果进行评分和迭代。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/bert-t5-gpt-02/</id>
      <title type="text">02_transformer_architecture_self_attention</title>
      <published>2026-07-10T00:00:00.000Z</published>
      <updated>2026-07-10T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/bert-t5-gpt-02/"/>
      <summary type="text">Transformer 与 GPT2 系列第二节</summary>
      <content type="html"><![CDATA[<p>我们在 <a href="/posts/BERT-08/">08</a> 和 <a href="/posts/BERT-09/">09</a> 节中介绍过了 attention 的计算与 Transformer 的架构。这节，让我们站在当今迅速发展的 GPT 系列模型的基础上，重新回顾关于 Transformer 与 Self-attention 的一切。</p>
<p>本节我们首先整体介绍 Transformer 的架构，然后介绍 encoder 部分。</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>import</span><span> transformers</span></div></div><div><div><div>4</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>5</div></div><div><span>import</span><span> pandas </span><span>as</span><span> pd</span></div></div><div><div><div>6</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>7</div></div><div><span>import</span><span> matplotlib </span><span>as</span><span> mpl</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>mpl.rcParams[</span><span>'figure.dpi'</span><span><span>] </span><span>=</span><span> </span></span><span>200</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(torch.</span><span>__version__</span><span>) </span><span># 2.6.0+cu124</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(transformers.</span><span>__version__</span><span>) </span><span># 5.9.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>1. summary<a href="#1-summary"><span>#</span></a></h1><ul>
<li>attention mechanism</li>
<li>encoder vs. decoder
<ul>
<li>seq2seq
<ul>
<li>seq of tokens -&gt; seq of tokens</li>
</ul>
</li>
<li>tasks
<ul>
<li>machine translation</li>
</ul>
</li>
<li>encoder
<ul>
<li>seq of tokens -&gt; seq of embedding vectors (hidden state/context)</li>
</ul>
</li>
<li>decoder
<ul>
<li>encoder’s hidden state -&gt; seq of tokens
<ul>
<li>iteratively generate
<ul>
<li>until EOS (end of seq) or reach max length limit</li>
</ul>
</li>
<li>one token at a time</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul><p>简单来说，encoder 的流程是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入一串 token → 输出一串 embedding 向量（也叫 hidden state / context）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>encoder 负责 <strong>理解输入</strong>：把输入的文本（token 序列）转换成一串稠密的向量表示（embedding），这些向量编码了输入文本的语义信息；</li>
<li>这些向量也叫 hidden state（隐状态） 或 context（上下文向量）。</li>
</ul><hr /><p>decoder 的流程则是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>利用编码器输出的隐状态 → 逐步生成输出序列，一次生成一个 token，直到遇到 EOS 或达到最大长度</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>decoder 负责 <strong>生成输出</strong>：利用编码器已经理解好的语义信息，逐个词地生成目标文本。</li>
<li>EOS（End of Sequence） 是序列结束标记。</li>
<li>它是<strong>自回归</strong>（autoregressive） 的：每生成一个词，就把这个词作为下一步的输入，再生成下一个词，像”串珠子”一样。</li>
</ul><p>Transformer 的架构示意图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/2d3dc8c29d142805229860bf5ec80fa1.png" alt="" /></p><p>Encoder 与 Decoder：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260620160756034.png" alt="" /></p><ul>
<li>encoder only: seq of text -&gt; rich representation (bidirectional attention)
<ul>
<li>task
<ul>
<li>text classification</li>
<li>NER</li>
</ul>
</li>
<li>models
<ul>
<li>BERT</li>
<li>RoBERTa</li>
<li>DistilBERT</li>
</ul>
</li>
<li>完形填空（bidirectional）
<ul>
<li>representation of a given token depends both on
<ul>
<li>left (before the token)</li>
<li>right (after the token)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>decoder only (causal or autoregressive attention)
<ul>
<li>GPT</li>
<li>词语接龙</li>
<li>representation of a given token depends only on the left context</li>
</ul>
</li>
<li>encoder-decoder
<ul>
<li>tasks
<ul>
<li>machine translation</li>
<li>summarization</li>
</ul>
</li>
<li>models
<ul>
<li>T5</li>
<li>BART</li>
</ul>
</li>
</ul>
</li>
</ul><p>我们上面是列举了 Transformer 的三种变体：</p><section><h2>1.1 Encoder-only<a href="#11-encoder-only"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入文本 → 丰富的语义表示（双向注意力）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>注意力方式：<strong>双向</strong>，每个词在计算表示时，可以看到它 <strong>左边和右边</strong> 的所有词；</li>
<li>典型任务：
<ul>
<li>文本分类</li>
<li>NER（命名实体识别）</li>
</ul>
</li>
<li>代表模型：BERT, RoBERTa，DistilBERT</li>
</ul><p>形象来说，encoder-only 的任务像是“完形填空”。比如句子 “I ____ you”，BERT 要预测 <code>____</code> 是什么词，它就会同时看：</p><ul>
<li>左边的词 <code>I</code></li>
<li>右边的词 <code>you</code></li>
</ul><p>两边都看，所以叫双向，这样能更好地理解整个句子的语义。</p></section><section><h2>1.2 Decoder-only<a href="#12-decoder-only"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>注意力只依赖于左边的上下文（因果注意力）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>注意力方式：因果 / 自回归（causal / autoregressive），每个词在生成时，只能看到它左边（已经生成的）词，看不到右边的（因为右边还没生成）。</li>
<li>代表模型：GPT 系列</li>
</ul><p>形象来说，decoder-only 的任务像是“词语接龙”，比如 GPT 要生成下一个词：“今天天气真__”.</p><p>它只能根据前面已经写出的 <code>今天天气真</code> 来预测下一个词是 <code>好</code>，<code>热</code> 还是 <code>不错</code>。它看不到后面还没写出来的词，所以叫单向的因果注意力。</p></section><section><h2>1.3 Encoder-Decoder<a href="#13-encoder-decoder"><span>#</span></a></h2><p>这就是我们上一段看到的经典 seq2seq 结构，最完整的 Transformer.</p><ul>
<li>工作方式：Encoder 先用双向注意力把输入理解好 → Decoder 再用单向注意力逐步生成输出。</li>
<li>典型任务：
<ul>
<li>机器翻译（英→中）</li>
<li>文本摘要（长文→短摘要）</li>
</ul>
</li>
<li>代表模型：T5、BART</li>
</ul></section></section>
<section><h1>2. encoder (encoder layer stack)<a href="#2-encoder-encoder-layer-stack"><span>#</span></a></h1><ul>
<li>encoder layer: encoding the contextual information (CNN)
<ul>
<li>input: seq of embeddings
<ul>
<li>multi-head self attention</li>
<li>FFN (fully-connected)</li>
</ul>
</li>
<li>output:
<ul>
<li>same shape as <code>input</code></li>
</ul>
</li>
<li>contextual information (contextualized embeddings)
<ul>
<li>apple: company-like or fruit-like ?
<ul>
<li>keynote</li>
<li>phone</li>
</ul>
</li>
<li>files
<ul>
<li>time flies like an arrowL soars</li>
<li>fruit likes a banana: insect</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>skip connection (residual connection) &amp; layer normalization
<ul>
<li>高效训练深度神经网络的技巧。</li>
</ul>
</li>
</ul><p>Transformer 的 Encoder 并不是一个单独的大模块，而是由 N 层相同的 Encoder Layer <strong>堆叠</strong> 而成，比如我们之前介绍的 BERT-base 模型就有 12 个 Transformer 层。</p><p>每一层 Encoder Layer 的结构如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入：一串 embedding 向量</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>├─ ❶ Multi-Head Self-Attention（多头自注意力）</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>├─ ❷ Skip Connection + Layer Normalization（残差连接 + 层归一化）</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>├─ ❸ Feed-Forward Network（前馈神经网络 / FC 全连接层）</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>├─ ❹ Skip Connection + Layer Normalization</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span>输出：与输入形状相同的向量（但包含了上下文信息）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它的特点是 <strong>输出形状与输入形状相同</strong>，所以可以一层接一层地堆叠下去。</p><hr /><p>关于上下文信息（contextualized embeddings）部分，我们上面举了两个例子：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>apple 是"苹果公司"还是"水果"？看周围词：</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>- keynote, phone → 像公司</span></div></div><div><div><div>4</div></div><div><span>- 如果是 eat, red → 像水果</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，同一个词 apple，在不同句子中含义不同。Self-Attention 的作用就是：让 apple 去看它周围的词，根据上下文来确定它的语义。所以输出的向量不再是固定的词向量，而是融入了上下文信息的动态向量，这就是 contextualized embeddings（上下文感知的词向量）。</p><p>在另一个例子中，我们引用了一句经典的 NLP 例子：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Time flies like an arrow; fruit flies like a banana.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>“时光如箭”，“果蝇喜欢香蕉”。同样，同一个词 flies，在两句中含义完全不同。Self-Attention 通过看周围的词（Time vs Fruit），就能正确区分。</p><hr /><p>残差连接（Skip Connection）与层归一化（Layer Normalization）是两个高效训练深度神经网络的技巧，它们的具体作用是：</p>

<table><thead><tr><th>技巧</th><th>作用</th><th>通俗理解</th></tr></thead><tbody><tr><td><strong>残差连接</strong></td><td>让梯度能直接”跳过”某一层反向传播</td><td>像给网络加了”快捷通道”，防止层数太深导致梯度消失/爆炸</td></tr><tr><td><strong>层归一化</strong></td><td>把每层的输出拉回到标准范围内</td><td>让训练更稳定，收敛更快</td></tr></tbody></table><section><h2>2.1 multi-head self-attention layer<a href="#21-multi-head-self-attention-layer"><span>#</span></a></h2><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260620163348199.png" alt="" /></p><section><h3>2.1.1 self-attention<a href="#211-self-attention"><span>#</span></a></h3><ul>
<li>each token
<ul>
<li>不是 fixed embeddings</li>
<li>而是 weighted average of each embedding of the whole input sequence</li>
</ul>
</li>
<li>a seq of token embedding: <span><span>x1,x2,...,xnx_1, x_2, ..., x_n</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>, 经过 self-attention 得到 a seq of updated embeddings: <span><span>x1′,x2′,...,xn′x_1^{'}, x_2^{'}, ..., x_n^{'}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>.</li>
</ul><span><span><span>xi′=∑j=1nwjixjx_i^{'} = \sum_{j=1}^n w_{ji}x_j</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><ul>
<li><span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>
<ul>
<li>attention weights</li>
<li><span><span>∑jwji=1\sum_j w_{ji}=1</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></li>
<li><span><span>w∈Rn×nw\in \mathcal{R}^{n\times n}</span><span><span><span></span><span>w</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span><span>×</span><span>n</span></span></span></span></span></span></span></span></span></span></span></span></li>
</ul>
</li>
</ul><p>在 Self-Attention 之前，词向量（如 Word2Vec、GloVe）是固定的——“apple”永远只有一个向量，无论它在什么句子中。</p><p>但在 Self-Attention 中，<strong>每个词的新表示 = 句子中所有词的加权平均</strong>。</p><p>也就是说，apple 的新向量会融合句子里其他词的信息。如果句子里有 keynote、phone，那 apple 会偏向”公司”的含义；如果有 eat、red，则偏向”水果”的含义。</p><hr /><p>关于上面的数学公式，它的意思是：</p><ul>
<li>我们输入词向量序列 <span><span>x1,x2,...,xnx_1, x_2, ..., x_n</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></li>
<li>经过 self-attention 后得到 <span><span>x1′,x2′,...,xn′x_1^{'}, x_2^{'}, ..., x_n^{'}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></li>
</ul><p>我们可以用一个句子来理解。假设句子是 “I love you”，有 3 个词：</p><ul>
<li><span><span>x1,x2,x3x_1,x_2,x_3</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ：输入的词向量，即 “I”, “love” 和 “you” 的原始向量；</li>
<li><span><span>x1′,x2′,x3′x_1^{'}, x_2^{'}, x_3^{'}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ：更新后的词向量，即 <strong>融入了上下文</strong> 后的新向量；</li>
<li><span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ：这是 attention weight，表示词 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 对词 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 的“重要性”。</li>
</ul><p>我们以 <span><span>x2′x_2^{'}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> （“love” 的新向量）为例：</p><span><span><span>x2′=w12x1+w22x2+w32x3x_2^{'} = w_{12}x_1 + w_{22}x_2 + w_{32}x_3</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>展开来就是：</p><span><span><span>xlove′=wI→love⋅xI+wlove→love⋅xlove+wyou→love⋅xyoux_{love}^{'} = w_{I→love}·x_I + w_{love→love}·x_{love} + w_{you→love}·x_{you}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>I</span><span>→</span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>⋅</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span><span>→</span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>⋅</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>y</span><span>o</span><span>u</span><span>→</span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>⋅</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>y</span><span>o</span><span>u</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>也就是说：“love” 的新向量 = <strong>“I”的信息 × 权重 + “love”自身的信息 × 权重 + “you”的信息 × 权重</strong></p><p>如果权重显示 “I” 和 “you” 与 “love” 关系很紧密（比如 <span><span>w12=0.4,w32=0.4w_{12}=0.4, w_{32}=0.4</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.4</span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.4</span></span></span></span>），那么 <span><span>xlove′x_{love}^{'}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就会更多地融入”I”和”you”的信息。</p><hr /><p>关于权重 <span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的性质：</p>

<table><thead><tr><th>性质</th><th>含义</th></tr></thead><tbody><tr><td><span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>词 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 对词 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 的注意力权重</td></tr><tr><td><span><span>∑jwji=1\sum_j w_{ji}=1</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></td><td>所有词对词 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 的权重之和为 1（像概率一样）</td></tr><tr><td><span><span>w∈Rn×nw \in \mathcal{R}^{n \times n}</span><span><span><span></span><span>w</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span><span>×</span><span>n</span></span></span></span></span></span></span></span></span></span></span></span></td><td>全部权重构成一个 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的矩阵</td></tr></tbody></table><p>这个 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的权重矩阵可以直观理解为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>          </span></span><span>I    love   you</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>I  [ w₁₁   w₁₂   w₁₃ ]    ← "I" 看各个词</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>love  [ w₂₁   w₂₂   w₂₃ ]    ← "love" 看各个词</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>you  [ w₃₁   w₃₂   w₃₃ ]    ← "you" 看各个词</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每一列是”这个词关注其他词的程度”，且<strong>每列之和 = 1</strong>。</p><p>比如第 2 列 <span><span>[w12,w22,w32]T[w_{12}, w_{22}, w_{32}]^T</span><span><span><span></span><span>[</span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 就是”love”对其他 3 个词的注意力分布。</p></section><section><h3>2.1.2 scaled dor-product attention<a href="#212-scaled-dor-product-attention"><span>#</span></a></h3><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260620170151484.png" alt="" /></p><span><span><span>Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V</span><span><span><span></span><span>A</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span>(</span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span><span>V</span></span></span></span></span><ul>
<li>Project each token embedding into 3 vectors called Query, Key and Value.</li>
<li>Compute attention scores.
<ul>
<li>dot-product (Query, Key) -&gt; attention scores;</li>
<li>a seq with <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> input tokens there is a corresponding <span><span>Rn×n\mathcal{R}^{n\times n}</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>n</span><span>×</span><span>n</span></span></span></span></span></span></span></span></span></span></span></span> matrix of attention scores.</li>
</ul>
</li>
<li>Compute attention weights from attention scores: <span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>
<ul>
<li>Dot products 的结果可能是任意大的数，会让整个训练过程非常不稳定；</li>
<li>将 attention scores 乘以一个 scaling factor</li>
<li>然后 softmax 归一化：<span><span>∑jwji=1\sum_j w_{ji}=1</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></li>
</ul>
</li>
<li>update the final embedding of the token (Value)
<ul>
<li><span><span>xi′=∑j=1nwjivjx_i^{'}=\sum_{j=1}^n w_{ji}v_j</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></li>
</ul>
</li>
</ul><p>如同我们在 <a href="/posts/BERT-09/">09</a> 节中介绍过的那样，Self-attention 中权重 <span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的计算分为清晰的 3 步：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>每个 token 的向量</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>第一步：投影出 3 个向量 → Query / Key / Value</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>第二步：Query 与 Key 做点积 → attention scores → scaling → softmax → attention weights</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>第三步：用 attention weights 对 Value 加权求和 → 得到最终输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面逐一展开。</p><hr /><section><h4>① Query、Key、Value（查询、键、值）<a href="#-querykeyvalue查询键值"><span>#</span></a></h4><blockquote><p>Project each token embedding into 3 vectors called Query, Key and Value.</p></blockquote><p>对于每个输入的词向量 <span><span>xix_i</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，我们通过三个不同的权重矩阵 <span><span>WQ,WK,WVW^Q, W^K, W^V</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>K</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span></span></span></span></span></span></span></span>，把它分别投影成三个向量：</p><span><span><span>qi=WQxi(Query)q_i = W^Q x_i \quad \text{(Query)}</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>(Query)</span></span></span></span></span></span><span><span><span>ki=WKxi(Key)k_i = W^K x_i \quad \text{(Key)}</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>K</span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>(Key)</span></span></span></span></span></span><span><span><span>vi=WVxi(Value)v_i = W^V x_i \quad \text{(Value)}</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>V</span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>(Value)</span></span></span></span></span></span><p><strong>为什么要分成三个角色？</strong> 用一个生活类比：</p>

<table><thead><tr><th>角色</th><th>类比</th><th>作用</th></tr></thead><tbody><tr><td><strong>Query（查询）</strong></td><td>🧐 你（读者）在图书馆<strong>搜索</strong>一本书</td><td>当前词”想找什么信息”</td></tr><tr><td><strong>Key（键）</strong></td><td>📚 每本书的书名标签</td><td>其他词”有什么信息可以给”</td></tr><tr><td><strong>Value（值）</strong></td><td>📖 书的内容本身</td><td>其他词”实际提供的信息内容”</td></tr></tbody></table><p>以句子 <strong>“I love you”</strong> 为例，当处理 <code>love</code> 这个词时：</p><ul>
<li><code>love</code> 的 <strong>Query</strong> 负责问：“谁跟我有关系？”</li>
<li><code>I</code> 和 <code>you</code> 的 <strong>Key</strong> 负责回答：“我在这里，我叫’I’ / ‘you’”</li>
<li>Query 和 Key 的匹配程度决定了 attention 权重</li>
<li>最后用权重去加权 <strong>Value</strong>（实际信息内容），得到 <code>love</code> 的新向量</li>
</ul><hr /></section><section><h4>② 计算 Attention Scores → Attention Weights<a href="#-计算-attention-scores--attention-weights"><span>#</span></a></h4><blockquote><p>dot-product (Query, Key) → attention scores</p></blockquote><p>句子有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个词，我们会得到一个 <strong><span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的 attention scores 矩阵</strong>。</p><p>对于词 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 对词 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 的得分（词 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 查询词 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span>）：</p><span><span><span>scoreji=qj⋅ki\text{score}_{ji} = q_j \cdot k_i</span><span><span><span></span><span><span><span>score</span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>点积越大 → Query 和 Key 越匹配 → 词 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 越应该关注词 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>。</p><section><h5>为什么要除以 scaling factor？<a href="#为什么要除以-scaling-factor"><span>#</span></a></h5><blockquote><p>Dot products 的结果可能是任意大的数，会让整个训练过程非常不稳定</p></blockquote><p>如果向量维度 <span><span>dkd_k</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 很大，点积的结果会非常大（因为很多数相加）。极端数值经过 softmax 后，会<strong>让大的更大、小的几乎消失</strong>，梯度变得非常小，模型学不动。</p><p>所以<strong>除以 <span><span>dk\sqrt{d_k}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></strong>（scaling factor），把数值拉回合理范围：</p><span><span><span>scaled scoreji=qj⋅kidk\text{scaled score}_{ji} = \frac{q_j \cdot k_i}{\sqrt{d_k}}</span><span><span><span></span><span><span><span>scaled score</span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span></section><section><h5>然后 softmax 归一化<a href="#然后-softmax-归一化"><span>#</span></a></h5><span><span><span>wji=exp⁡(scaled scoreji)∑t=1nexp⁡(scaled scorejt)w_{ji} = \frac{\exp(\text{scaled score}_{ji})}{\sum_{t=1}^n \exp(\text{scaled score}_{jt})}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>t</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>exp</span><span>(</span><span><span><span>scaled score</span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>exp</span><span>(</span><span><span><span>scaled score</span></span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>经过 softmax 后，<span><span>∑jwji=1\sum_j w_{ji} = 1</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，变成一个概率分布——这就是我们之前公式里的 <strong>attention weights</strong>。</p><hr /></section></section><section><h4>③ 用权重加权 Value，更新词向量<a href="#-用权重加权-value更新词向量"><span>#</span></a></h4><blockquote><p><span><span>xi′=∑j=1nwjivjx_i^{'} = \sum_{j=1}^n w_{ji} v_j</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></p></blockquote><p>最后一步，用刚刚算出的权重 <span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 对 <strong>Value 向量</strong> 做加权求和：</p><span><span><span>xlove′=wI→love⋅vI+wlove→love⋅vlove+wyou→love⋅vyoux_{love}^{'} = w_{I→love}·v_I + w_{love→love}·v_{love} + w_{you→love}·v_{you}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span><span><span></span><span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>I</span><span>→</span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>⋅</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span><span>→</span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>⋅</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>y</span><span>o</span><span>u</span><span>→</span><span>l</span><span>o</span><span>v</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>⋅</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>y</span><span>o</span><span>u</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>注意这里用的是 <strong><span><span>vjv_j</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（Value）</strong> 而不是原来的 <span><span>xjx_j</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这是因为 Value 是经过投影的”信息内容”，比原始向量更适合参与加权。</p><hr /></section><section><h4>完整流程一图流<a href="#完整流程一图流"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入:   x₁ (I)    x₂ (love)    x₃ (you)</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>│          │           │</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>─────┴──────────┴───────────┴────</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>│    W^Q        W^K         W^V  │   ← 三个投影矩阵</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>─────┬──────────┬───────────┬────</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>│          │           │</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>v          v           v</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>qᵢ         kᵢ          vᵢ</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Step 1:   q₂ · k₁   q₂ · k₂   q₂ · k₃     ← Query 与所有 Key 做点积</span></div></div><div><div><div>11</div></div><div><span><span>               </span></span><span>↓</span></div></div><div><div><div>12</div></div><div><span>Step 2:   [s₁, s₂, s₃] ÷ √dₖ → softmax → [w₁, w₂, w₃]   ← scaling + softmax</span></div></div><div><div><div>13</div></div><div><span><span>               </span></span><span>↓</span></div></div><div><div><div>14</div></div><div><span>Step 3:   x₂' = w₁·v₁ + w₂·v₂ + w₃·v₃     ← 加权求和 Value</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>2.1.3 visualization analysis<a href="#213-visualization-analysis"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> bertviz.transformers_neuron_view </span><span>import</span><span> BertModel</span></div></div><div><div><div>2</div></div><div><span>from</span><span> bertviz.neuron_view </span><span>import</span><span> show</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>(output): BertOutput(</span></div></div><div><div><div>29</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>30</div></div><div><span><span>          </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>33</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>40</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是我们之前介绍过许多次的经典 BERT 模型架构。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>sample_text </span><span>=</span><span> </span></span><span>'time flies like an arrow'</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>show</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>model,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>model_type</span><span>=</span><span>'bert'</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>tokenizer</span><span><span>=</span><span>tokenizer,</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>sentence_a</span><span><span>=</span><span>sample_text,</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>display_mode</span><span>=</span><span>'light'</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>layer</span><span>=</span><span>0</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>head</span><span>=</span><span>8</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里输出的是一个可交互的页面，让我们可以直观地看到每个词关注了哪些词。我们来大致解释一下 <code>show()</code> 里面的参数：</p><ul>
<li><code>model</code> ：加载的 BERT 模型，用来前向传播计算 attention 权重；</li>
<li><code>model_type='bert'</code> ：告诉 BertViz 你用的是哪种模型架构。因为 BertViz 支持多种模型（bert、gpt2、xlnet 等），每种模型的内部结构略有不同，需要指定类型才能正确解析 attention 矩阵；</li>
<li><code>tokenizer</code> ：分词器，用于把输入的句子转成 token IDs，同时知道每个 token 对应的文本是什么，方便在可视化中显示标签；</li>
<li><code>sentence_a</code> ：要可视化的句子。<code>sentence_a</code> 这个命名来自 BERT 的预训练任务——它本来可以接收两个句子（比如 NSP 任务），这里只需要一个句子所以只用 <code>sentence_a</code>；</li>
<li><code>display_mode</code> ：可视化界面的主题风格。<code>'light'</code> 是浅色背景，还有 <code>'dark'</code> 深色模式可选。</li>
<li><code>layer=0</code> ：选择哪一层的 attention 来可视化。BERT-base 有 12 层（0~11），<code>layer=0</code> 就是第 1 层（最靠近输入的那一层）。不同层关注的模式不同：
<ul>
<li>浅层（layer 0~3）：更多关注语法关系、相邻词</li>
<li>中层（layer 4~7）：开始捕捉一定的语义</li>
<li>深层（layer 8~11）：关注高层次的语义关联</li>
</ul>
</li>
<li><code>head</code> ：选择第几个注意力头来可视化。Multi-Head Attention 有多个头（BERT-base 有 12 个头，编号 0~11），每个头关注不同的模式。比方说：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>head 0 → 关注下一个词</span></div></div><div><div><div>2</div></div><div><span>head 5 → 关注句法依赖关系（如动词→名词）</span></div></div><div><div><div>3</div></div><div><span>head 8 → 关注句法关系（可能看到 "time" 关注 "flies" 的修饰关系）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不过实际上，选择 <code>layer</code> 和 <code>head</code> 只影响一开始展示的层和头。你可以在可交互页面的上端选择自由查看每一层和每个头。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260620171750388.png" alt="" /></p></section><section><h3>2.1.4 computation of self-attention<a href="#214-computation-of-self-attention"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(sample_text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>, </span><span>add_special_tokens</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>2</div></div><div><span>model_inputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[ 2051, 10029,  2066,  2019,  8612]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意这里我们没有加入 <code>[CLS]</code> 和 <code>[SEP]</code> 特殊 token.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoConfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>config </span><span>=</span><span> AutoConfig.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>2</div></div><div><span>config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertConfig {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>"add_cross_attention": false,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>"architectures": [</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>"BertForMaskedLM"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>"attention_probs_dropout_prob": 0.1,</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>"bos_token_id": null,</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>"classifier_dropout": null,</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>"eos_token_id": null,</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>"gradient_checkpointing": false,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>"hidden_act": "gelu",</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>"hidden_dropout_prob": 0.1,</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>"hidden_size": 768,</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>"initializer_range": 0.02,</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>"intermediate_size": 3072,</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>"is_decoder": false,</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>"layer_norm_eps": 1e-12,</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>"max_position_embeddings": 512,</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>"model_type": "bert",</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>"num_attention_heads": 12,</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>"num_hidden_layers": 12,</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>"pad_token_id": 0,</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>"position_embedding_type": "absolute",</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>"tie_word_embeddings": true,</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>"transformers_version": "5.9.0",</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>"type_vocab_size": 2,</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>"use_cache": true,</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>"vocab_size": 30522</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>以上就是 BERT 模型中的超参数，它们定义了 BERT 的骨架。需要注意的是，这里并不是 BERT 模型全部的超参数，训练的超参数需要在训练脚本（例如 HuggingFace 的 <code>Trainer</code> 中设置），比如学习率、优化器、<code>batch_size</code> 等等。</p><p>下面我们简单介绍一下每个参数的含义：</p><p><strong>核心结构参数（最重要的）</strong>：</p>

<table><thead><tr><th>参数</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>hidden_size</code></td><td><strong>768</strong></td><td>每个 token 的隐藏层向量维度。即输入/输出的 embedding 长度</td></tr><tr><td><code>num_hidden_layers</code></td><td><strong>12</strong></td><td>Encoder Layer 堆叠的层数</td></tr><tr><td><code>num_attention_heads</code></td><td><strong>12</strong></td><td>Multi-Head Attention 的头数。每头的维度 = 768/12 = <strong>64</strong></td></tr><tr><td><code>intermediate_size</code></td><td><strong>3072</strong></td><td>FFN 中间层的维度（先放大到 3072，再缩小回 768 = 4× 关系）</td></tr><tr><td><code>vocab_size</code></td><td><strong>30522</strong></td><td>词汇表大小，即 <code>word_embeddings</code> 矩阵的行数</td></tr><tr><td><code>max_position_embeddings</code></td><td><strong>512</strong></td><td>最大输入序列长度</td></tr></tbody></table><p>激活函数 &amp; Dropout：</p>

<table><thead><tr><th>参数</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>hidden_act</code></td><td><strong>gelu</strong></td><td>FFN 中的激活函数，不是 ReLU，是 GELU（高斯误差线性单元）</td></tr><tr><td><code>hidden_dropout_prob</code></td><td><strong>0.1</strong></td><td>隐藏层的 Dropout 概率</td></tr><tr><td><code>attention_probs_dropout_prob</code></td><td><strong>0.1</strong></td><td>Attention 权重上的 Dropout 概率</td></tr><tr><td><code>classifier_dropout</code></td><td><strong>null</strong></td><td>分类头专用 Dropout（此处未单独设置）</td></tr></tbody></table><p>特殊 Token &amp; 位置编码：</p>

<table><thead><tr><th>参数</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>pad_token_id</code></td><td><strong>0</strong></td><td>padding token 的 ID</td></tr><tr><td><code>bos_token_id</code></td><td><strong>null</strong></td><td>序列开始 token（BERT 不用，用 [CLS]）</td></tr><tr><td><code>eos_token_id</code></td><td><strong>null</strong></td><td>序列结束 token（BERT 不用）</td></tr><tr><td><code>position_embedding_type</code></td><td><strong>absolute</strong></td><td>位置编码方式：绝对位置编码</td></tr><tr><td><code>type_vocab_size</code></td><td><strong>2</strong></td><td>token type 数量（区分两个句子的 [0, 1]）</td></tr></tbody></table><p>其他配置：</p>

<table><thead><tr><th>参数</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>is_decoder</code></td><td><strong>false</strong></td><td>不是 decoder，是 encoder-only</td></tr><tr><td><code>layer_norm_eps</code></td><td><strong>1e-12</strong></td><td>Layer Norm 的分母微小值，防止除零</td></tr><tr><td><code>initializer_range</code></td><td><strong>0.02</strong></td><td>参数初始化的范围（截断正态分布）</td></tr><tr><td><code>gradient_checkpointing</code></td><td><strong>false</strong></td><td>是否用梯度检查点节省显存（慢但省显存）</td></tr><tr><td><code>tie_word_embeddings</code></td><td><strong>true</strong></td><td>是否绑定输入/输出 embedding 权重（BERT 用于 MLM）</td></tr><tr><td><code>add_cross_attention</code></td><td><strong>false</strong></td><td>是否添加 cross-attention（encoder-decoder 需要）</td></tr><tr><td><code>use_cache</code></td><td><strong>true</strong></td><td>是否缓存 KV（decoder 生成时加速用）</td></tr></tbody></table><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># config.vocab_size: 30522</span></div></div><div><div><div>2</div></div><div><span># config.hidden_size: 768=64*12</span></div></div><div><div><div>3</div></div><div><span><span>token_embedding </span><span>=</span><span> nn.</span><span>Embedding</span><span>(config.vocab_size, config.hidden_size)</span></span></div></div><div><div><div>4</div></div><div><span>token_embedding </span><span># Embedding(30522, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># forward of embedding module</span></div></div><div><div><div>2</div></div><div><span><span>input_embeddings </span><span>=</span><span> </span><span>token_embedding</span><span>(model_inputs[</span></span><span>'input_ids'</span><span>])</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># batch_size, seq_len, hidden_size</span></div></div><div><div><div>5</div></div><div><span>input_embeddings.shape </span><span># torch.Size([1, 5, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 先不考虑 position encoding</span></div></div><div><div><div>2</div></div><div><span># query: [1, 5, 768], key: [1, 5, 768], value: [1, 5, 768]</span></div></div><div><div><div>3</div></div><div><span># W_q, W_k, W_v: nn.linear, 作用在 token_embedding 上，依然通过 learing 最终决定</span></div></div><div><div><div>4</div></div><div><span><span>query </span><span>=</span><span> key </span><span>=</span><span> value </span><span>=</span><span> input_embeddings</span></span></div></div><div><div><div>5</div></div><div><span><span>dim_k </span><span>=</span><span> key.</span><span>size</span><span>(</span><span>-</span></span><span>1</span><span>) </span><span># 768</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(dim_k)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># /np.sqrt(dim_k) 是为了缩放，防止 dot product 过大导致 softmax 后梯度消失</span></div></div><div><div><div>9</div></div><div><span><span>attention_scores </span><span>=</span><span> torch.</span><span>matmul</span><span>(query, key.</span><span>transpose</span><span>(</span></span><span>1</span><span>, </span><span>2</span><span><span>)) </span><span>/</span><span> np.</span><span>sqrt</span><span>(dim_k)</span></span></div></div><div><div><div>10</div></div><div><span>attention_scores.shape </span><span># torch.Size([1, 5, 5])</span></div></div><div><div><div>11</div></div><div><span>attention_scores</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[27.1932,  1.1826, -0.4651,  0.3558,  0.2249],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[ 1.1826, 29.3970, -0.2585,  2.2841,  1.1054],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.4651, -0.2585, 26.4902, -0.1578, -0.9823],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>[ 0.3558,  2.2841, -0.1578, 28.4970,  1.3951],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.2249,  1.1054, -0.9823,  1.3951, 26.8615]]],</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>grad_fn=&lt;DivBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们得到的就是注意力分数矩阵，也就是公式中 <span><span>QKTdk\frac{QK^T}{\sqrt{d_k}}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 这一步的结果。</p><p>可以看出，对角线的值远远大于非对角线。这是因为每个词语自己最相似，所以点积最大。</p><p>需要注意的是，我们没有用可学习的 <span><span>WQ,WKW^Q, W^K</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>K</span></span></span></span></span></span></span></span></span></span></span> 投影，而是直接 <code>query = key = value = input_embeddings</code>，相当于 <span><span>X⋅XT/dkX\cdot X^T/\sqrt{d_k}</span><span><span><span></span><span>X</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>X</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，即每个 token 与其他所有 token 的点积。所以，这些分数只反映了 <strong>原始 token embedding 的余弦相似度</strong>，还不是真正经过学习的 attention.</p><p>如果采用 <span><span>WQ,WKW^Q, W^K</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>Q</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>K</span></span></span></span></span></span></span></span></span></span></span> 投影，它们会这些分数”调教”成更有语义意义的注意力分布——比如让 <code>flies</code> 更关注 <code>time</code>（语法修饰关系），而不是全靠自身。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># from scores -&gt; weights, softmax</span></div></div><div><div><div>2</div></div><div><span>import</span><span> torch.nn.functional </span><span>as</span><span> F</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>attention_weights </span><span>=</span><span> F.</span><span>softmax</span><span>(attention_scores, </span></span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>5</div></div><div><span>attention_weights.shape </span><span># torch.Size([1, 5, 5])</span></div></div><div><div><div>6</div></div><div><span>attention_weights</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[1.0000e+00, 5.0553e-12, 9.7307e-13, 2.2112e-12, 1.9401e-12],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[5.5803e-13, 1.0000e+00, 1.3206e-13, 1.6789e-12, 5.1658e-13],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[1.9655e-12, 2.4165e-12, 1.0000e+00, 2.6727e-12, 1.1718e-12],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>[6.0036e-13, 4.1294e-12, 3.5925e-13, 1.0000e+00, 1.6974e-12],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[2.7034e-12, 6.5209e-12, 8.0838e-13, 8.7118e-12, 1.0000e+00]]],</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>grad_fn=&lt;SoftmaxBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里就是上一步的 <code>attention_scores</code> 经过 softmax 归一化后的结果，也就是我们之前提到过的 <span><span>wjiw_{ji}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>.</p><p>不过这里我们会发现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>time    flies   like    an      arrow</span></div></div><div><div><div>2</div></div><div><span>time    1.00    ~0      ~0      ~0      ~0</span></div></div><div><div><div>3</div></div><div><span>flies   ~0      1.00    ~0      ~0      ~0</span></div></div><div><div><div>4</div></div><div><span>like    ~0      ~0      1.00    ~0      ~0</span></div></div><div><div><div>5</div></div><div><span>an      ~0      ~0      ~0      1.00    ~0</span></div></div><div><div><div>6</div></div><div><span>arrow   ~0      ~0      ~0      ~0      1.00</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几乎每个词都 100% 关注自己，而不看其他任何词。</p><p>这是我们前面简化的结果，在这种简化下，每个 token 和自己一模一样，点积当然远远大于与其他 token 的点积。这样的 attention 是<strong>完全失效的</strong>——每个词都是”孤岛”，没有吸收任何上下文信息。</p><p>不过，这里我们只是演示计算的过程，只需要知道真实的 BERT 并不是这样的即可。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>attention_weights.</span><span>sum</span><span>(</span></span><span>dim</span><span>=-</span><span>1</span><span>) </span><span># 每行加起来等于 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[1., 1., 1., 1., 1.]], grad_fn=&lt;SumBackward1&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，经过 softmax 归一化后，每行加起来都等于 1，也就对应前面我们提到过的 <span><span>∑jwji=1\sum_j w_{ji}=1</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 5*5, 5*768 -&gt; 5*768</span></div></div><div><div><div>2</div></div><div><span><span>attention_output </span><span>=</span><span> torch.</span><span>bmm</span><span>(attention_weights, value)</span></span></div></div><div><div><div>3</div></div><div><span>attention_output.shape </span><span># torch.Size([1, 5, 768])</span></div></div><div><div><div>4</div></div><div><span>attention_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[-0.3932,  0.1078, -1.3741,  ..., -0.8332,  1.7649, -0.8743],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-1.7124, -1.0425, -0.7992,  ...,  0.6528,  0.5512,  1.1871],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-1.1721, -1.1326,  0.3345,  ..., -0.1074,  0.3040, -0.2598],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>[-0.4535, -0.7321, -1.8562,  ..., -0.1357,  1.2970,  0.2048],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[-0.6879, -1.6019,  0.6278,  ...,  0.2712,  0.0040, -0.2760]]],</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>grad_fn=&lt;BmmBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就来到了公式中的最后一步，用 <code>attention_weights</code> 对 Value 进行加权求和，形状为 (1, 5, 768)：</p><ul>
<li>1 个 batch</li>
<li>5 个 token</li>
<li>每个 768 维</li>
</ul><p>同样，由于我们简化的结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>x₁' = 1.0 · v₁ + 0 · v₂ + 0 · v₃ + 0 · v₄ + 0 · v₅ = v₁</span></div></div><div><div><div>2</div></div><div><span>x₂' = 0 · v₁ + 1.0 · v₂ + 0 · v₃ + 0 · v₄ + 0 · v₅ = v₂</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出的每一行，其实就等于对应 token 自己的 Value 向量，几乎没有从其他词吸收任何信息。</p><p>我们用一个完整的函数覆盖上述步骤：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># batch_size, seq_len, hidden_size</span></div></div><div><div><div>2</div></div><div><span>def</span><span> </span><span>scaled_dot_product_attention</span><span>(</span><span>query</span><span>,</span><span><span> </span><span>key</span></span><span>,</span><span><span> </span><span>value</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>dim_k </span><span>=</span><span> key.</span><span>size</span><span>(</span><span>-</span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>attention_scores </span><span>=</span><span> torch.</span><span>matmul</span><span>(query, key.</span><span>transpose</span><span>(</span><span>1</span><span>, </span><span>2</span><span><span>)) </span><span>/</span><span> np.</span><span>sqrt</span><span>(dim_k)</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>attention_weights </span><span>=</span><span> F.</span><span>softmax</span><span>(attention_scores, </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>attention_output </span><span>=</span><span> torch.</span><span>bmm</span><span>(attention_weights, value)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> attention_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scaled_dot_product_attention</span><span>(query, key, value)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[-0.3932,  0.1078, -1.3741,  ..., -0.8332,  1.7649, -0.8743],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-1.7124, -1.0425, -0.7992,  ...,  0.6528,  0.5512,  1.1871],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-1.1721, -1.1326,  0.3345,  ..., -0.1074,  0.3040, -0.2598],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>[-0.4535, -0.7321, -1.8562,  ..., -0.1357,  1.2970,  0.2048],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[-0.6879, -1.6019,  0.6278,  ...,  0.2712,  0.0040, -0.2760]]],</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>grad_fn=&lt;BmmBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后，补充介绍一下 <code>.matmul()</code> 与 <code>.bmm()</code> 函数：</p><ul>
<li><code>torch.matmul()</code> ：<strong>通用矩阵乘法</strong>，支持多种维度的输入，会根据输入自动选择计算方式：</li>
</ul>

<table><thead><tr><th>输入维度</th><th>行为</th><th>例子</th></tr></thead><tbody><tr><td>两个 2D <code>(m, n) × (n, p)</code></td><td>标准矩阵乘法</td><td>→ <code>(m, p)</code></td></tr><tr><td>3D + 2D <code>(b, m, n) × (n, p)</code></td><td>批次广播</td><td>→ <code>(b, m, p)</code></td></tr><tr><td>两个 3D <code>(b, m, n) × (b, n, p)</code></td><td>批次矩阵乘法</td><td>→ <code>(b, m, p)</code></td></tr><tr><td>1D 向量</td><td>点积</td><td></td></tr></tbody></table><p>之前我们用的就是 3D 版本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># query: [1, 5, 768], key.transpose: [1, 768, 5]</span></div></div><div><div><div>2</div></div><div><span><span>torch.</span><span>matmul</span><span>(query, key.</span><span>transpose</span><span>(</span></span><span>1</span><span>, </span><span>2</span><span>))  </span><span># → [1, 5, 5]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li><code>torch.bmm()</code> ：<strong>专门用于 3D 张量的批次矩阵乘法</strong>，要求输入必须<strong>恰好是 3D</strong>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>torch.</span><span>bmm</span><span>(attention_weights, value)</span></span></div></div><div><div><div>2</div></div><div><span># weights: [1, 5, 5],  value: [1, 5, 768]</span></div></div><div><div><div>3</div></div><div><span># → output: [1, 5, 768]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p>二者关键区别：</p>

<table><thead><tr><th></th><th><code>matmul()</code></th><th><code>bmm()</code></th></tr></thead><tbody><tr><td>输入维度</td><td>灵活（1D~3D+）</td><td>必须两个都是 <strong>3D</strong></td></tr><tr><td>广播</td><td>✅ 支持</td><td>❌ 不支持（batch 维必须相等）</td></tr><tr><td>使用场景</td><td>Q 与 K^T 相乘（维数可能不同）</td><td>weights 与 V 相乘（都是明确 3D）</td></tr></tbody></table><p>简单来说：<strong><code>matmul</code> 通用灵活，<code>bmm</code> 专一高效</strong>。二者在 3D 输入且 batch 一致时的行为等价，但 <code>bmm</code> 更严格——如果你确定两个输入都是 <code>(batch, n, m)</code> 形状，用 <code>bmm</code> 可以提前捕捉维度错误。</p></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/bert-t5-gpt-01/</id>
      <title type="text">01_fine_tune_transformers_on_classification</title>
      <published>2026-07-09T00:00:00.000Z</published>
      <updated>2026-07-09T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/bert-t5-gpt-01/"/>
      <summary type="text">Transformer 与 GPT2 系列第一节</summary>
      <content type="html"><![CDATA[<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>import</span><span> transformers</span></div></div><div><div><div>4</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>5</div></div><div><span>import</span><span> pandas </span><span>as</span><span> pd</span></div></div><div><div><div>6</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(torch.</span><span>__version__</span><span>) </span><span># 2.6.0+cu124</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(transformers.</span><span>__version__</span><span>) </span><span># 5.9.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib </span><span>as</span><span> mpl</span></div></div><div><div><div>2</div></div><div><span>mpl.rcParams[</span><span>"figure.dpi"</span><span><span>] </span><span>=</span><span> </span></span><span>200</span><span> </span><span># 默认为 100，设置为 200 可以让图像更清晰</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>1. text classification<a href="#1-text-classification"><span>#</span></a></h1><ul>
<li>也叫 sequence classification（text 就是 sequence 的一种）</li>
<li>sentiment analysis
<ul>
<li>情感分析：就是一种文本分类
<ul>
<li>电商评论</li>
<li>social web: weibo/twitter</li>
</ul>
</li>
</ul>
</li>
</ul><section><h2>1.1 emotions 数据集<a href="#11-emotions-数据集"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> datasets </span><span>import</span><span> load_dataset</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions </span><span>=</span><span> </span><span>load_dataset</span><span>(</span></span><span>"dair-ai/emotion"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>emotions </span><span># dataset Dict</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DatasetDict({</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>train: Dataset({</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>features: ['text', 'label'],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>num_rows: 16000</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>validation: Dataset({</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>features: ['text', 'label'],</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>num_rows: 2000</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>test: Dataset({</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>features: ['text', 'label'],</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>num_rows: 2000</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>14</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可见，在默认设置下，会自动分为三块数据集：训练集、测试集和验证集，比例为 8:1&lt;1&gt;. 此外，数据集为一个字典类型。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions.</span><span>keys</span><span>() </span></span><span># dict_keys(['train', 'validation', 'test'])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>]), </span><span>type</span><span>(emotions[</span><span>'train'</span><span>]) </span><span># 数据集结构概览</span></div></div><div><div><div>2</div></div><div><span># 继续支持 key</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>][</span><span>'text'</span><span>][:</span><span>5</span><span>]) </span><span># 前五条文本数据</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>][</span><span>'label'</span><span>][:</span><span>5</span><span>]) </span><span># 前五条标签数据</span></div></div><div><div><div>5</div></div><div><span># 支持 index</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>][:</span><span>5</span><span>]) </span><span># 前五条数据，包含文本和标签</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Dataset({</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>features: ['text', 'label'],</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>num_rows: 16000</span></div></div><div><div><div>4</div></div><div><span>})</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>['i didnt feel humiliated', 'i can go from feeling so hopeless to so damned hopeful just from being around someone who cares and is awake', 'im grabbing a minute to post i feel greedy wrong', 'i am ever feeling nostalgic about the fireplace i will know that it is still on the property', 'i am feeling grouchy']</span></div></div><div><div><div>7</div></div><div><span>[0, 0, 3, 2, 3]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>{'text': ['i didnt feel humiliated', 'i can go from feeling so hopeless to so damned hopeful just from being around someone who cares and is awake', 'im grabbing a minute to post i feel greedy wrong', 'i am ever feeling nostalgic about the fireplace i will know that it is still on the property', 'i am feeling grouchy'], 'label': [0, 0, 3, 2, 3]}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的 <code>[0, 0, 3, 2, 3]</code> 是每条文本对应的情感分类标签（数字编码）。<code>emotions</code> 数据集有 6 类情感：</p>

<table><thead><tr><th>标签值</th><th>情感</th></tr></thead><tbody><tr><td>0</td><td>sadness（悲伤）</td></tr><tr><td>1</td><td>joy（快乐）</td></tr><tr><td>2</td><td>love（喜爱）</td></tr><tr><td>3</td><td>anger（愤怒）</td></tr><tr><td>4</td><td>fear（恐惧）</td></tr><tr><td>5</td><td>surprise（惊讶）</td></tr></tbody></table><p>所以前 5 条分别是：sadness、sadness、anger、love、anger.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>].features) </span><span># 查看所有特征</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>].features[</span><span>'label'</span><span>]) </span><span># 查看标签特征</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>print</span><span>(emotions[</span><span>'train'</span><span>].features[</span><span>'label'</span><span><span>].</span><span>int2str</span><span>(</span></span><span>2</span><span>)) </span><span># 查看标签数字 2 对应的字符串标签</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'text': Value('string'), 'label': ClassLabel(names=['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'])}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>ClassLabel(names=['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>love</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>第一行：这里我们可以从第一行输出看到，训练集有两个特征：</li>
</ul>

<table><thead><tr><th>特征</th><th>类型</th><th>说明</th></tr></thead><tbody><tr><td><code>text</code></td><td><code>Value('string')</code></td><td>文本内容，字符串类型</td></tr><tr><td><code>label</code></td><td><code>ClassLabel(names=[...])</code></td><td>分类标签，有 6 个类别名称</td></tr></tbody></table><ul>
<li>第二行：可以看到 <code>label</code> 是一个 <strong><code>ClassLabel</code></strong> 类型，内部维护了数字到情感名称的映射：</li>
</ul>

<table><thead><tr><th>数字</th><th>情感</th></tr></thead><tbody><tr><td>0</td><td>sadness（悲伤）</td></tr><tr><td>1</td><td>joy（快乐）</td></tr><tr><td>2</td><td>love（喜爱）</td></tr><tr><td>3</td><td>anger（愤怒）</td></tr><tr><td>4</td><td>fear（恐惧）</td></tr><tr><td>5</td><td>surprise（惊讶）</td></tr></tbody></table><ul>
<li>第 3 行：<code>int2str()</code> 是 <code>ClassLabel</code> 提供的方法，功能是<strong>将数字标签转换为对应的文字</strong>。这里传入 <code>2</code>，返回 <code>"love"</code>.</li>
</ul><p>对应的还有<strong>反向方法</strong> <code>str2int()</code>，例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>emotions[</span><span>'train'</span><span>].features[</span><span>'label'</span><span><span>].</span><span>str2int</span><span>(</span></span><span>'love'</span><span>)  </span><span># 返回 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>labels </span><span>=</span><span> emotions[</span></span><span>'train'</span><span>].features[</span><span>'label'</span><span>].names</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(labels) </span><span># 标签列表</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>num_classes </span><span>=</span><span> </span></span><span>len</span><span>(labels)</span></div></div><div><div><div>5</div></div><div><span>print</span><span>(num_classes) </span><span># 标签数量</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['sadness', 'joy', 'love', 'anger', 'fear', 'surprise']</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>6</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们把六类标签都提取出来，方便后面分析。</p></section><section><h2>1.2 data visualization analysis<a href="#12-data-visualization-analysis"><span>#</span></a></h2><ul>
<li>dataset -&gt; dataframe</li>
<li>text length</li>
<li>label freq</li>
</ul><section><h3>1.2.1 dataset to dataframe<a href="#121-dataset-to-dataframe"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions_df </span><span>=</span><span> pd.DataFrame.</span><span>from_dict</span><span>(emotions[</span></span><span>'train'</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们将 HuggingFace 中的 <code>Dataset</code> 转化成了 pandas 库中的 <code>DataFrame</code>. 因为 Hugging Face 的 <code>Dataset</code> 虽然功能强大，但在数据分析和可视化方面不如 pandas 方便，而 <code>DataFrame</code> 提供了大量便捷的方法。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions_df.shape, emotions_df.columns) </span><span># 数据集形状和列名</span></div></div><div><div><div>2</div></div><div><span>emotions_df[:</span><span>5</span><span>] </span><span># 前五行数据</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(16000, 2) Index(['text', 'label'], dtype='str')</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>                 </span></span><span>text                                label</span></div></div><div><div><div>5</div></div><div><span>0  i didnt feel humiliated                              0</span></div></div><div><div><div>6</div></div><div><span>1  i can go from feeling so hopeless to so damned...  0</span></div></div><div><div><div>7</div></div><div><span>2  im grabbing a minute to post i feel greedy wrong  3</span></div></div><div><div><div>8</div></div><div><span>3  i am ever feeling nostalgic about the fireplac...  2</span></div></div><div><div><div>9</div></div><div><span>4  i am feeling grouchy                              3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>emotions_df[</span><span>'label_name'</span><span><span>] </span><span>=</span><span> emotions_df[</span></span><span>'label'</span><span><span>].</span><span>apply</span><span>(</span></span><span>lambda</span><span> </span><span>x</span><span>: emotions[</span><span>'train'</span><span>].features[</span><span>'label'</span><span><span>].</span><span>int2str</span><span>(x))</span></span></div></div><div><div><div>2</div></div><div><span>emotions_df[:</span><span>5</span><span>] </span><span># 前五行数据，包含标签名称</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>          </span></span><span>text                                   label  label_name</span></div></div><div><div><div>2</div></div><div><span>0  i didnt feel humiliated                              0  sadness</span></div></div><div><div><div>3</div></div><div><span>1  i can go from feeling so hopeless to so damned...  0  sadness</span></div></div><div><div><div>4</div></div><div><span>2  im grabbing a minute to post i feel greedy wrong  3  anger</span></div></div><div><div><div>5</div></div><div><span>3  i am ever feeling nostalgic about the fireplac...  2  love</span></div></div><div><div><div>6</div></div><div><span>4  i am feeling grouchy                              3  anger</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2.2 label analysis<a href="#122-label-analysis"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions_df.label.</span><span>value_counts</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>label</span></div></div><div><div><div>2</div></div><div><span>1    5362</span></div></div><div><div><div>3</div></div><div><span>0    4666</span></div></div><div><div><div>4</div></div><div><span>3    2159</span></div></div><div><div><div>5</div></div><div><span>4    1937</span></div></div><div><div><div>6</div></div><div><span>2    1304</span></div></div><div><div><div>7</div></div><div><span>5     572</span></div></div><div><div><div>8</div></div><div><span>Name: count, dtype: int64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是训练集中的标签分布，所有加起来恰好为 16000 条样本。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions_df.label_name.</span><span>value_counts</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>label_name</span></div></div><div><div><div>2</div></div><div><span>joy         5362</span></div></div><div><div><div>3</div></div><div><span>sadness     4666</span></div></div><div><div><div>4</div></div><div><span>anger       2159</span></div></div><div><div><div>5</div></div><div><span>fear        1937</span></div></div><div><div><div>6</div></div><div><span>love        1304</span></div></div><div><div><div>7</div></div><div><span>surprise     572</span></div></div><div><div><div>8</div></div><div><span>Name: count, dtype: int64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>plt.</span><span>figure</span><span>(</span></span><span>figsize</span><span><span>=</span><span>(</span></span><span>4</span><span>,</span><span>3</span><span>))</span></div></div><div><div><div>2</div></div><div><span><span>emotions_df.label_name.</span><span>value_counts</span><span>().</span><span>plot</span><span>(</span></span><span>kind</span><span>=</span><span>'bar'</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>plt.</span><span>xlabel</span><span>(</span></span><span>'Label'</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>plt.</span><span>ylabel</span><span>(</span></span><span>'Count'</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>plt.</span><span>title</span><span>(</span></span><span>'Distribution of Labels'</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>plt.</span><span>xticks</span><span>(</span></span><span>rotation</span><span>=</span><span>45</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>plt.</span><span>show</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出如下图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619162336030.png" alt="" /></p></section><section><h3>1.2.3 text length analysis<a href="#123-text-length-analysis"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>plt.</span><span>figure</span><span>(</span></span><span>figsize</span><span><span>=</span><span>(</span></span><span>4</span><span>,</span><span>3</span><span>))</span></div></div><div><div><div>2</div></div><div><span>emotions_df[</span><span>'word per tweet'</span><span><span>] </span><span>=</span><span> emotions_df[</span></span><span>'text'</span><span><span>].str.</span><span>split</span><span>().</span><span>apply</span><span>(</span></span><span>len</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>emotions_df.</span><span>boxplot</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>column</span><span>=</span><span>'word per tweet'</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>by</span><span>=</span><span>'label_name'</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>grid</span><span>=</span><span>False</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>color</span><span>=</span><span>'blue'</span></div></div><div><div><div>8</div></div><div><span>    </span><span># showfliers=False</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div><span><span>plt.</span><span>suptitle</span><span>(</span></span><span>''</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>plt.</span><span>xlabel</span><span>(</span></span><span>''</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出如下图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619162913950.png" alt="" /></p><p>这里我们使用<strong>箱线图（Boxplot）<strong>进行分析，它展示了 <code>emotion</code> 数据集中，不同情感标签（怒、惧、喜、爱、悲、惊）的推文</strong>文本长度（单词数）的分布情况</strong>。</p><p>我们主要看以下几个关键元素：</p><ol>
<li><strong>横轴 (X 轴)</strong>：表示不同的分类标签（如 anger, fear, joy 等）。</li>
<li><strong>纵轴 (Y 轴)</strong>：表示推文的单词数量（word per tweet）。</li>
<li><strong>蓝色的矩形箱体 (Box)</strong>：
<ul>
<li><strong>中间的横线</strong>：表示<strong>中位数 (Median)</strong>。将该类别下所有推文的长度从小到大排列后，排在最中间的那个长度值。例如，所有类别推文长度的中位数都在 15-20 个单词左右。</li>
<li><strong>箱子的顶边</strong>：表示<strong>上四分位数 (75th Percentile)</strong>。有 75% 的推文长度小于这个值。</li>
<li><strong>箱子的底边</strong>：表示<strong>下四分位数 (25th Percentile)</strong>。有 25% 的推文长度小于这个值。</li>
<li><strong>箱子的高度</strong>：表示<strong>四分位距 (IQR)</strong>，包含了中间 50% 数据所在的范围，反映了数据的集中程度。</li>
</ul>
</li>
<li><strong>上下延伸的线段 (Whiskers, “胡须”)</strong>：
<ul>
<li>表示数据的范围（不包含异常值）。通常延伸到 <span><span>1.5×IQR1.5 \times IQR</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>I</span><span>QR</span></span></span></span> 内的最极值。</li>
<li>顶端的横线是<strong>最大值</strong>（非异常值情况下的最大长度）。</li>
<li>底端的横线是<strong>最小值</strong>（非异常值情况下的最小长度）。可以看到所有类别的最小词数都在 2 个左右。</li>
</ul>
</li>
<li><strong>异常值 (Outliers)</strong>：在代码中，你设置了 <code>showfliers=False</code>，这意味着这幅图隐藏了离群点（异常长或异常短的极端个例），从而让主体分布更加清晰。</li>
</ol><p><strong>从这幅图中可以得出的结论：</strong></p><ul>
<li>不同情感的推文长度分布非常相似，中位数大多在 15-20 词之间。</li>
<li>各个情感类别的绝大多数推文（约 75%）长度都在 30 个单词以内，最长的（不计极端异常值）在 45-50 个单词左右。</li>
<li>这表明各种情感表达在句子长度上并没有明显的差异偏好。这对于后续使用 Transformer 模型（比如 BERT，它有最大序列长度限制，如 512，这取决于 Token 数量而非单词，但通过单词数可以看出通常比较短，不太会被截断）进行文本分类是一个有用的参考信息。</li>
</ul><p>当我们把 <code>showfliers=False</code> 注解掉后，就会看到有一些异常点。</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619163259282.png" alt="" /></p><p>回顾一下箱线图的知识：</p><ol>
<li>
<p><strong>正常范围（胡须的范围）</strong>：
箱线图有一条规则用来界定什么是“正常”的数据范围。通常，程序会计算一个叫做 <strong>四分位距（IQR，即上四分位数减去下四分位数，也就是蓝色箱子的高度）</strong> 的值。</p>
<ul>
<li>上方的胡须最高会延伸到：<strong>上四分位数 + <span><span>1.5×IQR1.5 \times IQR</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>I</span><span>QR</span></span></span></span></strong>。</li>
<li>超过这个界限的数据点，就会被判定为<strong>异常值</strong>。</li>
</ul>
</li>
<li>
<p><strong>异常点的意义</strong>：
在这个特定的数据集中，这些小黑圈代表<strong>个别极其冗长的推文</strong>。</p>
<ul>
<li>绝大部分推文长度都在 50 个单词（上方的蓝色横线）以内。</li>
<li>但有少数人在推文中写了 50~70 多个单词甚至更多（图中的黑圈最高达到了接近 70）。</li>
<li>这些非常长的句子不符合大众的发推习惯，所以被当做异常点单独画在上面。</li>
</ul>
</li>
</ol><div><div><div></div><div>对我们做深度学习/自然语言处理的启示</div></div><div><p>当我们把文本输入给 BERT 模型时，BERT 有一个最大长度限制（往往设为 128、256 或 512）。看到最高的黑圈也就是 70 左右的&gt; 单词数，我们就可以很放心地把模型最大序列长度（<code>max_length</code>）截断设得比较小（比如 128），因为即使是这些“异常”的超长文 &gt; 本，也完全能被容纳，不会因为被截断而丢失关键信息。</p></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions_df[</span><span>'word per tweet'</span><span><span>].</span><span>max</span><span>()) </span></span><span># 最长文本的单词数量</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(emotions_df[</span><span>'word per tweet'</span><span><span>].</span><span>idxmax</span><span>()) </span></span><span># 最长文本的索引</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>66</span></div></div><div><div><div>2</div></div><div><span>6322</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，训练集中最长文本的索引就是第 6322 条数据，共有 66 个单词。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions_df.iloc[</span><span>6322</span><span>]) </span><span># 查看第 6322 条数据</span></div></div><div><div><div>2</div></div><div><span>emotions_df.iloc[</span><span>6322</span><span>][</span><span>'text'</span><span>] </span><span># 查看第 6322 条数据的文本内容</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>text              i guess which meant or so i assume no photos n...</span></div></div><div><div><div>2</div></div><div><span>label                                                             0</span></div></div><div><div><div>3</div></div><div><span>label_name                                                  sadness</span></div></div><div><div><div>4</div></div><div><span>word per tweet                                                   66</span></div></div><div><div><div>5</div></div><div><span>Name: 6322, dtype: object</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>'i guess which meant or so i assume no photos no words or no other way to convey what it really feels unless you feels it yourself or khi bi t au th m i bi t th ng ng i b au i rephrase it to a bit more gloomy context unless you are hurt yourself you will never have sympathy for the hurt ones'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们使用到了 <code>iloc</code> 方法。<code>iloc</code> 是 pandas 中<strong>基于整数位置（integer-location）的索引方法</strong>，用于按行号（从 0 开始）访问 DataFrame 或 Series 中的数据。</p><p>它的核心特点如下：</p>

<table><thead><tr><th>方法</th><th>基于</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td><code>iloc</code></td><td><strong>整数位置</strong>（行号）</td><td><code>df.iloc[0]</code></td><td>第 0 行</td></tr><tr><td><code>loc</code></td><td><strong>标签/索引名</strong></td><td><code>df.loc[0]</code></td><td>索引为 0 的行（可能不是第一行）</td></tr><tr><td><code>[]</code></td><td>列名</td><td><code>df['text']</code></td><td>获取列</td></tr></tbody></table><p><code>iloc</code> vs <code>loc</code> 对比如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># iloc —— 按行号（不考虑索引值）</span></div></div><div><div><div>2</div></div><div><span>emotions_df.iloc[</span><span>0</span><span>]     </span><span># 无论索引是什么，取第 0 行</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># loc —— 按索引标签（不考虑位置）</span></div></div><div><div><div>5</div></div><div><span>emotions_df.loc[</span><span>0</span><span>]      </span><span># 取索引值等于 0 的行</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当 DataFrame 的索引是默认的 <code>0, 1, 2, ...</code> 时，两者结果相同。但如果索引被重置或乱序，它们就不同了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>df </span><span>=</span><span> pd.</span><span>DataFrame</span><span>({</span></span><span>'a'</span><span>: [</span><span>10</span><span>, </span><span>20</span><span>, </span><span>30</span><span>]}, </span><span>index</span><span><span>=</span><span>[</span></span><span>2</span><span>, </span><span>0</span><span>, </span><span>1</span><span>])</span></div></div><div><div><div>2</div></div><div><span>df.iloc[</span><span>0</span><span>]  </span><span># 返回索引为 2 的行：a=10 （第 0 个位置）</span></div></div><div><div><div>3</div></div><div><span>df.loc[</span><span>0</span><span>]   </span><span># 返回索引为 0 的行：a=20 （标签为 0）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在这里，因为 DataFrame 使用的是默认整数索引，所以 <code>iloc[6322]</code> 和 <code>loc[6322]</code> 结果一样，但 <code>iloc</code> 的语义更明确——就是<strong>按行号取数据</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions_df[</span><span>'word per tweet'</span><span><span>].</span><span>min</span><span>()) </span></span><span># 最短文本的单词数量</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(emotions_df[</span><span>'word per tweet'</span><span><span>].</span><span>idxmin</span><span>()) </span></span><span># 最短文本的索引</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>2</span></div></div><div><div><div>2</div></div><div><span>4150</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，训练集中最短文本的索引就是第 4150 条数据，只有 2 个单词。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(emotions_df.iloc[</span><span>4150</span><span>]) </span><span># 查看第 4150 条数据</span></div></div><div><div><div>2</div></div><div><span>emotions_df.iloc[</span><span>4150</span><span>][</span><span>'text'</span><span>] </span><span># 查看第 4150 条数据的文本内容</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>text              earth crake</span></div></div><div><div><div>2</div></div><div><span>label                       4</span></div></div><div><div><div>3</div></div><div><span>label_name               fear</span></div></div><div><div><div>4</div></div><div><span>word per tweet              2</span></div></div><div><div><div>5</div></div><div><span>Name: 4150, dtype: object</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>'earth crake'</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>1.3 text -&gt; tokens<a href="#13-text---tokens"><span>#</span></a></h2><p>数据集转化为模型接受的输入类型：</p><ul>
<li>Subword Tokenization
<ul>
<li>WordPiece
<ul>
<li>BERT and DistilBERT</li>
</ul>
</li>
</ul>
</li>
<li>HuggingFace
<ul>
<li><code>~/.cache/huggingface/</code></li>
<li>model config
<ul>
<li><code>tokenizer.model_max_length</code></li>
<li><code>tokenizer.model_input_names</code></li>
</ul>
</li>
</ul>
</li>
</ul><p>我们接下来要做的就是——<strong>将原始文本转换为模型能理解的输入格式</strong>。</p><section><h3>1.3.1 tokenizer<a href="#131-tokenizer"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'distilbert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的模型名称也包含一些信息 <code>distilbert-base-uncased</code>：</p><ul>
<li><code>base</code> 表示模型大小，有 <code>medium</code>， <code>large</code> ，我们这里选用的是 <code>base</code> 大小；</li>
<li><code>uncased</code> 表示对大小写不敏感，或者说，就是小写的意思，表示这个模型在训练时将所有文本转换为小写。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span><span>(tokenizer.</span><span>encode</span><span>(</span></span><span>'hello world'</span><span>)) </span><span># 编码文本为 token id 列表</span></div></div><div><div><div>2</div></div><div><span>print</span><span><span>(tokenizer.</span><span>encode</span><span>(</span></span><span>'HELLO WORLD'</span><span>)) </span><span># 编码文本为 token id 列表，注意大写会被转换为小写</span></div></div><div><div><div>3</div></div><div><span>print</span><span><span>(tokenizer.</span><span>encode</span><span>(</span></span><span>'Hello World'</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[101, 7592, 2088, 102]</span></div></div><div><div><div>2</div></div><div><span>[101, 7592, 2088, 102]</span></div></div><div><div><div>3</div></div><div><span>[101, 7592, 2088, 102]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，转化出来的 token id 都是相同的。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer.</span><span>encode</span><span>(emotions_df.iloc[</span></span><span>6322</span><span>][</span><span>'text'</span><span>]) </span><span># 编码第 6322 条数据的文本内容为 token id 列表</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[101,</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>1045,</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>3984,</span></div></div><div><div><div>4</div></div><div><span>...</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>2005,</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>1996,</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>3480,</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>3924,</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>102]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意这里以 <code>[CLS]</code> 开头，<code>[SEP]</code> 结束。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(tokenizer.vocab_size) </span><span># 查看 tokenizer 的词汇表大小</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(tokenizer.model_max_length) </span><span># 查看 tokenizer 的最大输入长度</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(tokenizer.model_input_names) </span><span># 查看 tokenizer 的输入名称列表</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>30522</span></div></div><div><div><div>2</div></div><div><span>512</span></div></div><div><div><div>3</div></div><div><span>['input_ids', 'token_type_ids', 'attention_mask']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> special_id </span><span>in</span><span> tokenizer.all_special_ids:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>print</span><span><span>(special_id, tokenizer.</span><span>decode</span><span>(special_id)) </span></span><span># 查看特殊 token 的 id 和对应的字符串</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>100 [UNK]</span></div></div><div><div><div>2</div></div><div><span>102 [SEP]</span></div></div><div><div><div>3</div></div><div><span>0 [PAD]</span></div></div><div><div><div>4</div></div><div><span>101 [CLS]</span></div></div><div><div><div>5</div></div><div><span>103 [MASK]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这部分在前面我们探索 BERT 模型时已经进行了详尽的介绍，这里不再赘述。</p></section><section><h3>1.3.2 tokenize the whole dataset<a href="#132-tokenize-the-whole-dataset"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>batch_tokenize</span><span>(</span><span>batch</span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>return</span><span><span> </span><span>tokenizer</span><span>(batch[</span></span><span>'text'</span><span>], </span><span>padding</span><span>=</span><span>True</span><span>, </span><span>truncation</span><span>=</span><span>True</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions_encoded </span><span>=</span><span> emotions.</span><span>map</span><span>(batch_tokenize, </span></span><span>batched</span><span>=</span><span>True</span><span>, </span><span>batch_size</span><span>=</span><span>None</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>emotions_encoded</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DatasetDict({</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>train: Dataset({</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>num_rows: 16000</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>validation: Dataset({</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>num_rows: 2000</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>test: Dataset({</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask'],</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>num_rows: 2000</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>14</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，经过 tokenize 的数据集除了原来的 <code>text</code> 和 <code>label</code> 之外，还多出了 <code>input_ids</code>, <code>token_type_ids</code>, <code>attention_mask</code>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>batch_tokenize</span><span>(emotions[</span></span><span>'train'</span><span>][:</span><span>5</span><span>]) </span><span># 批量编码前五条数据的文本内容</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': [[101, 1045, 2134, 2102, 2514, 26608, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [101, 1045, 2064, 2175, 2013, 3110, 2061, 20625, 2000, 2061, 9636, 17772, 2074, 2013, 2108, 2105, 2619, 2040, 14977, 1998, 2003, 8300, 102], [101, 10047, 9775, 1037, 3371, 2000, 2695, 1045, 2514, 20505, 3308, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [101, 1045, 2572, 2412, 3110, 16839, 9080, 12863, 2055, 1996, 13788, 1045, 2097, 2113, 2008, 2009, 2003, 2145, 2006, 1996, 3200, 102, 0], [101, 1045, 2572, 3110, 24665, 7140, 11714, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里就是简单看一下具体编码出来的东西，还记得我们之前在 <code>batch_tokenize</code> 函数中设置了 <code>padding=True</code>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>type</span><span>(emotions_encoded[</span><span>'train'</span><span>][</span><span>'input_ids'</span><span>])) </span><span># 查看编码后的输入 id 的数据类型</span></div></div><div><div><div>2</div></div><div><span>emotions_encoded[</span><span>'train'</span><span>][</span><span>'input_ids'</span><span>][:</span><span>3</span><span>] </span><span># 查看编码后的输入 id 列表的前 3 条数据</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>datasets.arrow_dataset.Column</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>[[101,</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>1045,</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>2134,</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>2102,</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>2514,</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>26608,</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>102,</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>0,</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>0,</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>0,</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>0,</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>0,</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>0]]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的 <code>Column</code> 是 Hugging Face <code>dataset</code> 库中基于 Apache Arrow 格式的一种列式数据结构，可以简单地看作是一个增强版的 list：</p><ul>
<li>它的外部接口兼容 list，使用起来与 list 一样；</li>
<li>内部存储用 Arrow，兼顾了易用性和高性能。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions_encoded.</span><span>set_format</span><span>(</span></span><span>type</span><span>=</span><span>'torch'</span><span>, </span><span>columns</span><span><span>=</span><span>[</span></span><span>'input_ids'</span><span>, </span><span>'attention_mask'</span><span>, </span><span>'label'</span><span>]) </span><span># 设置数据集格式为 PyTorch，指定需要返回的列</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>type</span><span>(emotions_encoded[</span><span>'train'</span><span>][</span><span>'input_ids'</span><span>])) </span><span># 查看编码后的输入 id 的数据类型</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>type</span><span>(emotions_encoded[</span><span>'train'</span><span>][</span><span>0</span><span>][</span><span>'input_ids'</span><span>])) </span><span># 取单行</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>type</span><span>(emotions_encoded[</span><span>'train'</span><span>][:</span><span>5</span><span>][</span><span>'input_ids'</span><span>])) </span><span># 取多行</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;class 'datasets.arrow_dataset.Column'&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;class 'torch.Tensor'&gt;</span></div></div><div><div><div>3</div></div><div><span>&lt;class 'torch.Tensor'&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里需要注意的是，<code>set_format</code> 只在我们 <strong>取行（样本）</strong> 时生效，而不是在取整列时生效。</p><p>这是因为 <code>set_format</code> 主要是为了配合 PyTorch 的 <code>DataLoader</code> 使用——<code>DataLoader</code> 按行（样本）逐个取数据，而不是按列取。</p></section></section></section>
<section><h1>2. fine-tune transformers<a href="#2-fine-tune-transformers"><span>#</span></a></h1><section><h2>2.1 distilbert-base-uncased<a href="#21-distilbert-base-uncased"><span>#</span></a></h2><p><strong>DistilBERT 是 BERT 的精简版（蒸馏版）</strong>，通过**知识蒸馏（Knowledge Distillation）**技术训练得到。</p><p>核心区别：</p>

<table><thead><tr><th>对比项</th><th>BERT-base</th><th>DistilBERT</th></tr></thead><tbody><tr><td><strong>参数量</strong></td><td>~110M</td><td>~66M（<strong>减少 40%</strong>）</td></tr><tr><td><strong>层数（Transformer blocks）</strong></td><td>12 层</td><td>6 层（<strong>减半</strong>）</td></tr><tr><td><strong>推理速度</strong></td><td>基准</td><td><strong>快约 60%</strong></td></tr><tr><td><strong>性能（准确率）</strong></td><td>基准</td><td><strong>保留约 97%</strong> 的性能</td></tr><tr><td><strong>模型大小（磁盘）</strong></td><td>~440MB</td><td>~260MB</td></tr></tbody></table><hr /><p>如何实现的？</p><p>DistilBERT 使用 <strong>知识蒸馏</strong> 三步走：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1. 训练好一个大的 BERT（Teacher 模型）</span></div></div><div><div><div>2</div></div><div><span>2. DistilBERT（Student 模型）复制 Teacher 的中间层的一半</span></div></div><div><div><div>3</div></div><div><span>3. Student 学习模仿 Teacher 的输出分布，而不仅仅是 ground truth 标签</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键点：</p><ul>
<li><strong>层数减半</strong>：6 层 vs 12 层（DistilBERT 每 2 层对应 BERT 的 1 层）</li>
<li><strong>去掉了 Token Type IDs</strong>：DistilBERT 不支持 <code>token_type_ids</code>（因为蒸馏时去掉了下一句预测任务）</li>
<li><strong>保留了核心能力</strong>：仍支持 Masked LM，仍可用于分类、QA、NER 等任务</li>
</ul><p>一句话总结：</p><blockquote><p><strong>DistilBERT ≈ BERT 的 “轻量版”</strong>——体积小 40%、速度快 60%、性能只下降约 3%，非常适合<strong>资源受限</strong>的场景或对<strong>推理延迟</strong>有要求的应用。</p></blockquote><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoModel</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'distilbert-base-uncased'</span></div></div><div><div><div>4</div></div><div><span><span>model </span><span>=</span><span> AutoModel.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>5</div></div><div><span>model </span><span># 模型结构概览</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertModel(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(embeddings): Embeddings(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>(transformer): Transformer(</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>(0-5): 6 x TransformerBlock(</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>(attention): DistilBertSelfAttention(</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>(q_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(k_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>14</div></div><div><span><span>          </span></span><span>(v_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>          </span></span><span>(out_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>(ffn): FFN(</span></div></div><div><div><div>20</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>21</div></div><div><span><span>          </span></span><span>(lin1): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>          </span></span><span>(lin2): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>(activation): GELUActivation()</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>26</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里我们可以看到 DistilBERT 模型的整体架构：</p><hr /><section><h3>2.1.1 DistilBERT 整体架构概览<a href="#211-distilbert-整体架构概览"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertModel</span></div></div><div><div><div>2</div></div><div><span>├── Embeddings（嵌入层）</span></div></div><div><div><div>3</div></div><div><span>└── Transformer（Transformer 层）</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>└── 6 × TransformerBlock</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>├── Attention（多头自注意力）</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>├── sa_layer_norm（注意力后的 LayerNorm）</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>└── FFN（前馈神经网络）</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>└── output_layer_norm（FFN 后的 LayerNorm）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><section><h4>1️⃣ Embeddings（嵌入层）<a href="#1️⃣-embeddings嵌入层"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Embeddings(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(word_embeddings):       Embedding(30522, 768)     ← 词嵌入</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(position_embeddings):   Embedding(512, 768)       ← 位置嵌入</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(LayerNorm):             LayerNorm(768)            ← 层归一化</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>(dropout):               Dropout(p=0.1)            ← 随机失活</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>组件</th><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>word_embeddings</code></td><td>30522 × 768</td><td>词汇表大小 30522，每个词映射为 768 维向量</td></tr><tr><td><code>position_embeddings</code></td><td>512 × 768</td><td>最大序列长度 512，每个位置一个 768 维向量</td></tr><tr><td><code>LayerNorm</code></td><td>768</td><td>层归一化，稳定训练</td></tr><tr><td><code>Dropout</code></td><td>p=0.1</td><td>10% 的神经元随机丢弃，防止过拟合</td></tr></tbody></table><blockquote><p><strong>注意</strong>：与 BERT 不同，DistilBERT <strong>没有 <code>token_type_ids</code>（片段嵌入）</strong>，因为蒸馏时去掉了下一句预测任务。</p></blockquote><hr /></section><section><h4>2️⃣ Transformer（6 层 TransformerBlock）<a href="#2️⃣-transformer6-层-transformerblock"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(0-5): 6 x TransformerBlock</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>BERT-base 有 12 层，DistilBERT 精简为 <strong>6 层</strong>，每层由 Attention + FFN 组成。</p><hr /><section><h5>Attention（多头自注意力）<a href="#attention多头自注意力"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertSelfAttention(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(q_lin):     Linear(768 → 768)   ← Query 投影</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(k_lin):     Linear(768 → 768)   ← Key 投影</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(v_lin):     Linear(768 → 768)   ← Value 投影</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>(out_lin):   Linear(768 → 768)   ← 输出投影</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>(dropout):   Dropout(p=0.1)</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>输入维度</strong>：768（每个 token 的隐藏状态）</li>
<li><strong>输出维度</strong>：768</li>
<li><strong>多头注意力</strong>：q/k/v 的维度 768 会被拆分为多个注意力头（通常是 12 头，每头 64 维）</li>
<li>计算流程：<span><span>Attention(Q,K,V)=softmax(QKTdk)VAttention(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V</span><span><span><span></span><span>A</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span>(</span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>softmax</span></span><span></span><span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span></span><span></span><span>V</span></span></span></span></li>
<li><code>out_lin</code> 将多头结果拼接后投影回 768 维</li>
</ul><hr /></section><section><h5><code>sa_layer_norm</code>（注意力后的层归一化）<a href="#sa_layer_norm注意力后的层归一化"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>LayerNorm(768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在<strong>残差连接之后</strong>进行归一化。这是 DistilBERT 的标准化设计——先 Attention，再加残差，最后 LayerNorm。</p><hr /></section><section><h5>FFN（前馈神经网络）<a href="#ffn前馈神经网络"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>FFN(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(lin1):       Linear(768 → 3072)    ← 升维 4 倍</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(activation): GELUActivation()       ← 激活函数</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(lin2):       Linear(3072 → 768)    ← 降维回 768</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>(dropout):    Dropout(p=0.1)</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>两层线性层，中间是一个<strong>扩张-收缩</strong>结构：
<ul>
<li><code>lin1</code>：768 → 3072（<strong>扩大 4 倍</strong>）</li>
<li><code>GELU</code> 激活函数（高斯误差线性单元，比 ReLU 更平滑）</li>
<li><code>lin2</code>：3072 → 768（<strong>压缩回</strong>原始维度）</li>
</ul>
</li>
<li>这种结构让模型能在更高维空间中学习复杂的特征交互</li>
</ul><hr /></section><section><h5><code>output_layer_norm</code><a href="#output_layer_norm"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>LayerNorm(768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>与 Attention 一样，在 FFN 的<strong>残差连接之后</strong>再进行一次归一化。</p><hr /></section></section></section><section><h3>2.1.2 单层 TransformerBlock 的数据流<a href="#212-单层-transformerblock-的数据流"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入 (768)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌──────────────────────┐</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│  Self-Attention      │</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  Q/K/V 投影 + 多头计算 │</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>└────────┬─────────────┘</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>↓ (残差连接)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>+ 输入</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>LayerNorm (sa_layer_norm)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>┌──────────────────────┐</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>│  FFN                 │</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>│  768 → 3072 → 768    │</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>└────────┬─────────────┘</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>↓ (残差连接)</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>+ 输入</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>LayerNorm (output_layer_norm)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>↓</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>输出 (768) → 进入下一层</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h3>2.1.3 与 BERT-base 的架构差异总结<a href="#213-与-bert-base-的架构差异总结"><span>#</span></a></h3>

<table><thead><tr><th>组件</th><th>BERT-base (12 层)</th><th>DistilBERT (6 层)</th></tr></thead><tbody><tr><td><strong>嵌入层</strong></td><td>word + position + <strong>token_type</strong></td><td>word + position (<strong>无 token_type</strong>)</td></tr><tr><td><strong>每层结构</strong></td><td>Attention → LayerNorm → FFN → LayerNorm</td><td><strong>(残差后)</strong> LayerNorm → Attention → LayerNorm → FFN</td></tr><tr><td><strong>注意力头数</strong></td><td>12 头</td><td>12 头</td></tr><tr><td><strong>隐藏维度</strong></td><td>768</td><td>768</td></tr><tr><td><strong>激活函数</strong></td><td>GELU</td><td>GELU</td></tr></tbody></table><p>架构上的主要区别是 <strong>LayerNorm 的位置</strong>——DistilBERT 将 LayerNorm 移到了残差连接之后（Post-LN 的变体），这是一种更稳定的设计选择。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformer_utils </span><span>import</span><span> get_params</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 查看模型参数总数</span></div></div><div><div><div>2</div></div><div><span><span>get_params</span><span>(model) </span></span><span># np.int64(66362880)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoModel</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>4</div></div><div><span><span>model </span><span>=</span><span> AutoModel.</span><span>from_pretrained</span><span>(model_ckpt)</span></span></div></div><div><div><div>5</div></div><div><span><span>get_params</span><span>(model) </span></span><span># np.int64(109482240)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，<code>distilbert-base-uncased</code> 仅有 6 千万参数量；而我们之前频繁接触的 <code>bert-base-uncased</code> 有超过 1 亿参数量。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>27</div></div><div><span><span>          </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>(output): BertOutput(</span></div></div><div><div><div>30</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>BERT 模型的结构我们在之前的章节中已经介绍地非常具体了，二者模型的架构也在前面进行了详尽的对比，这里仅做回顾。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoModelForSequenceClassification</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>model_ckpt </span><span>=</span><span> </span></span><span>'distilbert-base-uncased'</span></div></div><div><div><div>4</div></div><div><span><span>device </span><span>=</span><span> torch.</span><span>device</span><span>(</span></span><span>'cuda'</span><span> </span><span>if</span><span><span> torch.cuda.</span><span>is_available</span><span>() </span></span><span>else</span><span> </span><span>'cpu'</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>model </span><span>=</span><span> AutoModelForSequenceClassification.</span><span>from_pretrained</span><span>(model_ckpt, </span></span><span>num_labels</span><span><span>=</span><span>num_classes).</span><span>to</span><span>(device)</span></span></div></div><div><div><div>6</div></div><div><span>model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertForSequenceClassification(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(distilbert): DistilBertModel(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(embeddings): Embeddings(</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>(transformer): Transformer(</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>(0-5): 6 x TransformerBlock(</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>(attention): DistilBertSelfAttention(</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>(q_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(k_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(v_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(out_lin): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(sa_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>20</div></div><div><span><span>          </span></span><span>(ffn): FFN(</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(lin1): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>(lin2): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>(activation): GELUActivation()</span></div></div><div><div><div>25</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>(output_layer_norm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>(pre_classifier): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>(classifier): Linear(in_features=768, out_features=6, bias=True)</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>(dropout): Dropout(p=0.2, inplace=False)</span></div></div><div><div><div>34</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>2.1.4 <code>DistilBertModel</code> vs <code>DistilBertForSequenceClassification</code><a href="#214-distilbertmodel-vs-distilbertforsequenceclassification"><span>#</span></a></h3><p>仔细观察输出，核心区别在于 <strong>分类头（Classification Head）</strong> 的添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertModel                                   DistilBertForSequenceClassification</span></div></div><div><div><div>2</div></div><div><span>│                                                 │</span></div></div><div><div><div>3</div></div><div><span>├── Embeddings                                    ├── Embeddings（完全相同）</span></div></div><div><div><div>4</div></div><div><span>│   ├── word_embeddings (30522, 768)              │   ├── word_embeddings (30522, 768)</span></div></div><div><div><div>5</div></div><div><span>│   ├── position_embeddings (512, 768)            │   ├── position_embeddings (512, 768)</span></div></div><div><div><div>6</div></div><div><span>│   ├── LayerNorm(768)                            │   ├── LayerNorm(768)</span></div></div><div><div><div>7</div></div><div><span>│   └── Dropout(p=0.1)                            │   └── Dropout(p=0.1)</span></div></div><div><div><div>8</div></div><div><span>│                                                 │</span></div></div><div><div><div>9</div></div><div><span>├── Transformer (6层)     ← 完全相同 →            ├── Transformer (6层)</span></div></div><div><div><div>10</div></div><div><span>│                                                 │</span></div></div><div><div><div>11</div></div><div><span>│                                  ⭐ 多出的部分：│</span></div></div><div><div><div>12</div></div><div><span>│                                                 ├── pre_classifier: Linear(768→768)</span></div></div><div><div><div>13</div></div><div><span>│                                                 ├── dropout: Dropout(p=0.2)  ← 注意：0.2</span></div></div><div><div><div>14</div></div><div><span>│                                                 └── classifier: Linear(768→6)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><section><h4>新增的三个组件详解<a href="#新增的三个组件详解"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>(pre_classifier): </span><span>Linear</span><span>(</span></span><span>in_features</span><span>=</span><span>768</span><span>, </span><span>out_features</span><span>=</span><span>768</span><span>, </span><span>bias</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>(dropout):       </span><span>Dropout</span><span>(</span></span><span>p</span><span>=</span><span>0.2</span><span>, </span><span>inplace</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>(classifier):    </span><span>Linear</span><span>(</span></span><span>in_features</span><span>=</span><span>768</span><span>, </span><span>out_features</span><span>=</span><span>6</span><span>, </span><span>bias</span><span>=</span><span>True</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>组件</th><th>维度变化</th><th>作用</th></tr></thead><tbody><tr><td><code>pre_classifier</code></td><td><strong>768 → 768</strong></td><td>额外的线性变换层，在分类前做一次特征映射，增加模型表达能力</td></tr><tr><td><code>Dropout(p=0.2)</code></td><td>—</td><td>20% 随机失活，比 base 模型的 0.1 更高，因为分类头容易过拟合</td></tr><tr><td><code>classifier</code></td><td><strong>768 → 6</strong></td><td>最终分类层，将 768 维隐层状态映射到 6 类情感得分</td></tr></tbody></table><p>这里需要注意的是，如果我们没有传入 <code>num_labels=num_classes</code> 参数，那么默认设置为 2 个分类头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>(pre_classifier): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(classifier): Linear(in_features=768, out_features=2, bias=True)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(dropout): Dropout(p=0.2, inplace=False)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而传入 <code>num_labels=num_classes</code> 后，根据我们前面计算的结果，<code>num_classes=6</code>，所以模型就有了 6 个分类头。</p><hr /></section><section><h4>数据流对比<a href="#数据流对比"><span>#</span></a></h4><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertModel (基础模型):</span></div></div><div><div><div>2</div></div><div><span>输入 → Embeddings → 6层 Transformer → [CLS]位置的768维向量</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>DistilBertForSequenceClassification (分类模型):</span></div></div><div><div><div>5</div></div><div><span>输入 → Embeddings → 6层 Transformer → [CLS]位置的768维向量</span></div></div><div><div><div>6</div></div><div><span><span>                                          </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span><span>                                    </span></span><span>pre_classifier (768→768)</span></div></div><div><div><div>8</div></div><div><span><span>                                          </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span><span>                                       </span></span><span>ReLU 激活</span></div></div><div><div><div>10</div></div><div><span><span>                                          </span></span><span>↓</span></div></div><div><div><div>11</div></div><div><span><span>                                    </span></span><span>Dropout(p=0.2)</span></div></div><div><div><div>12</div></div><div><span><span>                                          </span></span><span>↓</span></div></div><div><div><div>13</div></div><div><span><span>                                    </span></span><span>classifier (768→6)</span></div></div><div><div><div>14</div></div><div><span><span>                                          </span></span><span>↓</span></div></div><div><div><div>15</div></div><div><span><span>                                    </span></span><span>6 类情感得分 (logits)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>关键点：</p><ul>
<li><strong><code>DistilBertModel</code></strong> 输出的是所有 token 位置的完整 768 维隐层状态（<code>[batch_size, seq_len, 768]</code>），你需要自己取 <code>[CLS]</code> 并添加分类层</li>
<li><strong><code>DistilBertForSequenceClassification</code></strong> 已经内置了分类头，自动取 <code>[CLS]</code> token 的输出经过分类头，直接输出 <strong>6 类情感得分</strong>（<code>[batch_size, 6]</code>），可以直接用 <code>CrossEntropyLoss</code> 训练</li>
</ul><p>一句话总结：</p><blockquote><p><strong><code>DistilBertForSequenceClassification</code> = <code>DistilBertModel</code> + 分类头</strong>。base 模型只负责提取特征，<code>ForSequenceClassification</code> 在顶部额外挂了 <code>pre_classifier → Dropout → classifier</code> 三层，让你可以直接做分类任务。</p></blockquote><p>在运行完上述代码后，此时如果我们用 <code>nvidia-smi</code> 查看显卡状态，会发现模型已经加载到了显存中。</p><p>此外，还要注意上述代码的另一段输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Loading weights: 100%|██████████| 100/100 [00:00&lt;00:00, 16317.71it/s]</span></div></div><div><div><div>2</div></div><div><span>[transformers] [1mDistilBertForSequenceClassification LOAD REPORT[0m from: distilbert-base-uncased</span></div></div><div><div><div>3</div></div><div><span>Key                     | Status     |</span></div></div><div><div><div>4</div></div><div><span>------------------------+------------+-</span></div></div><div><div><div>5</div></div><div><span>vocab_transform.bias    | UNEXPECTED |</span></div></div><div><div><div>6</div></div><div><span>vocab_layer_norm.bias   | UNEXPECTED |</span></div></div><div><div><div>7</div></div><div><span>vocab_projector.bias    | UNEXPECTED |</span></div></div><div><div><div>8</div></div><div><span>vocab_layer_norm.weight | UNEXPECTED |</span></div></div><div><div><div>9</div></div><div><span>vocab_transform.weight  | UNEXPECTED |</span></div></div><div><div><div>10</div></div><div><span>pre_classifier.weight   | MISSING    |</span></div></div><div><div><div>11</div></div><div><span>classifier.weight       | MISSING    |</span></div></div><div><div><div>12</div></div><div><span>classifier.bias         | MISSING    |</span></div></div><div><div><div>13</div></div><div><span>pre_classifier.bias     | MISSING    |</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>Notes:</span></div></div><div><div><div>16</div></div><div><span>- UNEXPECTED:  can be ignored when loading from different task/architecture; not ok if you expect identical arch.</span></div></div><div><div><div>17</div></div><div><span>- MISSING:  those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里我们会发现，<code>pre_classifier.weight</code>, <code>classifier.weight</code>, <code>classifier.bias</code> 和 <code>pre_classifier.bias</code> 参数是没有被正确初始化的。换句话说，它们目前只是一些随机值。要想用 <code>DistilBertForSequenceClassification</code> 这个模型去做预测，我们需要在自己的下游任务中训练这个模型，以 fine-tune 这部分数值。</p></section></section></section><section><h2>2.2 trainer<a href="#22-trainer"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> huggingface_hub </span><span>import</span><span> notebook_login</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>notebook_login</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> TrainingArguments, Trainer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>batch_size </span><span>=</span><span> </span></span><span>64</span></div></div><div><div><div>2</div></div><div><span><span>logging_steps </span><span>=</span><span> </span></span><span>len</span><span>(emotions_encoded[</span><span>'train'</span><span><span>]) </span><span>//</span><span> batch_size</span></span></div></div><div><div><div>3</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>f</span><span>'</span><span>{</span><span>model_ckpt</span><span>}</span><span>-finetuned-emotion-classification'</span></div></div><div><div><div>4</div></div><div><span><span>training_args </span><span>=</span><span> </span><span>TrainingArguments</span><span>(</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>output_dir</span><span><span>=</span><span>model_name,</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>learning_rate</span><span>=</span><span>2e-5</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>num_train_epochs</span><span>=</span><span>4</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>per_device_train_batch_size</span><span><span>=</span><span>batch_size,</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>per_device_eval_batch_size</span><span><span>=</span><span>batch_size,</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>eval_strategy</span><span>=</span><span>'epoch'</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>weight_decay</span><span>=</span><span>0.01</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>disable_tqdm</span><span>=</span><span>False</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>logging_steps</span><span><span>=</span><span>logging_steps,</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>push_to_hub</span><span>=</span><span>True</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>log_level</span><span>=</span><span>'error'</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们来简单介绍一下上面的几个参数：</p><hr /><ul>
<li><code>eval_strategy='epoch'</code> ：<strong>评估策略</strong>；</li>
</ul><p>控制<strong>什么时候在验证集上评估模型</strong>。</p><blockquote><p>⚠️ <strong>注意</strong>：在 transformers 5.x 版本中，这个参数已从 <code>evaluation_strategy</code> <strong>重命名</strong>为 <code>eval_strategy</code>。如果用了旧名字会报错 <code>unexpected keyword argument</code>。</p></blockquote>

<table><thead><tr><th>取值</th><th>含义</th></tr></thead><tbody><tr><td><code>'no'</code></td><td>训练期间不评估</td></tr><tr><td><code>'steps'</code></td><td>每 <code>eval_steps</code> 步评估一次</td></tr><tr><td><code>'epoch'</code></td><td><strong>每个 epoch 结束后评估一次</strong> ✅ 最常用</td></tr></tbody></table><p>设置为 <code>'epoch'</code> 意味着每个 epoch 跑完后，模型会自动在验证集上计算损失和准确率，这样你就可以看到<strong>每个 epoch 的训练损失和验证损失</strong>，判断模型是否过拟合。</p><hr /><ul>
<li><code>weight_decay=0.01</code> ：<strong>权重衰减</strong>；</li>
</ul><p>权重衰减是一种 <strong>正则化技术</strong>，也叫 <strong>L2 正则化</strong>。它的核心思想是：在每次更新参数时，让权重<strong>向零方向稍微缩小一点点</strong>，防止模型过度依赖某些特征。</p><p><strong>数学原理</strong></p><p>标准梯度下降更新公式：</p><p><span><span>wt+1=wt−η⋅∇L(wt)w_{t+1} = w_t - \eta \cdot \nabla L(w_t)</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>t</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span></span><span>⋅</span><span></span></span><span><span></span><span>∇</span><span>L</span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></p><p>加入权重衰减后：</p><p><span><span>wt+1=wt−η⋅(∇L(wt)+λ⋅wt)w_{t+1} = w_t - \eta \cdot (\nabla L(w_t) + \lambda \cdot w_t)</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>t</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span></span><span>⋅</span><span></span></span><span><span></span><span>(</span><span>∇</span><span>L</span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>λ</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></p><p>展开：</p><p><span><span>wt+1=wt−η⋅∇L(wt)−ηλ⋅wtw_{t+1} = w_t - \eta \cdot \nabla L(w_t) - \eta \lambda \cdot w_t</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>t</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span></span><span>⋅</span><span></span></span><span><span></span><span>∇</span><span>L</span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span>λ</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></p><p><span><span>wt+1=(1−ηλ)⋅wt−η⋅∇L(wt)w_{t+1} = (1 - \eta\lambda) \cdot w_t - \eta \cdot \nabla L(w_t)</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>t</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span>λ</span><span>)</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span></span><span>⋅</span><span></span></span><span><span></span><span>∇</span><span>L</span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></p><p>可以看到，权重 <span><span>wtw_t</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在更新前先被乘以一个小于 1 的系数 <span><span>(1−ηλ)(1 - \eta\lambda)</span><span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>η</span><span>λ</span><span>)</span></span></span></span>，这就是”衰减”的含义。</p><blockquote><p>想象你在画一条曲线来拟合一些数据点。如果没有权重衰减，你可能会为了让曲线穿过每一个点而把曲线画得<strong>弯弯曲曲、剧烈抖动</strong>（过拟合）。权重衰减就像给曲线加了一个”<strong>弹性约束</strong>”——让曲线尽量<strong>平滑、简单</strong>，不要过度扭曲。</p></blockquote><p>一些直觉理解：</p><ul>
<li><strong>过拟合</strong>的模型往往权重值很大、很极端（对某些特征过度敏感）</li>
<li>权重衰减惩罚大权重，迫使权重<strong>保持较小的值</strong></li>
<li>小权重意味着模型对<strong>每个特征的依赖更均衡</strong>，不会因为某个特征而做出极端判断</li>
</ul><p>实际效果：</p><ul>
<li>✅ 防止过拟合</li>
<li>✅ 提高泛化能力</li>
<li>✅ 在 NLP 任务中几乎总是有益的</li>
<li>⚠️ 值太大（如 0.1）会导致欠拟合；值太小（如 0.0001）几乎没效果</li>
<li><code>0.01</code> 是一个比较常用且合理的值</li>
</ul><hr /><ul>
<li><code>disable_tqdm=False</code> ：<strong>显示进度条</strong>；</li>
</ul><hr /><ul>
<li><code>logging_steps=logging_steps</code> ：<strong>日志输出步长</strong>；</li>
</ul><p>控制<strong>每隔多少个训练步</strong>输出一次日志（损失值、学习率等）。</p><p>在我们的设置中，<strong>训练集有 16000 条数据，每批 64 条，所以每个 epoch 有 250 步。</strong> 因此 <code>logging_steps=250</code> 意味着<strong>每训练完一个 epoch 输出一次日志</strong>。</p><p>关于如何选择合适的值：</p>

<table><thead><tr><th>场景</th><th><code>logging_steps</code> 建议</th></tr></thead><tbody><tr><td><strong>想看每步的损失变化</strong></td><td>较小值，如 <code>10</code> 或 <code>50</code></td></tr><tr><td><strong>只想看每个 epoch 的概况</strong></td><td><code>len(train_dataset) // batch_size</code>（即每 epoch 一次）</td></tr><tr><td><strong>训练很快（几分钟）</strong></td><td>小一些，可以看到损失下降曲线</td></tr><tr><td><strong>训练很慢（几小时）</strong></td><td>大一些，避免日志太多刷屏</td></tr></tbody></table><hr /><ul>
<li><code>log_level='error'</code> ：<strong>日志级别</strong>；</li>
</ul><p>控制 Hugging Face Transformers 库本身输出的<strong>日志详细程度</strong>。</p><p>日志级别金字塔：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                    </span></span><span>╔═══════════════╗</span></div></div><div><div><div>2</div></div><div><span><span>                    </span></span><span>║   CRITICAL    ║  ← 最严重，程序可能崩溃</span></div></div><div><div><div>3</div></div><div><span><span>                    </span></span><span>╠═══════════════╣</span></div></div><div><div><div>4</div></div><div><span><span>                    </span></span><span>║    ERROR      ║  ← 发生了错误但程序还能继续</span></div></div><div><div><div>5</div></div><div><span><span>                    </span></span><span>╠═══════════════╣</span></div></div><div><div><div>6</div></div><div><span><span>                    </span></span><span>║    WARNING    ║  ← ⭐ 默认级别，潜在问题提醒</span></div></div><div><div><div>7</div></div><div><span><span>                    </span></span><span>╠═══════════════╣</span></div></div><div><div><div>8</div></div><div><span><span>                    </span></span><span>║     INFO      ║  ← 一般信息（模型加载、保存等）</span></div></div><div><div><div>9</div></div><div><span><span>                    </span></span><span>╠═══════════════╣</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>║    DEBUG      ║  ← 最详细，调试用</span></div></div><div><div><div>11</div></div><div><span><span>                    </span></span><span>└───────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>设置的含义：</p>

<table><thead><tr><th><code>log_level</code></th><th>显示的信息</th><th>举例</th></tr></thead><tbody><tr><td><code>'debug'</code></td><td>全部信息</td><td>最详细的调试输出</td></tr><tr><td><code>'info'</code></td><td>info 及以上</td><td>“Loading model…” “Saving checkpoint…”</td></tr><tr><td><code>'warning'</code></td><td>warning 及以上</td><td>“Some weights are not used” <strong>⭐ 默认</strong></td></tr><tr><td><code>'error'</code></td><td><strong>仅 error</strong></td><td>只有真正的错误才显示</td></tr><tr><td><code>'critical'</code></td><td>仅 critical</td><td>几乎什么都不显示</td></tr></tbody></table><p>在我们的场景下，<code>log_level='error'</code> 说明：</p><ul>
<li>这意味着 Transformers 库内部的所有信息、警告都被<strong>抑制了</strong></li>
<li>只有<strong>真正的错误</strong>才会打印出来</li>
<li>这样做的好处是<strong>输出更干净</strong>，不会被一大堆 “Some weights of the model checkpoint were not used…” 之类的警告刷屏</li>
</ul><hr /><p>关于其他训练设置：</p><ul>
<li>trainer 默认自动开启 torch 的多 gpu 模式；
<ul>
<li><code>per_device_train_batch_size</code> 设定每个 GPU 上的样本数量；</li>
<li>一般而言，多 GPU 模式希望多个 GPU 的性能尽量接近，否则多 GPU 的速度将由最慢的 GPU 决定；</li>
</ul>
</li>
<li><code>per_device_eval_batch_size</code> 类似；</li>
<li><code>learning_rate</code> 以及 <code>weight_decay</code> 默认采用 AdamW 的优化算法。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformer_utils </span><span>import</span><span> compute_classification_metrics</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>trainer </span><span>=</span><span> </span><span>Trainer</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>model</span><span><span>=</span><span>model,</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>args</span><span><span>=</span><span>training_args,</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>train_dataset</span><span><span>=</span><span>emotions_encoded[</span></span><span>'train'</span><span>],</span></div></div><div><div><div>7</div></div><div><span>    </span><span>eval_dataset</span><span><span>=</span><span>emotions_encoded[</span></span><span>'validation'</span><span>],</span></div></div><div><div><div>8</div></div><div><span>    </span><span>compute_metrics</span><span><span>=</span><span>compute_classification_metrics</span></span></div></div><div><div><div>9</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>trainer.</span><span>train</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>TrainOutput(global_step=1000, training_loss=0.31053504371643065, metrics={'train_runtime': 123.707, 'train_samples_per_second': 517.352, 'train_steps_per_second': 8.084, 'total_flos': 1440685723392000.0, 'train_loss': 0.31053504371643065, 'epoch': 4.0})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上面就是 Hugging Face <code>Trainer</code> 训练完成后输出的<strong>训练报告</strong>，包含了三个主要部分：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>TrainOutput</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>global_step</span><span>=</span><span>1000</span><span>,                                            </span><span># ① 总步数</span></div></div><div><div><div>3</div></div><div><span>    </span><span>training_loss</span><span>=</span><span>0.31053504371643065</span><span>,                           </span><span># ② 训练损失</span></div></div><div><div><div>4</div></div><div><span>    </span><span>metrics</span><span><span>=</span><span>{</span><span>...</span><span>}                                                </span></span><span># ③ 详细指标字典</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li><code>global_step=1000</code> — 总训练步数</li>
</ul><p><span><span>总步数=训练集样本数×epoch数÷batch_size\text{总步数} = \text{训练集样本数} \times \text{epoch数} \div batch\_size</span><span><span><span></span><span><span>总步数</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>训练集样本数</span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>epoch</span><span>数</span></span><span></span><span>÷</span><span></span></span><span><span></span><span>ba</span><span>t</span><span>c</span><span>h</span><span>_</span><span>s</span><span>i</span><span>z</span><span>e</span></span></span></span></p><p><span><span>=16,000×4÷64=1,000= 16,000 \times 4 \div 64 = 1,000</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>16</span><span>,</span><span></span><span>000</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span><span></span><span>÷</span><span></span></span><span><span></span><span>64</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>000</span></span></span></span></p><hr /><ul>
<li><code>training_loss=0.3105</code> — 平均训练损失</li>
</ul><p><strong>损失函数（Loss）</strong> 衡量的是模型预测与真实标签之间的差距。</p><p><span><span>Loss=−1N∑i=1N∑c=1Cyi,clog⁡(pi,c)\text{Loss} = -\frac{1}{N}\sum_{i=1}^{N}\sum_{c=1}^{C} y_{i,c} \log(p_{i,c})</span><span><span><span></span><span><span>Loss</span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>N</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span><span>N</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>c</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span><span>C</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>c</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>lo<span>g</span></span><span>(</span><span><span>p</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>c</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></p><p>这是<strong>交叉熵损失（Cross-Entropy Loss）</strong>，多分类任务的标配：</p>

<table><thead><tr><th>损失值</th><th>含义</th></tr></thead><tbody><tr><td><strong>~2.0 以上</strong></td><td>模型几乎在瞎猜（6 类随机猜 ≈ log6 ≈ 1.79）</td></tr><tr><td><strong>~1.0</strong></td><td>模型开始学到一些规律</td></tr><tr><td><strong>~0.31（我们的值）</strong></td><td>✅ 模型学到了很强的规律</td></tr><tr><td><strong>~0.0</strong></td><td>完美拟合（也要小心过拟合）</td></tr></tbody></table><p>从开始时的较高损失下降到 <code>0.31</code>，说明模型<strong>收敛得很好</strong>。</p><hr /><ul>
<li><code>metrics</code> — 详细指标</li>
</ul><p>训练用时：</p>

<table><thead><tr><th>指标</th><th>值</th><th>解读</th></tr></thead><tbody><tr><td><code>train_runtime</code></td><td><strong>123.7 秒（约 2 分钟）</strong></td><td>训练总耗时</td></tr><tr><td><code>train_samples_per_second</code></td><td><strong>517.35 条/秒</strong></td><td>每秒处理 517 条文本</td></tr><tr><td><code>train_steps_per_second</code></td><td><strong>8.08 步/秒</strong></td><td>每秒更新 8 次参数</td></tr></tbody></table><p>这里使用的设备是单张 RTX4080 laptop.</p><p>总计算量：</p>

<table><thead><tr><th>指标</th><th>值</th><th>解读</th></tr></thead><tbody><tr><td><code>total_flos</code></td><td><strong>1.44 × 10¹⁵ FLOPs</strong></td><td>浮点运算总数</td></tr></tbody></table><blockquote><p><strong>FLOPs（Floating Point Operations）</strong>：衡量计算量的单位。这个数字的意思是——整个训练过程一共执行了约 <strong>1.4 千万亿次</strong>浮点运算。</p></blockquote><p>最终损失：</p>

<table><thead><tr><th>指标</th><th>值</th></tr></thead><tbody><tr><td><code>train_loss</code></td><td><strong>0.3105</strong>（和上面的 <code>training_loss</code> 一样）</td></tr></tbody></table><hr /><p>一张图总结：</p><div><div><div><div><span><p>结果</p></span></div><div><span><p>训练配置</p></span></div><div><span><p>输入</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>16000 条推文<br /><br />6 类情感</p></span></div><div><span><p>DistilBERT<br /><br />66M 参数</p></span></div><div><span><p>Batch = 64<br /><br />Epoch = 4</p></span></div><div><span><p>总步数 = 1000</p></span></div><div><span><p>训练损失: 0.31<br /><br />耗时: 2 分钟</p></span></div>
</div><div><div><span><p>结果</p></span></div><div><span><p>训练配置</p></span></div><div><span><p>输入</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>16000 条推文<br /><br />6 类情感</p></span></div><div><span><p>DistilBERT<br /><br />66M 参数</p></span></div><div><span><p>Batch = 64<br /><br />Epoch = 4</p></span></div><div><span><p>总步数 = 1000</p></span></div><div><span><p>训练损失: 0.31<br /><br />耗时: 2 分钟</p></span></div>
</div></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>preds_output </span><span>=</span><span> trainer.</span><span>predict</span><span>(emotions_encoded[</span></span><span>'validation'</span><span>])</span></div></div><div><div><div>2</div></div><div><span>preds_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>PredictionOutput(predictions=array([[ 5.302656  , -1.1895534 , -1.4358829 , -1.5534474 , -1.7579308 ,</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>-1.9462327 ],</span></div></div><div><div><div>3</div></div><div><span><span>       </span></span><span>[ 5.2939024 , -1.075267  , -2.0208688 , -1.311696  , -1.3683631 ,</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>-1.9535345 ],</span></div></div><div><div><div>5</div></div><div><span><span>       </span></span><span>[-1.6112493 ,  2.7801807 ,  3.1485417 , -1.6580076 , -2.5439014 ,</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>-2.3933396 ],</span></div></div><div><div><div>7</div></div><div><span><span>       </span></span><span>...,</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>[-1.6095595 ,  5.4891787 , -0.56173605, -1.5967947 , -2.361022  ,</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>-1.9980701 ],</span></div></div><div><div><div>10</div></div><div><span><span>       </span></span><span>[-2.050608  ,  3.349502  ,  3.0134823 , -1.7501646 , -2.6897838 ,</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>-2.3941755 ],</span></div></div><div><div><div>12</div></div><div><span><span>       </span></span><span>[-1.664848  ,  5.413096  , -0.7544189 , -2.052569  , -2.1643562 ,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>-1.0560712 ]], shape=(2000, 6), dtype=float32), label_ids=array([0, 0, 2, ..., 1, 1, 1], shape=(2000,)), metrics={'test_loss': 0.14947177469730377, 'test_accuracy': 0.934, 'test_precision': 0.9347108725250716, 'test_recall': 0.934, 'test_f1': 0.934171624082316, 'test_runtime': 1.3177, 'test_samples_per_second': 1517.783, 'test_steps_per_second': 24.285})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上面就是 <code>trainer.predict()</code> 在 <strong>验证集</strong>（2000 条样本）上的输出，它的结构如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>PredictionOutput</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>predictions</span><span><span>=</span><span>...</span><span>,    </span></span><span># ① 模型输出的原始分数（logits）</span></div></div><div><div><div>3</div></div><div><span>    </span><span>label_ids</span><span><span>=</span><span>...</span><span>,      </span></span><span># ② 真实标签</span></div></div><div><div><div>4</div></div><div><span>    </span><span>metrics</span><span><span>=</span><span>...</span><span>         </span></span><span># ③ 评估指标</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>predictions</code> ：<strong>模型输出的原始分数</strong>。</li>
</ul><p>形状为 <code>(2000, 6)</code>，即 2000 条样本 × 6 类情感，每个值叫做 <strong>logit（logits）</strong>——也就是 softmax 之前的原始分数。</p><p>看前几条数据：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>样本1: [ 5.30, -1.19, -1.44, -1.55, -1.76, -1.95]  → 类别 0 (sadness) 得分最高 ✅</span></div></div><div><div><div>2</div></div><div><span>样本2: [ 5.29, -1.08, -2.02, -1.31, -1.37, -1.95]  → 类别 0 (sadness) 得分最高 ✅</span></div></div><div><div><div>3</div></div><div><span>样本3: [-1.61,  2.78,  3.15, -1.66, -2.54, -2.39]  → 类别 2 (love) 得分最高 ✅</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Logits 如何变成预测结果？</p><p><span><span>softmax(zi)=ezi∑j=16ezj→argmax→最终类别\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{6} e^{z_j}} \quad\rightarrow\quad \text{argmax} \rightarrow \text{最终类别}</span><span><span><span></span><span><span>softmax</span></span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>j</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span><span>6</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>z</span><span><span><span><span><span><span></span><span>j</span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>z</span><span><span><span><span><span><span></span><span>i</span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span></span><span>→</span><span></span><span></span></span><span><span></span><span><span>argmax</span></span><span></span><span>→</span><span></span></span><span><span></span><span><span>最终类别</span></span></span></span></span></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># compute_classification_metrics 中正是这样做的</span></div></div><div><div><div>2</div></div><div><span><span>preds </span><span>=</span><span> pred.predictions.</span><span>argmax</span><span>(</span><span>-</span></span><span>1</span><span>)   </span><span># 取每行最大值所在的索引 → 预测类别</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>原始 logits</th><th>softmax 后</th><th>argmax（预测类别）</th></tr></thead><tbody><tr><td>[<strong>5.30</strong>, -1.19, -1.44, …]</td><td>[<strong>0.998</strong>, 0.0003, 0.0002, …]</td><td><strong>0 (sadness)</strong> ✅</td></tr><tr><td>[-1.61, 2.78, <strong>3.15</strong>, …]</td><td>[0.006, 0.407, <strong>0.574</strong>, …]</td><td><strong>2 (love)</strong> ✅</td></tr></tbody></table><hr /><ul>
<li><code>label_ids</code> ：<strong>真实标签</strong></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>array([0, 0, 2, ..., 1, 1, 1], shape=(2000,))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对应 6 类情感：</p>

<table><thead><tr><th>数字</th><th>情感</th></tr></thead><tbody><tr><td>0</td><td>sadness</td></tr><tr><td>1</td><td>joy</td></tr><tr><td>2</td><td>love</td></tr><tr><td>3</td><td>anger</td></tr><tr><td>4</td><td>fear</td></tr><tr><td>5</td><td>surprise</td></tr></tbody></table><hr /><ul>
<li><code>metrics</code> ：<strong>评估指标 + 核心结果</strong></li>
</ul><p>总体性能：</p>

<table><thead><tr><th>指标</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>test_loss</code></td><td><strong>0.1495</strong></td><td>验证集上的损失，比训练损失 0.31 还低，说明模型泛化很好</td></tr><tr><td><code>test_accuracy</code></td><td><strong>0.934 (93.4%)</strong></td><td>准确率——2000 条中正确预测了约 <strong>1868 条</strong></td></tr><tr><td><code>test_precision</code></td><td><strong>0.9347</strong></td><td>精确率（加权平均）</td></tr><tr><td><code>test_recall</code></td><td><strong>0.9340</strong></td><td>召回率（加权平均）</td></tr><tr><td><code>test_f1</code></td><td><strong>0.9342</strong></td><td>F1 分数（精确率和召回率的调和平均）</td></tr></tbody></table><blockquote><p>四个指标都接近 93.4%，说明模型在<strong>各类情感上表现均衡</strong>，没有对某些类别特别偏袒或歧视。</p></blockquote><p>推理性能：</p>

<table><thead><tr><th>指标</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>test_runtime</code></td><td><strong>1.32 秒</strong></td><td>评估 2000 条只用了 1.3 秒</td></tr><tr><td><code>test_samples_per_second</code></td><td><strong>1517.78 条/秒</strong></td><td>每秒处理约 1500 条推文</td></tr><tr><td><code>test_steps_per_second</code></td><td><strong>24.29 步/秒</strong></td><td>每秒执行约 24 步</td></tr></tbody></table><p>相比训练时每秒 517 条，推理速度快了 <strong>3 倍</strong>（因为推理不需要反向传播计算梯度）。</p><hr /><p>一张图总结：</p><div><div><div><div><span><p>结果</p></span></div><div><span><p>对比</p></span></div><div><span><p>后处理</p></span></div><div><span><p>模型推理</p></span></div><div><span><p>输入</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>验证集<br /><br />2000 条推文</p></span></div><div><span><p>DistilBERT<br /><br />→ 6 类 logits<br /><br />(2000 × 6)</p></span></div><div><span><p>softmax → 概率分布</p></span></div><div><span><p>argmax → 预测类别</p></span></div><div><span><p>预测类别 vs 真实标签</p></span></div><div><span><p>✅ 准确率 93.4%<br /><br />✅ F1 分数 93.4%<br /><br />✅ 仅用 1.3 秒</p></span></div>
</div><div><div><span><p>结果</p></span></div><div><span><p>对比</p></span></div><div><span><p>后处理</p></span></div><div><span><p>模型推理</p></span></div><div><span><p>输入</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>验证集<br /><br />2000 条推文</p></span></div><div><span><p>DistilBERT<br /><br />→ 6 类 logits<br /><br />(2000 × 6)</p></span></div><div><span><p>softmax → 概率分布</p></span></div><div><span><p>argmax → 预测类别</p></span></div><div><span><p>预测类别 vs 真实标签</p></span></div><div><span><p>✅ 准确率 93.4%<br /><br />✅ F1 分数 93.4%<br /><br />✅ 仅用 1.3 秒</p></span></div>
</div></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>preds_output </span><span>=</span><span> trainer.</span><span>predict</span><span>(emotions_encoded[</span></span><span>'validation'</span><span>])</span></div></div><div><div><div>2</div></div><div><span><span>y_preds </span><span>=</span><span> np.</span><span>argmax</span><span>(preds_output.predictions, </span></span><span>axis</span><span>=-</span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就是提取预测结果中的类别，其中：</p><ul>
<li><code>trainer.predict()</code> 返回一个 <code>PredictionOutput</code> 对象，包含三个部分：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>PredictionOutput</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>predictions</span><span><span>=</span><span>array</span><span>(</span></span><span>shape</span><span><span>=</span><span>(</span></span><span>2000</span><span>, </span><span>6</span><span>)),   </span><span># ← 每条样本对 6 类情感的原始得分 (logits)</span></div></div><div><div><div>3</div></div><div><span>    </span><span>label_ids</span><span><span>=</span><span>array</span><span>(</span></span><span>shape</span><span><span>=</span><span>(</span></span><span>2000</span><span>,)),       </span><span># ← 每条样本的真实标签 (0~5)</span></div></div><div><div><div>4</div></div><div><span>    </span><span>metrics</span><span><span>=</span><span>{</span><span>...</span><span>}                          </span></span><span># ← 评估指标</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>np.argmax()</code> 就是沿着最后一个维度（6 类情感）找出得分最高的索引。
<ul>
<li><code>predictions</code> 的形状如上所示，为 (2000, 6)，每一行是这条样本对 6 类情感的得分。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>y_true </span><span>=</span><span> emotions_encoded[</span></span><span>'validation'</span><span>][</span><span>'label'</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们从预处理好的验证集中提取 <strong>真实标签列</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># import importlib</span></div></div><div><div><div>2</div></div><div><span># import transformer_utils</span></div></div><div><div><div>3</div></div><div><span># importlib.reload(transformer_utils)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformer_utils </span><span>import</span><span> plot_confusion_matrix</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>plot_confusion_matrix</span><span>(y_true, y_preds, </span></span><span>labels</span><span><span>=</span><span>labels)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出如下图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619215942480.png" alt="" /></p><p>从输出的混淆矩阵可以看出：</p><ul>
<li>97% 的 sadness 被正确预测；
<ul>
<li>分别有 1% 的 sadness 被错误预测为了 anger 和 fear.</li>
</ul>
</li>
<li>95% 的 joy 被正确预测；</li>
<li>93% 的 anger 被正确预测；</li>
<li>91% 的 fear 被正确预测；</li>
<li>88% 的 love 被正确预测；</li>
<li>81% 的 surprise 被正确预测；</li>
</ul><p>下面我们来看测试集的结果。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>preds_output </span><span>=</span><span> trainer.</span><span>predict</span><span>(emotions_encoded[</span></span><span>'test'</span><span>])</span></div></div><div><div><div>2</div></div><div><span><span>y_preds </span><span>=</span><span> np.</span><span>argmax</span><span>(preds_output.predictions, </span></span><span>axis</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>y_true </span><span>=</span><span> emotions_encoded[</span></span><span>'test'</span><span>][</span><span>'label'</span><span>]</span></div></div><div><div><div>4</div></div><div><span><span>plot_confusion_matrix</span><span>(y_true, y_preds, </span></span><span>labels</span><span><span>=</span><span>labels)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出结果如下图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619220902249.png" alt="" /></p><p>可以看到，在测试集上，模型在区分 surprise 和 fear 时表现较差，有 23% 的 surprise 都被错误归类为了 fear.</p></section></section>
<section><h1>3. result analysis<a href="#3-result-analysis"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> torch.nn.functional </span><span>import</span><span> cross_entropy</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tokenizer.model_input_names</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>forward_pass_with_label</span><span>(</span><span>batch</span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span># 将输入数据移动到设备上，并过滤出模型需要的输入</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>inputs </span><span>=</span><span> {k: v.</span><span>to</span><span>(device) </span><span>for</span><span> k, v </span><span>in</span><span><span> batch.</span><span>items</span><span>() </span></span><span>if</span><span> k </span><span>in</span><span> tokenizer.model_input_names}</span></div></div><div><div><div>4</div></div><div><span>    </span><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>outputs </span><span>=</span><span> </span><span>model</span><span>(**inputs)</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>pred_label </span><span>=</span><span> torch.</span><span>argmax</span><span>(outputs.logits, </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>loss </span><span>=</span><span> </span><span>cross_entropy</span><span>(outputs.logits, batch[</span><span>'label'</span><span><span>].</span><span>to</span><span>(device), </span></span><span>reduction</span><span>=</span><span>'none'</span><span>)</span></div></div><div><div><div>8</div></div><div><span>    </span><span># 将 loss 和 pred_label 从 GPU 移动到 CPU，并转换为 numpy 数组返回</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> {</span><span>'loss'</span><span><span>: loss.</span><span>cpu</span><span>().</span><span>numpy</span><span>(), </span></span><span>'pred_label'</span><span><span>: pred_label.</span><span>cpu</span><span>().</span><span>numpy</span><span>()}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个函数的目的是对每一条样本单独计算损失，而非对整个 batch 求平均：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>inputs </span><span>=</span><span> {k: v.</span><span>to</span><span>(device) </span></span><span>for</span><span> k, v </span><span>in</span><span><span> batch.</span><span>items</span><span>() </span></span><span>if</span><span> k </span><span>in</span><span> tokenizer.model_input_names}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>遍历 batch 中的所有字段，即 <code>['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask', 'loss', 'pred_label']</code>；</li>
<li>只保留 <code>tokenizer.model_input_names</code> 中的字段，即 <code>['input_ids', 'token_type_ids', 'attention_mask']</code>，排除 <code>label</code> 和 <code>text</code> 等非模型输入字段；</li>
<li>将所有选中的输入移到 GPU 上，即 <code>device</code>.</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>outputs </span><span>=</span><span> </span><span>model</span><span>(**inputs)</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>pred_label </span><span>=</span><span> torch.</span><span>argmax</span><span>(outputs.logits, </span><span>dim</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>loss </span><span>=</span><span> </span><span>cross_entropy</span><span>(outputs.logits, batch[</span><span>'label'</span><span><span>].</span><span>to</span><span>(device), </span></span><span>reduction</span><span>=</span><span>'none'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里是很经典的前向传播过程了，因此这里只重点介绍一下 <code>cross_entropy</code> 的参数：</p><ul>
<li><code>outputs.logits</code> ：预测，形状为 (batch_size, 6)，原始的 logits</li>
<li><code>batch['label'].to(device)</code> ：真实的标签，形状为 (batch_size,)，每个值 0-5.
<ul>
<li>要注意的是因为我们前面是在 GPU 上创建了 <code>inputs</code>，而此时 <code>batch</code> 还在 CPU 上，PyTorch 要求参与计算的张量必须在同一个设备上，所以我们这里要将 <code>batch['label']</code> 移动到 GPU.</li>
</ul>
</li>
<li><code>reduction='none'</code> ：表示不对 batch 求 mean，而是返回每个样本独立的 loss，形状从 () 变为 (batch_size,).
<ul>
<li>默认情形下，<code>reduction='mean'</code>，即把一个 batch 中所有样本的损失平均成一个数。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>return</span><span> {</span><span>'loss'</span><span><span>: loss.</span><span>cpu</span><span>().</span><span>numpy</span><span>(), </span></span><span>'pred_label'</span><span><span>: pred_label.</span><span>cpu</span><span>().</span><span>numpy</span><span>()}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>将 GPU 上的张量移回 CPU，否则 HuggingFace Dataset 无法存储；</li>
<li>转为 numpy 数组，便于后续分析和可视化；</li>
<li>返回一个字典，包含两个字段：<code>loss</code> 和 <code>pred_label</code>.</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>emotions_encoded[</span><span>'validation'</span><span><span>] </span><span>=</span><span> emotions_encoded[</span></span><span>'validation'</span><span><span>].</span><span>map</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>forward_pass_with_label, </span><span>batched</span><span>=</span><span>True</span><span>, </span><span>batch_size</span><span>=</span><span>16</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>通过 <code>map</code> 将 <code>forward_pass_with_label</code> 应用到整个验证集上。这样，验证集的每条样本都多了两个新字段：</p><ul>
<li><code>loss</code> ：该样本的损失；</li>
<li><code>pred_label</code> ：模型预测的类别。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>emotions_encoded[</span><span>'validation'</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Dataset({</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>features: ['text', 'label', 'input_ids', 'token_type_ids', 'attention_mask', 'loss', 'pred_label'],</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>num_rows: 2000</span></div></div><div><div><div>4</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，现在验证集中确实多出了两个新字段。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>selected_cols </span><span>=</span><span> [</span></span><span>'text'</span><span>, </span><span>'label'</span><span>, </span><span>'loss'</span><span>, </span><span>'pred_label'</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>emotions_encoded.</span><span>reset_format</span><span>()</span></span></div></div><div><div><div>2</div></div><div><span><span>valid_df </span><span>=</span><span> pd.DataFrame.</span><span>from_dict</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>4</div></div><div><span>        </span><span>'text'</span><span>: emotions_encoded[</span><span>'validation'</span><span>][</span><span>'text'</span><span>],</span></div></div><div><div><div>5</div></div><div><span>        </span><span>'label'</span><span>: emotions_encoded[</span><span>'validation'</span><span>][</span><span>'label'</span><span>],</span></div></div><div><div><div>6</div></div><div><span>        </span><span>'pred_label'</span><span>: emotions_encoded[</span><span>'validation'</span><span>][</span><span>'pred_label'</span><span>],</span></div></div><div><div><div>7</div></div><div><span>        </span><span>'loss'</span><span>: emotions_encoded[</span><span>'validation'</span><span>][</span><span>'loss'</span><span>]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div><span>valid_df[</span><span>'label'</span><span><span>] </span><span>=</span><span> valid_df[</span></span><span>'label'</span><span><span>].</span><span>apply</span><span>(</span></span><span>lambda</span><span> </span><span>x</span><span>: labels[x])</span></div></div><div><div><div>11</div></div><div><span>valid_df[</span><span>'pred_label'</span><span><span>] </span><span>=</span><span> valid_df[</span></span><span>'pred_label'</span><span><span>].</span><span>apply</span><span>(</span></span><span>lambda</span><span> </span><span>x</span><span>: labels[x])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>valid_df</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                        </span></span><span>text                          label  pred_label  loss</span></div></div><div><div><div>2</div></div><div><span>0  im feeling quite sad and sorry for myself but ...  sadness  sadness      0.005036</span></div></div><div><div><div>3</div></div><div><span>1  i feel like i am still looking at a blank canv...  sadness  sadness      0.005398</span></div></div><div><div><div>4</div></div><div><span>2  i feel like a faithful servant                      love  love      0.340381</span></div></div><div><div><div>5</div></div><div><span>3  i am just feeling cranky and blue                  anger  anger      0.006064</span></div></div><div><div><div>6</div></div><div><span>4  i can have for a treat or if i am feeling festive  joy      joy          0.004903</span></div></div><div><div><div>7</div></div><div><span>...  ...  ...  ...  ...</span></div></div><div><div><div>8</div></div><div><span>1995  im having ssa examination tomorrow in the morn...  sadness  sadness  0.006089</span></div></div><div><div><div>9</div></div><div><span>1996  i constantly worry about their fight against n...  joy      joy      0.005474</span></div></div><div><div><div>10</div></div><div><span>1997  i feel its important to share this info for th...  joy      joy      0.004890</span></div></div><div><div><div>11</div></div><div><span>1998  i truly feel that if you are passionate enough...  joy      joy      0.575697</span></div></div><div><div><div>12</div></div><div><span>1999  i feel like i just wanna buy any cute make up ...  joy      joy      0.006726</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>2000 rows × 4 columns</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>valid_df[valid_df[</span><span>'label'</span><span><span>] </span><span>!=</span><span> valid_df[</span></span><span>'pred_label'</span><span>]]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                       </span></span><span>text                              label  pred_label  loss</span></div></div><div><div><div>2</div></div><div><span>17  i know what it feels like he stressed glaring ...      anger  sadness      2.504660</span></div></div><div><div><div>3</div></div><div><span>27  i feel as if i am the beloved preparing hersel...      joy      love      1.369006</span></div></div><div><div><div>4</div></div><div><span>35  i am feeling very blessed today that they shar...      joy      love      0.834904</span></div></div><div><div><div>5</div></div><div><span>60  i miss our talks our cuddling our kissing and ...      love  joy          0.946954</span></div></div><div><div><div>6</div></div><div><span>91  i feel like the people i know are really gener...      joy      love      0.953229</span></div></div><div><div><div>7</div></div><div><span>...  ...  ...  ...  ...</span></div></div><div><div><div>8</div></div><div><span>1958  i so desperately want to be able to help but i...  fear  sadness      0.850377</span></div></div><div><div><div>9</div></div><div><span>1963  i called myself pro life and voted for perry w...  joy      sadness      5.088876</span></div></div><div><div><div>10</div></div><div><span>1964  i feel vaguely cheated and a little amused          joy      anger      4.081220</span></div></div><div><div><div>11</div></div><div><span>1981  i spent a lot of time feeling overwhelmed with...  fear  surprise  0.746432</span></div></div><div><div><div>12</div></div><div><span>1990  i just feel too overwhelmed i can t see the fo...  fear  surprise  0.984590</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>130 rows × 4 columns</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里输出的就是验证集上所有预测错误的样本，总共 130 条。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># most labels incorrectly</span></div></div><div><div><div>2</div></div><div><span>valid_df[valid_df[</span><span>'label'</span><span><span>] </span><span>!=</span><span> valid_df[</span></span><span>'pred_label'</span><span><span>]].label.</span><span>value_counts</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>label</span></div></div><div><div><div>2</div></div><div><span>joy         38</span></div></div><div><div><div>3</div></div><div><span>love        22</span></div></div><div><div><div>4</div></div><div><span>fear        20</span></div></div><div><div><div>5</div></div><div><span>anger       18</span></div></div><div><div><div>6</div></div><div><span>sadness     17</span></div></div><div><div><div>7</div></div><div><span>surprise    15</span></div></div><div><div><div>8</div></div><div><span>Name: count, dtype: int64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们可以查看分类错误的样本里的内部分布。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>valid_df.</span><span>sort_values</span><span>(</span></span><span>by</span><span>=</span><span>'loss'</span><span>, </span><span>ascending</span><span>=</span><span>False</span><span><span>).</span><span>head</span><span>(</span></span><span>10</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                          </span></span><span>text                              label      pred_label  loss</span></div></div><div><div><div>2</div></div><div><span>1500  i guess we would naturally feel a sense of lon...      anger      sadness      6.607152</span></div></div><div><div><div>3</div></div><div><span>1950  i as representative of everything thats wrong ...      surprise  sadness      6.467403</span></div></div><div><div><div>4</div></div><div><span>1111  im lazy my characters fall into categories of ...      joy          fear      6.427592</span></div></div><div><div><div>5</div></div><div><span>1509  i guess this is a memoir so it feels like that...      joy          fear      6.364819</span></div></div><div><div><div>6</div></div><div><span>882      i feel badly about reneging on my commitment t...      love      sadness      6.108779</span></div></div><div><div><div>7</div></div><div><span>1963  i called myself pro life and voted for perry w...      joy          sadness      5.088876</span></div></div><div><div><div>8</div></div><div><span>1840  id let you kill it now but as a matter of fact...      joy          fear      5.073016</span></div></div><div><div><div>9</div></div><div><span>318      i felt ashamed of these feelings and was scare...      fear      sadness      4.791914</span></div></div><div><div><div>10</div></div><div><span>405      i have been feeling extraordinarily indecisive...      fear      joy          4.677145</span></div></div><div><div><div>11</div></div><div><span>1836  i got a very nasty electrical shock when i was...      fear      anger      4.654828</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以打印出 loss 最高的前 10 条样本。</p><p>我们可以举其中一个例子：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>valid_df.iloc[</span><span>882</span><span>].text</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>'i feel badly about reneging on my commitment to bring donuts to the faithful at holy family catholic church in columbus ohio'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>中文翻译过来就是：“我为自己食言，没有给俄亥俄州哥伦布市圣家天主教堂的信徒们带甜甜圈而感到愧疚。”</p><p>你发现了吗？这其实是一个标记错误的数据，数据集中给它的 label 是 love，但显然，这句话并不是 love，因此模型预测的 sadness 反而是对的。</p><p>对于 <code>dair-ai/emotion</code> 这个数据集而言，它的数据来自于推文，一般的标注流程是：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>采集推文</p></span></div><div><span><p>分发给多个标注者</p></span></div><div><span><p>标注者 A: love</p></span></div><div><span><p>标注者 B: joy</p></span></div><div><span><p>标注者 C: sadness</p></span></div><div><span><p>取多数投票<br /><br />或随机选择一个</p></span></div><div><span><p>最终标签: love</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>采集推文</p></span></div><div><span><p>分发给多个标注者</p></span></div><div><span><p>标注者 A: love</p></span></div><div><span><p>标注者 B: joy</p></span></div><div><span><p>标注者 C: sadness</p></span></div><div><span><p>取多数投票<br /><br />或随机选择一个</p></span></div><div><span><p>最终标签: love</p></span></div>
</div></div></div><p>由于情感标注本身就具有主观性，加之推文是 <strong>脱离上下文</strong> 被标注的，所以不同的标注者很有可能做出不同的标注。实际上，在学术数据集中，“噪声标签” 是普遍存在的。</p><p>所以，我们之前看到的模型 93.4% 的准确率，有一部分”错误”其实是标注本身的歧义造成的——模型可能在某些样本上才是”正确”的。</p><p>而通过 loss 排序，我们可以快速发现这些可疑的标注。因为模型在数据中的规律（而非噪声）上收敛，当遇到一个违背规律的标注时，它的 loss 就会异常高。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># less loss -&gt; more confident</span></div></div><div><div><div>2</div></div><div><span><span>valid_df.</span><span>sort_values</span><span>(</span></span><span>'loss'</span><span>, </span><span>ascending</span><span>=</span><span>True</span><span><span>).</span><span>head</span><span>(</span></span><span>10</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                          </span></span><span>text                          label  pred_label  loss</span></div></div><div><div><div>2</div></div><div><span>1310  i feel like an ungrateful asshole                  sadness  sadness      0.004158</span></div></div><div><div><div>3</div></div><div><span>702      i only find out that they are looking and feel...  joy      joy          0.004176</span></div></div><div><div><div>4</div></div><div><span>21      i feel try to tell me im ungrateful tell me im...  sadness  sadness      0.004195</span></div></div><div><div><div>5</div></div><div><span>845      i already feel very glamorous have a great day...  joy      joy          0.004204</span></div></div><div><div><div>6</div></div><div><span>1601  i feel so ungrateful when thinking saying thes...  sadness  sadness      0.004205</span></div></div><div><div><div>7</div></div><div><span>1502  i feel ungrateful for stupid shit like              sadness  sadness      0.004215</span></div></div><div><div><div>8</div></div><div><span>212      i own the brushes are constantly used and i fe...  joy      joy          0.004218</span></div></div><div><div><div>9</div></div><div><span>578      i got to christmas feeling positive about the ...  joy      joy          0.004222</span></div></div><div><div><div>10</div></div><div><span>1466  i feel so ungrateful to be wishing this pregna...  sadness  sadness      0.004224</span></div></div><div><div><div>11</div></div><div><span>133      i and feel quite ungrateful for it but i m loo...  sadness  sadness      0.004237</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们再来看升序的情况。可以发现，模型对预测 joy 和 sadness 非常自信</p></section>
<section><h1>4. to huggingface hub<a href="#4-to-huggingface-hub"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 设置标签映射到模型 config 中</span></div></div><div><div><div>2</div></div><div><span><span>id2label </span><span>=</span><span> {</span></span><span>0</span><span>: </span><span>'sadness'</span><span>, </span><span>1</span><span>: </span><span>'joy'</span><span>, </span><span>2</span><span>: </span><span>'love'</span><span>, </span><span>3</span><span>: </span><span>'anger'</span><span>, </span><span>4</span><span>: </span><span>'fear'</span><span>, </span><span>5</span><span>: </span><span>'surprise'</span><span>}</span></div></div><div><div><div>3</div></div><div><span><span>label2id </span><span>=</span><span> {v: k </span></span><span>for</span><span> k, v </span><span>in</span><span><span> id2label.</span><span>items</span><span>()}</span></span></div></div><div><div><div>4</div></div><div><span><span>model.config.id2label </span><span>=</span><span> id2label</span></span></div></div><div><div><div>5</div></div><div><span><span>model.config.label2id </span><span>=</span><span> label2id</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 2. 推送 tokenizer（包含标签映射）</span></div></div><div><div><div>8</div></div><div><span><span>Hug_name </span><span>=</span><span> </span></span><span>""</span><span> </span><span># 替换为你的 Hugging Face Hub 用户名</span></div></div><div><div><div>9</div></div><div><span><span>tokenizer.</span><span>push_to_hub</span><span>(</span></span><span>f</span><span>"</span><span>{</span><span>Hug_name</span><span>}</span><span>/distilbert-base-uncased-finetuned-emotion-classification"</span><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># 3. 强制重新推送模型权重</span></div></div><div><div><div>12</div></div><div><span><span>trainer.</span><span>push_to_hub</span><span>(</span></span><span>commit_message</span><span>=</span><span>"Fix: add id2label and re-upload fine-tuned weights"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就是将微调好的模型推送到 HuggingFace Hub 上：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>本地训练好的模型<br /><br />distilbert-base-uncased-<br /><br />finetuned-emotion-<br /><br />classification</p></span></div><div><span><p>打包模型权重 + 配置</p></span></div><div><span><p>上传到 Hugging Face Hub</p></span></div><div><span><p>https://huggingface.co/<br /><br />你的用户名/<br /><br />distilbert-base-uncased-<br /><br />finetuned-emotion-<br /><br />classification</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>本地训练好的模型<br /><br />distilbert-base-uncased-<br /><br />finetuned-emotion-<br /><br />classification</p></span></div><div><span><p>打包模型权重 + 配置</p></span></div><div><span><p>上传到 Hugging Face Hub</p></span></div><div><span><p>https://huggingface.co/<br /><br />你的用户名/<br /><br />distilbert-base-uncased-<br /><br />finetuned-emotion-<br /><br />classification</p></span></div>
</div></div></div><p>我们上传的内容包括：</p><ul>
<li><code>pytorch_model.bin</code> — 模型权重（66M 参数的数值）</li>
<li><code>config.json</code> — 模型配置（6 分类、隐藏层维度等）</li>
<li><code>tokenizer.json</code> / <code>vocab.txt</code> — 分词器文件</li>
</ul><p>上传后，任何人都可以以类似下面的方式加载我们的模型：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> pipeline</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># 就像从官网加载 distilbert 一样</span></div></div><div><div><div>4</div></div><div><span><span>classifier </span><span>=</span><span> </span><span>pipeline</span><span>(</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>"text-classification"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>model</span><span>=</span><span>"你的用户名/distilbert-base-uncased-finetuned-emotion-classification"</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>classifier</span><span>(</span></span><span>"I feel so happy today!"</span><span>)</span></div></div><div><div><div>10</div></div><div><span># [{'label': 'joy', 'score': 0.99}]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们在下面给出了示例，加载你刚刚上传到 HuggingFace 的模型直接用于推理：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hide_output</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> pipeline</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 清除缓存后重新加载</span></div></div><div><div><div>5</div></div><div><span>import</span><span> shutil</span></div></div><div><div><div>6</div></div><div><span><span>cache_dir </span><span>=</span><span> </span></span><span>"C:</span><span>\\</span><span>Users</span><span>\\</span><span>HP</span><span>\\</span><span>.cache</span><span>\\</span><span>huggingface</span><span>\\</span><span>hub</span><span>\\</span><span>models--hac1224--distilbert-base-uncased-finetuned-emotion-classification"</span></div></div><div><div><div>7</div></div><div><span><span>shutil.</span><span>rmtree</span><span>(cache_dir, </span></span><span>ignore_errors</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 创建 classifier</span></div></div><div><div><div>10</div></div><div><span><span>classifier </span><span>=</span><span> </span><span>pipeline</span><span>(</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>"text-classification"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>model</span><span>=</span><span>"hac1224/distilbert-base-uncased-finetuned-emotion-classification"</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pipeline</code> 会自动帮你完成这些步骤：</p><ol>
<li>加载模型，相当于 <code>AutoModelForSequenceClassification.from_pretrained(...)</code>；</li>
<li>加载分词器，相当于 <code>AutoTokenizer.from_pretrained(...)</code>；</li>
<li>文本编码，相当于 <code>tokenizer(text, padding=True, truncation=True)</code>；</li>
<li>模型推理，相当于 <code>model(**inputs)</code>；</li>
<li>后处理，相当于 <code>softmax</code> -&gt; <code>argmax</code> -&gt; 映射标签名；</li>
</ol><p>通过 <code>classifier</code> 创建一个分类器后，我们之后就可以像这样使用了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>classifier</span><span>(</span></span><span>"I feel so happy today!"</span><span>)</span></div></div><div><div><div>2</div></div><div><span># 输出: [{'label': 'joy', 'score': 0.998}]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它会自动：</p><ol>
<li>从 Hub 下载模型权重和分词器</li>
<li>对输入文本进行分词</li>
<li>用模型推理得到 logits</li>
<li>通过 softmax 计算概率</li>
<li>返回 label（标签名）和 score（置信度）</li>
</ol><p>所以，<code>pipline</code> = 模型 + 分词器 + 预处理 + 后处理的”一键打包”，我们不需要手写任何预处理或后处理代码，输入文本直接出结果。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># custom_tweet = 'I saw a movie today and it was really good.'</span></div></div><div><div><div>2</div></div><div><span><span>custom_tweet </span><span>=</span><span> </span></span><span>'I saw a movie today and it suck.'</span></div></div><div><div><div>3</div></div><div><span><span>preds </span><span>=</span><span> </span><span>classifier</span><span>(custom_tweet, </span></span><span>top_k</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>4</div></div><div><span>preds</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为（<code>top_k=None</code> 返回全部 6 类的概率）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 第一句话的输出</span></div></div><div><div><div>2</div></div><div><span>[{'label': 'joy', 'score': 0.9890854954719543},</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>{'label': 'anger', 'score': 0.003720962442457676},</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>{'label': 'sadness', 'score': 0.002655301010236144},</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>{'label': 'love', 'score': 0.002034853445366025},</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>{'label': 'surprise', 'score': 0.0015041438164189458},</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>{'label': 'fear', 'score': 0.0009992503328248858}]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 第二句话的输出</span></div></div><div><div><div>10</div></div><div><span>[{'label': 'anger', 'score': 0.8032434582710266},</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>{'label': 'sadness', 'score': 0.09835370630025864},</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>{'label': 'fear', 'score': 0.04329225793480873},</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>{'label': 'joy', 'score': 0.040239084511995316},</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>{'label': 'love', 'score': 0.008198064751923084},</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>{'label': 'surprise', 'score': 0.006673524621874094}]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><blockquote><p><strong>注意</strong>：在 transformers 5.x 中，<code>return_all_scores=True</code> 已废弃，需用 <code>top_k=None</code> 代替。</p></blockquote><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>preds_df </span><span>=</span><span> pd.</span><span>DataFrame</span><span>(preds)</span></span></div></div><div><div><div>2</div></div><div><span><span>plt.</span><span>bar</span><span>(preds_df[</span></span><span>'label'</span><span>], </span><span>100</span><span><span> </span><span>*</span><span> preds_df[</span></span><span>'score'</span><span>], </span><span>color</span><span>=</span><span>'blue'</span><span>) </span><span># 这里之前是 labels</span></div></div><div><div><div>3</div></div><div><span><span>plt.</span><span>title</span><span>(</span></span><span>f</span><span>'"</span><span>{</span><span>custom_tweet</span><span>}</span><span>"'</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>plt.</span><span>ylabel</span><span>(</span></span><span>"Class probability (%)"</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>plt.</span><span>show</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出如下图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619235316852.png" alt="" /></p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260619235435793.png" alt="" /></p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-03/</id>
      <title type="text">skill 技巧</title>
      <published>2026-07-08T00:00:00.000Z</published>
      <updated>2026-07-08T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-03/"/>
      <summary type="text">介绍 Agent Skills 官方的最佳实践</summary>
      <content type="html"><![CDATA[<blockquote><p>如何编写范围明确且与任务相匹配的技能。</p></blockquote>
<section><h2>1. 从真正的专业知识出发<a href="#1-从真正的专业知识出发"><span>#</span></a></h2><p>技能创建中一个常见的误区是，让 LLM 在不提供特定领域上下文的情况下生成技能——完全依赖 LLM 的通用训练知识。这样得到的结果往往是模糊、泛泛的流程（例如 “handle errors appropriately”, “follow best practices for authentication”），而非具体的 API 模式、边缘情况和项目约定——而这些正是让技能变得有价值的东西。</p><p>有效的技能植根于真正的专业知识。关键在于将特定领域的上下文注入创建过程。</p><section><h3>1.1 从实际操作任务中提炼<a href="#11-从实际操作任务中提炼"><span>#</span></a></h3><p>在与代理的对话中完成一个真实任务，过程中提供上下文、纠正和偏好。然后，将可复用的模式提炼为技能。注意以下几点：</p><ul>
<li><strong>有效的步骤</strong>——通向成功的动作序列</li>
<li><strong>你做的纠正</strong>——你引导代理调整方向的地方（例如，“用 X 库而不是 Y 库”“检查边缘情况 Z”）</li>
<li><strong>输入/输出格式</strong>——数据输入和输出时的样子</li>
<li><strong>你提供的上下文</strong>——代理尚不知道的项目特定事实、约定或约束</li>
</ul></section><section><h3>1.2 从现有项目产物中综合<a href="#12-从现有项目产物中综合"><span>#</span></a></h3><p>当你拥有大量既有知识时，可以将其输入 LLM，并要求其综合出一个技能。从团队实际的事故报告和运维手册中综合出的数据管道技能，会比从一篇泛泛的 “data engineering best practices” 文章中综合出的技能表现更好，因为它捕捉了<em>你的</em>数据模式、故障模式和恢复流程。关键在于使用项目特定的材料，而非泛泛的参考资料。</p><p>好的源材料包括：</p><ul>
<li>内部文档、运维手册和风格指南</li>
<li>API 规范、模式（schema）和配置文件</li>
<li>代码评审意见和问题跟踪器（捕捉反复出现的问题和评审者的期望）</li>
<li>版本控制历史，尤其是补丁和修复（通过实际变更揭示模式）</li>
<li>真实世界的失败案例及其解决方案</li>
</ul></section></section>
<section><h2>2. 通过实际运行来改进<a href="#2-通过实际运行来改进"><span>#</span></a></h2><p>技能的第一稿通常需要改进。用真实任务运行技能，然后将结果——不仅仅是失败的结果——全部反馈到创建过程中。问自己：什么触发了误报？遗漏了什么？哪些可以删掉？</p><p>即使只做一次“执行-修改”的循环，也能明显提升质量，而复杂领域通常受益于多次循环。</p><div><div><div></div><div>Tip</div></div><div><p>阅读代理的执行轨迹，而不仅仅是最终输出。如果代理在无效步骤上浪费时间，常见原因包括：指令过于模糊（代理尝试多种方法才找到可行的一种）、指令不适用于当前任务（代理仍然照做）、或者提供了太多选项却没有明确的默认值。</p></div></div><p>如需更结构化的迭代方法，包括测试用例、断言和评分，请参阅<a href="/posts/skill-05/">skill 评估</a>。</p></section>
<section><h2>3. 明智地使用上下文<a href="#3-明智地使用上下文"><span>#</span></a></h2><p>一旦技能被激活，其完整的 <code>SKILL.md</code> 正文就会加载到代理的上下文窗口中，连同对话历史、系统上下文和其他激活的技能一起。技能中的每一个 token 都在与窗口中的其他内容争夺代理的注意力。</p><section><h3>3.1 增加代理缺乏的，省略代理已知的<a href="#31-增加代理缺乏的省略代理已知的"><span>#</span></a></h3><p>专注于代理 <em>没有</em> 你的技能就不会知道的内容：项目特定的约定、领域特定的流程、不明显的边缘情况，以及要使用的特定工具或 API。你不需要解释 PDF 是什么、HTTP 如何工作，或者数据库迁移是做什么的。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!-- 过于冗长——代理已经知道 PDF 是什么 --&gt;</span></div></div><div><div><div>2</div></div><div><span>## 提取 PDF 文本</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>PDF（便携式文档格式）文件是一种常见的文件格式，包含</span></div></div><div><div><div>5</div></div><div><span>文本、图像和其他内容。要从 PDF 中提取文本，你需要</span></div></div><div><div><div>6</div></div><div><span>使用一个库。推荐使用 pdfplumber，因为它能很好地处理大多数情况。</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>&lt;!-- 更好——直接切入代理靠自己不会知道的内容 --&gt;</span></div></div><div><div><div>9</div></div><div><span>## 提取 PDF 文本</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>使用 pdfplumber 进行文本提取。对于扫描文档，回退使用</span></div></div><div><div><div>12</div></div><div><span>pdf2image 搭配 pytesseract。</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>```python</span></div></div><div><div><div>15</div></div><div><span>import</span><span> pdfplumber</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>with</span><span><span> pdfplumber.</span><span>open</span><span>(</span></span><span>"file.pdf"</span><span>) </span><span>as</span><span> pdf:</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>text </span><span>=</span><span> pdf.pages[</span><span>0</span><span><span>].</span><span>extract_text</span><span>()</span></span></div></div><div><div><div>19</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>针对每部分内容问自己：“如果没有这条指令，代理会不会犯错？”如果答案是否定的，就删掉。如果不确定，就测试一下。如果代理不需要技能就能很好地完成整个任务，那么这个技能可能没有增加价值。关于如何系统性地测试，请参阅<a href="/posts/skill-05/">skill 评估</a>。</p></section><section><h3>3.2 设计内聚的单元<a href="#32-设计内聚的单元"><span>#</span></a></h3><p>决定一个技能应该涵盖什么，类似于决定一个函数应该做什么：你希望它封装一个内聚的工作单元，并能与其他技能良好组合。范围过窄的技能会迫使单个任务加载多个技能，带来开销和指令冲突的风险。范围过宽的技能则难以精确激活。一个用于查询数据库并格式化结果的技能可能是一个内聚的单元，而一个同时涵盖数据库管理的技能则可能试图做得太多。</p></section><section><h3>3.3 力求适度的详细程度<a href="#33-力求适度的详细程度"><span>#</span></a></h3><p>过于全面的技能弊大于利——代理难以提取相关部分，并可能因不适用于当前任务的指令而走上无效路径。简洁、分步的指导加上可运行的示例，往往比详尽的文档更有效。当你发现自己正在覆盖每一个边缘情况时，请考虑其中大多数是否更适合由代理自己的判断来处理。</p></section><section><h3>3.4 通过渐进式披露组织大型技能<a href="#34-通过渐进式披露组织大型技能"><span>#</span></a></h3><p><a href="/posts/skill-02/">agentskills.io 规范</a>建议将 <code>SKILL.md</code> 控制在 500 行和 5,000 个 token 以内——即代理每次运行所需的核心指令。当技能确实需要更多内容时，应将详细的参考材料移至 <code>references/</code> 或类似目录中的单独文件。</p><p>关键在于告诉代理 <em>何时</em> 加载每个文件。“如果 API 返回非 200 状态码，请阅读 <code>references/api-errors.md</code>”比泛泛的 “参见 references/ 中的详情” 更有用。这能让代理按需加载上下文，而不是预先加载，这正是 <a href="/posts/skill-02/">渐进式披露</a> 的设计初衷。</p></section></section>
<section><h2>4. 校准控制粒度<a href="#4-校准控制粒度"><span>#</span></a></h2><p>并非技能的每个部分都需要同样的指令强度。根据任务的脆弱性来匹配指令的具体程度。</p><section><h3>4.1 根据脆弱性匹配具体程度<a href="#41-根据脆弱性匹配具体程度"><span>#</span></a></h3><p><strong>给代理自由</strong>，当多种方法都有效且任务允许变化时。对于灵活的指令，解释  <em>原因</em> 可能比严格的指令更有效——理解指令背后目的的代理能做出更好的、依赖上下文的决策。代码审查技能可以描述要关注什么，而不必规定具体步骤：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 代码审查流程</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>1.</span><span> 检查所有数据库查询是否存在 SQL 注入（使用参数化查询）</span></span></div></div><div><div><div>4</div></div><div><span><span>2.</span><span> 验证每个端点上的身份验证检查</span></span></div></div><div><div><div>5</div></div><div><span><span>3.</span><span> 在并发代码路径中查找竞态条件</span></span></div></div><div><div><div>6</div></div><div><span><span>4.</span><span> 确认错误消息不会泄露内部细节</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>提供明确的指令</strong>，当操作脆弱、一致性重要，或必须遵循特定顺序时：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 数据库迁移</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>请精确运行以下命令序列：</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>```bash</span></div></div><div><div><div>6</div></div><div><span>python</span><span> </span><span>scripts/migrate.py</span><span> </span><span>--verify</span><span> </span><span>--backup</span></div></div><div><div><div>7</div></div><div><span>```</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>不要修改命令或添加其他标志。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>大多数技能都是混合的。请独立校准每个部分。</p></section><section><h3>4.2 提供默认值，而不是菜单<a href="#42-提供默认值而不是菜单"><span>#</span></a></h3><p>当多种工具或方法都可行时，选择一个默认值，并简要提及替代方案，而不是将它们作为同等选项呈现。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!-- 选项太多 --&gt;</span></div></div><div><div><div>2</div></div><div><span>你可以使用 pypdf、pdfplumber、PyMuPDF 或 pdf2image...</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>&lt;!-- 明确的默认值，附有备用方案 --&gt;</span></div></div><div><div><div>5</div></div><div><span>使用 pdfplumber 进行文本提取：</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>```python</span></div></div><div><div><div>8</div></div><div><span>import</span><span> pdfplumber</span></div></div><div><div><div>9</div></div><div><span>```</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>对于需要 OCR 的扫描 PDF，请使用 pdf2image 搭配 pytesseract 作为替代。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4.3 倾向于流程而非声明<a href="#43-倾向于流程而非声明"><span>#</span></a></h3><p>技能应该教会代理 <em>如何处理</em> 一类问题，而不是针对特定实例 <em>生成什么</em>。比较以下两者：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;!-- 特定答案——仅对此确切任务有用 --&gt;</span></div></div><div><div><div>2</div></div><div><span>将 </span><span><span>`</span><span>orders</span><span>`</span></span><span> 表与 </span><span><span>`</span><span>customers</span><span>`</span></span><span> 表按 </span><span><span>`</span><span>customer_id</span><span>`</span></span><span> 连接，筛选</span></div></div><div><div><div>3</div></div><div><span><span>`</span><span>region = 'EMEA'</span><span>`</span></span><span>，并对 </span><span><span>`</span><span>amount</span><span>`</span></span><span> 列求和。</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>&lt;!-- 可复用的方法——适用于任何分析查询 --&gt;</span></div></div><div><div><div>6</div></div><div><span><span>1.</span><span> 从 </span></span><span><span>`</span><span>references/schema.yaml</span><span>`</span></span><span> 读取 schema，查找相关表</span></div></div><div><div><div>7</div></div><div><span><span>2.</span><span> 使用 </span></span><span><span>`</span><span>_id</span><span>`</span></span><span> 外键约定连接表</span></div></div><div><div><div>8</div></div><div><span><span>3.</span><span> 根据用户请求中的筛选条件应用 WHERE 子句</span></span></div></div><div><div><div>9</div></div><div><span><span>4.</span><span> 根据需要聚合数值列，并格式化为 markdown 表格</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这并不意味着技能不能包含具体细节——输出格式模板、约束条件如 “绝不输出 PII” 以及特定工具指令都很有价值。关键在于，即使个别细节是具体的，<em>方法</em> 本身应该具有通用性。</p></section></section>
<section><h2>5. 有效指令的模式<a href="#5-有效指令的模式"><span>#</span></a></h2><p>这些是组织技能内容的可复用技巧。并非每个技能都需要全部——请使用适合你任务的那些。</p><section><h3>5.1 易错点（Gotchas）部分<a href="#51-易错点gotchas部分"><span>#</span></a></h3><p>许多技能中价值最高的内容是一份易错点列表——那些违背合理假设的环境特定事实。这些不是泛泛的建议（“妥善处理错误”），而是具体的纠正，让代理在未被明确告知时避免犯错：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 易错点</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>-</span><span> </span></span><span><span>`</span><span>users</span><span>`</span></span><span> 表使用软删除。查询必须包含</span></div></div><div><div><div>4</div></div><div><span>  </span><span><span>`</span><span>WHERE deleted_at IS NULL</span><span>`</span></span><span>，否则结果会包含已停用的账户。</span></div></div><div><div><div>5</div></div><div><span><span>-</span><span> 用户 ID 在数据库中是 </span></span><span><span>`</span><span>user_id</span><span>`</span></span><span>，在认证服务中是 </span><span><span>`</span><span>uid</span><span>`</span></span><span>，</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>在计费 API 中是 </span><span><span>`</span><span>accountId</span><span>`</span></span><span>。这三个值指向同一个实体。</span></div></div><div><div><div>7</div></div><div><span><span>-</span><span> </span></span><span><span>`</span><span>/health</span><span>`</span></span><span> 端点只要 Web 服务器在运行就返回 200，</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>即使数据库连接已断开。请使用 </span><span><span>`</span><span>/ready</span><span>`</span></span><span> 来检查完整的服务健康状态。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>请将易错点放在 <code>SKILL.md</code> 中，以便代理在遇到情况前读到它们。如果使用单独的参考文件，则需要告诉代理何时加载，但对于不明显的问题，代理可能无法识别触发条件。</p><div><div><div></div><div>Tip</div></div><div><p>当代理犯错而你不得不纠正时，请将纠正内容添加到易错点部分。这是迭代改进技能最直接的方法之一。</p></div></div></section><section><h3>5.2 输出格式模板<a href="#52-输出格式模板"><span>#</span></a></h3><p>当你需要代理以特定格式生成输出时，请提供一个模板。这比用文字描述格式更可靠，因为代理对具体结构有很好的模式匹配能力。简短的模板可以直接放在 <code>SKILL.md</code> 中；对于较长的模板，或仅在特定情况下需要的模板，请将它们存储在 <code>assets/</code> 中，并在 <code>SKILL.md</code> 中引用，以便仅在需要时加载。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 报告结构</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>使用此模板，并根据具体分析调整各部分：</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>```markdown</span></div></div><div><div><div>6</div></div><div><span># </span><span>[</span><span>分析标题</span><span>]</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>## 执行摘要</span></div></div><div><div><div>9</div></div><div><span>[</span><span>关键发现的一段落概述</span><span>]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>## 主要发现</span></div></div><div><div><div>12</div></div><div><span><span>-</span><span> 发现 1，附带支持数据</span></span></div></div><div><div><div>13</div></div><div><span><span>-</span><span> 发现 2，附带支持数据</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>## 建议</span></div></div><div><div><div>16</div></div><div><span><span>1.</span><span> 具体可操作的建议</span></span></div></div><div><div><div>17</div></div><div><span><span>2.</span><span> 具体可操作的建议</span></span></div></div><div><div><div>18</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>5.3 多步骤工作流的检查清单<a href="#53-多步骤工作流的检查清单"><span>#</span></a></h3><p>明确的检查清单有助于代理跟踪进度并避免跳过步骤，尤其是在步骤之间存在依赖或验证门槛时。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 表单处理工作流</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>进度：</span></div></div><div><div><div>4</div></div><div><span><span>-</span><span> [ ] 步骤 1：分析表单（运行 </span></span><span><span>`</span><span>scripts/analyze_form.py</span><span>`</span></span><span>）</span></div></div><div><div><div>5</div></div><div><span><span>-</span><span> [ ] 步骤 2：创建字段映射（编辑 </span></span><span><span>`</span><span>fields.json</span><span>`</span></span><span>）</span></div></div><div><div><div>6</div></div><div><span><span>-</span><span> [ ] 步骤 3：验证映射（运行 </span></span><span><span>`</span><span>scripts/validate_fields.py</span><span>`</span></span><span>）</span></div></div><div><div><div>7</div></div><div><span><span>-</span><span> [ ] 步骤 4：填写表单（运行 </span></span><span><span>`</span><span>scripts/fill_form.py</span><span>`</span></span><span>）</span></div></div><div><div><div>8</div></div><div><span><span>-</span><span> [ ] 步骤 5：验证输出（运行 </span></span><span><span>`</span><span>scripts/verify_output.py</span><span>`</span></span><span>）</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>5.4 验证循环<a href="#54-验证循环"><span>#</span></a></h3><p>指示代理在继续之前验证自己的工作。模式是：执行工作、运行验证器（脚本、参考检查清单或自检）、修复问题、重复直到验证通过。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 编辑工作流</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>1.</span><span> 进行编辑</span></span></div></div><div><div><div>4</div></div><div><span><span>2.</span><span> 运行验证：</span></span><span><span>`</span><span>python scripts/validate.py output/</span><span>`</span></span></div></div><div><div><div>5</div></div><div><span><span>3.</span><span> 如果验证失败：</span></span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>-</span><span> 查看错误消息</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>-</span><span> 修复问题</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>-</span><span> 再次运行验证</span></div></div><div><div><div>9</div></div><div><span><span>4.</span><span> 仅在验证通过后才继续</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>参考文档也可以充当“验证器”——指示代理在最终确定之前，根据参考文档检查其工作。</p></section><section><h3>5.5 计划-验证-执行<a href="#55-计划-验证-执行"><span>#</span></a></h3><p>对于批量操作或破坏性操作，让代理以结构化格式创建中间计划，根据事实来源进行验证，然后才执行。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## PDF 表单填写</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>1.</span><span> 提取表单字段：</span></span><span><span>`</span><span>python scripts/analyze_form.py input.pdf</span><span>`</span></span><span> → </span><span><span>`</span><span>form_fields.json</span><span>`</span></span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>（列出每个字段名称、类型以及是否为必填）</span></div></div><div><div><div>5</div></div><div><span><span>2.</span><span> 创建 </span></span><span><span>`</span><span>field_values.json</span><span>`</span></span><span>，将每个字段名称映射到其预期值</span></div></div><div><div><div>6</div></div><div><span><span>3.</span><span> 验证：</span></span><span><span>`</span><span>python scripts/validate_fields.py form_fields.json field_values.json</span><span>`</span></span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>（检查每个字段名称在表单中是否存在、类型是否兼容、必填字段是否缺失）</span></div></div><div><div><div>8</div></div><div><span><span>4.</span><span> 如果验证失败，修改 </span></span><span><span>`</span><span>field_values.json</span><span>`</span></span><span> 并重新验证</span></div></div><div><div><div>9</div></div><div><span><span>5.</span><span> 填写表单：</span></span><span><span>`</span><span>python scripts/fill_form.py input.pdf field_values.json output.pdf</span><span>`</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键要素是步骤 3：一个验证脚本，用于检查计划（<code>field_values.json</code>）是否符合事实来源（<code>form_fields.json</code>）。像“未找到字段 ‘signature_date’——可用字段：customer_name, order_total, signature_date_signed”这样的错误信息，为代理提供了足够的信息来自我修正。</p></section><section><h3>5.6 捆绑可复用脚本<a href="#56-捆绑可复用脚本"><span>#</span></a></h3><p>当 <a href="/posts/skill-05/">迭代技能</a> 时，比较代理在不同测试用例中的执行轨迹。如果你注意到代理每次运行都在独立地重复发明相同的逻辑——构建图表、解析特定格式、验证输出——那就说明应该编写一次经过测试的脚本，并将其捆绑在 <code>scripts/</code> 中。</p><p>关于设计和捆绑脚本的更多信息，请参阅 <a href="/posts/skill-06/">skill 脚本</a> 。</p></section></section>
<section><h2>6. 后续步骤<a href="#6-后续步骤"><span>#</span></a></h2><p>当你有了一个可以运行的 skill 后，你可以参考这些优化你的 skill：</p><ul>
<li><a href="/posts/skill-05/">skill 评估</a></li>
<li><a href="/posts/skill-04/">优化 skill 描述</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-02/</id>
      <title type="text">agentskills.io 规范</title>
      <published>2026-07-07T00:00:00.000Z</published>
      <updated>2026-07-07T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-02/"/>
      <summary type="text">介绍 agentskills.io 的 skill 规范</summary>
      <content type="html"><![CDATA[<section><h1>1. <code>SKILL.md</code> 格式<a href="#1-skillmd-格式"><span>#</span></a></h1><p>如上一节 <a href="/posts/skill-01/">skill 入门</a> 所说，<code>SKILL.md</code> 的开头包含一段元数据，关于元数据字段的完整指南如下：</p>

<table><thead><tr><th>字段</th><th>是否必须</th><th>要求</th></tr></thead><tbody><tr><td><code>name</code></td><td>Yes</td><td>最多 64 个字符，只允许：小写字母、数字和连字符；不能以连字符开头或结尾</td></tr><tr><td><code>description</code></td><td>Yes</td><td>最多 1024 个字符；非空；描述这个 skill 是做什么的，以及何时调用</td></tr><tr><td><code>license</code></td><td>No</td><td>列出许可证名或捆绑的许可证文件</td></tr><tr><td><code>compatiability</code></td><td>No</td><td>最多 500 字符；声明这个 skill 运行时需要什么环境</td></tr><tr><td><code>metadata</code></td><td>No</td><td>关于附加元数据的任意值映射</td></tr><tr><td><code>allowed-tools</code></td><td>No</td><td>用空格分隔的字符串；描述允许该 skill 使用的工具</td></tr></tbody></table><p>下面是一个 skill 的最小示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>name</span><span>: </span><span>skill-name</span></div></div><div><div><div>3</div></div><div><span>description</span><span>: </span><span>A description of what this skill does and when to use it.</span></div></div><div><div><div>4</div></div><div><span>---</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面是关于可选字段的一个示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>name</span><span>: </span><span>pdf-processing</span></div></div><div><div><div>3</div></div><div><span>description</span><span>: </span><span>Extract PDF text, fill forms, merge files. Use when handling PDFs.</span></div></div><div><div><div>4</div></div><div><span>license</span><span>: </span><span>Apache-2.0</span></div></div><div><div><div>5</div></div><div><span>metadata</span><span>:</span></div></div><div><div><div>6</div></div><div><span>  </span><span>author</span><span>: </span><span>example-org</span></div></div><div><div><div>7</div></div><div><span>  </span><span>version</span><span>: </span><span>"1.0"</span></div></div><div><div><div>8</div></div><div><span>---</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>1.1 字段的详细要求<a href="#11-字段的详细要求"><span>#</span></a></h2><p><code>name</code> 字段：</p><ul>
<li>必须为 1-64 个字符；</li>
<li>只能包含：<code>a-z</code>, <code>0-9</code>, <code>-</code>；</li>
<li>不能以 <code>-</code> 开头或结尾；</li>
<li>不能包含连续的连字符 <code>--</code>；</li>
<li>必须与 parent directory name 保持一致。</li>
</ul><p><code>description</code> 字段：</p><ul>
<li>必须为 1-1024 个字符；</li>
<li>应该同时描述：
<ul>
<li>这个 skill 是做什么的；</li>
<li>什么时候该调用它；</li>
</ul>
</li>
<li>应该包含特定的关键词，帮助 Agent 识别相关的任务。</li>
</ul><p><code>license</code> 字段示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>license</span><span>: </span><span>Proprietary. LICENSE.txt has complete terms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>compatiability</code> 字段：</p><ul>
<li>大多数 skill 不需要该字段，仅当 skill 有特定环境要求的时候才应包含。</li>
</ul><p><code>metadata</code> 字段：</p><ul>
<li>包含从字符串键到字符串值的映射；</li>
<li>客户端可以用该字段存储 <a href="https://agentskills.io/" target="_blank">agentskills.io</a> 规范未定义的其他属性；</li>
<li>建议使用相对唯一的键名，以免冲突。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>metadata</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>author</span><span>: </span><span>example-org</span></div></div><div><div><div>3</div></div><div><span>  </span><span>version</span><span>: </span><span>"1.0"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>allowed-tools</code> 字段：</p><ul>
<li>以空格分隔的字符串，列出预先批准可运行的工具；</li>
<li>实验性功能，不同 Agent 对该字段的支持可能不同。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>allowed-tools</span><span>: </span><span>Bash(git:*) Bash(jq:*) Read</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>1.2 正文内容<a href="#12-正文内容"><span>#</span></a></h2><p>前置元数据之后的 Markdown 正文包含技能指令。没有格式限制。编写任何有助于代理有效执行任务的内容。</p><p>建议包含的部分：</p><ul>
<li>分步说明</li>
<li>输入和输出示例</li>
<li>常见边缘情况</li>
</ul><p>请注意，一旦代理决定激活技能，它将加载整个 <code>SKILL.md</code> 文件。如果内容较长，建议将部分内容拆分到引用的文件中。</p></section></section>
<section><h1>2. 可选目录<a href="#2-可选目录"><span>#</span></a></h1><section><h2>2.1 <code>scripts/</code><a href="#21-scripts"><span>#</span></a></h2><p>包含代理可以运行的可执行代码。脚本应：</p><ul>
<li>自包含或清楚说明依赖项</li>
<li>包含有用的错误信息</li>
<li>优雅地处理边缘情况</li>
</ul><p>支持的语言取决于代理实现。常见选项包括 Python、Bash 和 JavaScript。</p></section><section><h2>2.2 <code>references/</code><a href="#22-references"><span>#</span></a></h2><p>包含代理在需要时可读取的附加文档：</p><ul>
<li><code>REFERENCE.md</code> - 详细技术参考</li>
<li><code>FORMS.md</code> - 表单模板或结构化数据格式</li>
<li>领域特定文件（<code>finance.md</code>、<code>legal.md</code> 等）</li>
</ul><p>保持每个参考文件内容聚焦。代理按需加载这些文件，文件越小，占用的上下文就越少。</p></section><section><h2>2.3 <code>assets/</code><a href="#23-assets"><span>#</span></a></h2><p>包含静态资源：</p><ul>
<li>模板（文档模板、配置模板）</li>
<li>图片（图表、示例）</li>
<li>数据文件（查找表、模式）</li>
</ul></section></section>
<section><h1>3. 渐进式披露<a href="#3-渐进式披露"><span>#</span></a></h1><p>代理<em>逐步</em>加载技能，仅在任务需要时才拉取更多细节。技能结构应充分利用这一点：</p><ol>
<li><strong>元数据</strong>（约 100 个 token）：所有技能的 <code>name</code> 和 <code>description</code> 字段在启动时加载</li>
<li><strong>指令</strong>（建议 &lt; 5000 个 token）：技能激活时加载完整的 <code>SKILL.md</code> 正文</li>
<li><strong>资源</strong>（按需）：仅在需要时加载文件（例如 <code>scripts/</code>、<code>references/</code> 或 <code>assets/</code> 中的文件）</li>
</ol><p>建议将主 <code>SKILL.md</code> 控制在 500 行以内。将详细的参考材料移到单独的文件中。</p><section><h2>3.1 文件引用<a href="#31-文件引用"><span>#</span></a></h2><p>在技能中引用其他文件时，请使用相对于技能根目录的路径：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>See </span><span>[</span><span>the reference guide</span><span>]</span><span><span>(</span><span>references/REFERENCE.md</span><span>)</span></span><span> for details.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>Run the extraction script:</span></div></div><div><div><div>4</div></div><div><span>scripts/extract.py</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>请保持从 <code>SKILL.md</code> 出发的文件引用深度不超过一层。避免深层嵌套的引用链。</p></section><section><h2>3.2 验证<a href="#32-验证"><span>#</span></a></h2><p>使用 <a href="https://github.com/agentskills/agentskills/tree/main/skills-ref" target="_blank">skills-ref</a> 参考库来验证你的技能：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>skills-ref</span><span> </span><span>validate</span><span> </span><span>./my-skill</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>该命令会检查你的 <code>SKILL.md</code> 前置元数据是否有效，并遵循所有命名约定。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-12/</id>
      <title type="text">12_masked_LM</title>
      <published>2026-07-06T00:00:00.000Z</published>
      <updated>2026-07-06T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-12/"/>
      <summary type="text">动手写 BERT 系列笔记第十二节（终章）</summary>
      <content type="html"><![CDATA[<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>from</span><span> transformers.models.bert </span><span>import</span><span> BertModel, BertTokenizer, BertForMaskedLM</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>1. model load and data processing<a href="#1-model-load-and-data-processing"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_type </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_type)</span></span></div></div><div><div><div>2</div></div><div><span><span>bert </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_type)</span></span></div></div><div><div><div>3</div></div><div><span><span>mlm </span><span>=</span><span> BertForMaskedLM.</span><span>from_pretrained</span><span>(model_type, </span></span><span>output_hidden_states</span><span>=</span><span>True</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bert</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>mlm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># BertModel 的结构</span></div></div><div><div><div>2</div></div><div><span>BertModel(</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>14</div></div><div><span><span>          </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>24</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>27</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>28</div></div><div><span><span>          </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>(output): BertOutput(</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>33</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>42</div></div><div><span>)</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span># BertForMaskedLM 的结构</span></div></div><div><div><div>45</div></div><div><span>BertForMaskedLM(</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>(bert): BertModel(</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>48</div></div><div><span><span>      </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>49</div></div><div><span><span>      </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>50</div></div><div><span><span>      </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>51</div></div><div><span><span>      </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>52</div></div><div><span><span>      </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>55</div></div><div><span><span>      </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>57</div></div><div><span><span>          </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>58</div></div><div><span><span>            </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>59</div></div><div><span><span>              </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>60</div></div><div><span><span>              </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>61</div></div><div><span><span>              </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>62</div></div><div><span><span>              </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>63</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>64</div></div><div><span><span>            </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>65</div></div><div><span><span>              </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>66</div></div><div><span><span>              </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>67</div></div><div><span><span>              </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>69</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>70</div></div><div><span><span>          </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>71</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>72</div></div><div><span><span>            </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>73</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>74</div></div><div><span><span>          </span></span><span>(output): BertOutput(</span></div></div><div><div><div>75</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>76</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>77</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>78</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>80</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>(cls): BertOnlyMLMHead(</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>(predictions): BertLMPredictionHead(</span></div></div><div><div><div>85</div></div><div><span><span>      </span></span><span>(transform): BertPredictionHeadTransform(</span></div></div><div><div><div>86</div></div><div><span><span>        </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>87</div></div><div><span><span>        </span></span><span>(transform_act_fn): GELUActivation()</span></div></div><div><div><div>88</div></div><div><span><span>        </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>89</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>90</div></div><div><span><span>      </span></span><span>(decoder): Linear(in_features=768, out_features=30522, bias=True)</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>92</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>93</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们从输出中可以看出，实际上 <code>BertModel</code> 与 <code>BertForMaskedLM</code> 只是在最后结尾的部分结构有所不同：</p><div><div><div><div><span><p>BertForMaskedLM</p></span></div><div><span></span></div><div><span><p>bert: BertModel</p></span></div><div><span><p>cls: BertOnlyMLMHead</p></span></div><div><span><p>BertModel</p></span></div><div><span></span></div><div><span></span></div><div><span><p>Embeddings</p></span></div><div><span><p>Encoder (12层)</p></span></div><div><span><p>Pooler</p></span></div>
</div><div><div><span><p>BertForMaskedLM</p></span></div><div><span></span></div><div><span><p>bert: BertModel</p></span></div><div><span><p>cls: BertOnlyMLMHead</p></span></div><div><span><p>BertModel</p></span></div><div><span></span></div><div><span></span></div><div><span><p>Embeddings</p></span></div><div><span><p>Encoder (12层)</p></span></div><div><span><p>Pooler</p></span></div>
</div></div></div>

<table><thead><tr><th>组件</th><th>BertModel</th><th>BertForMaskedLM</th></tr></thead><tbody><tr><td><code>embeddings</code></td><td>✅ 有</td><td>✅ 有（在 <code>.bert</code> 里）</td></tr><tr><td><code>encoder</code> (12层)</td><td>✅ 有</td><td>✅ 有（在 <code>.bert</code> 里）</td></tr><tr><td><code>pooler</code></td><td>✅ 有</td><td>✅ 有（在 <code>.bert</code> 里）</td></tr><tr><td><code>cls</code> (MLM Head)</td><td>❌ 没有</td><td>✅ <strong>独有的</strong></td></tr></tbody></table><hr /><p>这是 <code>BertForMaskedLM</code> 比 <code>BertModel</code> 多出来的唯一模块：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(cls): BertOnlyMLMHead(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>(predictions): BertLMPredictionHead(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>(transform): BertPredictionHeadTransform(</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>(dense): Linear(768 → 768)</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>(transform_act_fn): GELU</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>(LayerNorm)</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>(decoder): Linear(768 → 30522)  ← 关键！</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>10</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它做了两件事：</p><ol>
<li><strong>transform</strong>：把 encoder 输出的 768 维隐藏向量，通过一个全连接 + GELU 激活 + LayerNorm 做一次非线性变换</li>
<li><strong>decoder</strong>：把 768 维映射到 <strong>30522 维</strong>（= BERT 词表大小），每个位置对应一个词的预测分数</li>
</ol><hr /><section><h2>1.1 为什么会有这些区别？<a href="#11-为什么会有这些区别"><span>#</span></a></h2><section><h3>1.1.1 分工不同<a href="#111-分工不同"><span>#</span></a></h3><ul>
<li><strong>BertModel</strong> 是”通用编码器”——输入文本 → 输出每个 token 的上下文向量。至于拿到向量后做什么，它不管。</li>
<li><strong>BertForMaskedLM</strong> 是”专用任务模型”——在 BertModel 的基础上，加了一个预测头，专门做<strong>完形填空</strong>：输入 <code>"The [MASK] is blue"</code> → 预测 <code>[MASK]</code> 是 <code>"sky"</code>。</li>
</ul></section><section><h3>1.1.2 设计模式：骨干网络 + 任务头<a href="#112-设计模式骨干网络--任务头"><span>#</span></a></h3><p>这是 HuggingFace Transformers 的统一设计模式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertForMaskedLM = BertModel (骨干) + MLM Head (任务头)</span></div></div><div><div><div>2</div></div><div><span>BertForSequenceClassification = BertModel (骨干) + 分类头</span></div></div><div><div><div>3</div></div><div><span>BertForQuestionAnswering = BertModel (骨干) + QA头</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个 <code>BertModel</code> 被不同任务模型<strong>复用</strong>，只需换不同的”头”即可。</p></section><section><h3>1.1.3 为什么 decoder 是 768 → 30522？<a href="#113-为什么-decoder-是-768--30522"><span>#</span></a></h3><p>BERT 的词表大小是 30522，所以 MLM 做的是一个 <strong>30522 分类问题</strong>——对每个被 mask 的位置，从 30522 个词中选出最可能的那个。</p></section><section><h3>1.1.4 Pooler 虽然在，但 MLM 不用它<a href="#114-pooler-虽然在但-mlm-不用它"><span>#</span></a></h3><p><code>BertForMaskedLM</code> 内部的 <code>BertModel</code> 仍然包含 <code>pooler</code>，这是因为 HuggingFace 直接复用了整个 <code>BertModel</code> 类。但 MLM 任务实际上<strong>只用 encoder 最后一层的隐藏状态</strong>，pooler 在这里是”冗余但无害”的。</p><hr /><p>一句话总结：</p><blockquote><p><strong>BertModel 只负责”理解”，BertForMaskedLM 在理解的基础上多了一步”预测被遮住的词”。</strong></p></blockquote><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>text </span><span>=</span><span> (</span></span><span>"After Abraham Lincoln won the November 1860 presidential "</span></div></div><div><div><div>2</div></div><div><span>        </span><span>"election on an anti-slavery platform, an initial seven "</span></div></div><div><div><div>3</div></div><div><span>        </span><span>"slave states declared their secession from the country "</span></div></div><div><div><div>4</div></div><div><span>        </span><span>"to form the Confederacy. War broke out in April 1861 "</span></div></div><div><div><div>5</div></div><div><span>        </span><span>"when secessionist forces attacked Fort Sumter in South "</span></div></div><div><div><div>6</div></div><div><span>        </span><span>"Carolina, just over a month after Lincoln's "</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"inauguration."</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>text</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div><div><div><div>2</div></div><div><span>inputs[</span><span>'input_ids'</span><span>].shape </span><span># torch.Size([1, 62])</span></div></div><div><div><div>3</div></div><div><span>inputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[  101,  2044,  8181,  5367,  2180,  1996,  2281,  7313,  4883,  2602,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>2006,  2019,  3424,  1011,  8864,  4132,  1010,  2019,  3988,  2698,</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>6658,  2163,  4161,  2037, 22965,  2013,  1996,  2406,  2000,  2433,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>1996, 18179,  1012,  2162,  3631,  2041,  1999,  2258,  6863,  2043,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>22965,  2923,  2749,  4457,  3481,  7680,  3334,  1999,  2148,  3792,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>1010,  2074,  2058,  1037,  3204,  2044,  5367,  1005,  1055, 17331,</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>1012,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 <code>inputs['input_ids']</code> 的形状为 (1, 64)，它代表的是 <code>(batch_size, sequence_length)</code>. 注意这里的 62 个 token 包含了开头自动加的 <code>[CLS]</code> 和结尾的 <code>[SEP]</code>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>' '</span><span><span>.</span><span>join</span><span>(tokenizer.</span><span>convert_ids_to_tokens</span><span>(inputs[</span></span><span>'input_ids'</span><span>][</span><span>0</span><span>]))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"[CLS] after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession ##ist forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . [SEP]"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section></section>
<section><h1>2. masking<a href="#2-masking"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inputs[</span><span>'labels'</span><span><span>] </span><span>=</span><span> inputs[</span></span><span>'input_ids'</span><span><span>].</span><span>detach</span><span>().</span><span>clone</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这行代码是我们在 MLM（Masked Language Model）任务中<strong>准备标签数据</strong>的一步：</p><ul>
<li><code>.detach()</code>：<strong>切断计算图链接</strong>，防止梯度传播。
<ul>
<li><code>input_ids</code> 本身是模型输入的整数张量，按理说是”数据”不需要梯度</li>
<li>但 <code>.detach()</code> 是 PyTorch 的安全习惯，明确告诉框架：“这个张量只是作为标签使用，不需要反向传播”</li>
</ul>
</li>
<li><code>.clone()</code>：<strong>创建一个独立的副本</strong>。
<ul>
<li>如果不 <code>.clone()</code>，新变量和原变量会<strong>共享同一块内存</strong>。之后我们会在 <code>input_ids</code> 上随机把某些 token 换成 <code>[MASK]</code>（变成模型的输入），但 <code>labels</code> 需要保留<strong>原始未 mask 的值</strong>作为正确答案，所以必须复制一份。</li>
</ul>
</li>
<li><code>inputs['labels']</code>：这就是 MLM 的”<strong>标准答案</strong>”。</li>
</ul><p>整体流程如下图：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>原始句子</p></span></div><div><span><p>input_ids=[CLS] after ... [SEP]</p></span></div><div><span><p>labels=[CLS] after ... [SEP] （备份）</p></span></div><div><span><p>把部分 token 替换为 [MASK]</p></span></div><div><span><p>模型预测 masked 位置</p></span></div><div><span><p>与 labels 对比算损失</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>原始句子</p></span></div><div><span><p>input_ids=[CLS] after ... [SEP]</p></span></div><div><span><p>labels=[CLS] after ... [SEP] （备份）</p></span></div><div><span><p>把部分 token 替换为 [MASK]</p></span></div><div><span><p>模型预测 masked 位置</p></span></div><div><span><p>与 labels 对比算损失</p></span></div>
</div></div></div><p>我们总体的步骤将是：</p><ol>
<li>先把原始 <code>input_ids</code> <strong>备份</strong>一份到 <code>labels</code></li>
<li>然后把 <code>input_ids</code> 中的部分 token 替换成 <code>[MASK]</code></li>
<li>模型预测 <code>[MASK]</code> 位置应该是什么词</li>
<li>拿预测结果和 <code>labels</code> 里的<strong>原始词</strong>对比，计算损失</li>
</ol><p>所以这行代码的核心作用就是：<strong>在把句子弄乱之前，先保存一份”正确答案”。</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inputs[</span><span>'labels'</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[  101,  2044,  8181,  5367,  2180,  1996,  2281,  7313,  4883,  2602,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>2006,  2019,  3424,  1011,  8864,  4132,  1010,  2019,  3988,  2698,</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>6658,  2163,  4161,  2037, 22965,  2013,  1996,  2406,  2000,  2433,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>1996, 18179,  1012,  2162,  3631,  2041,  1999,  2258,  6863,  2043,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>22965,  2923,  2749,  4457,  3481,  7680,  3334,  1999,  2148,  3792,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>1010,  2074,  2058,  1037,  3204,  2044,  5367,  1005,  1055, 17331,</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>1012,   102]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mask </span><span>=</span><span> torch.</span><span>rand</span><span>(inputs[</span></span><span>'input_ids'</span><span><span>].shape) </span><span>&lt;</span><span> </span></span><span>0.15</span></div></div><div><div><div>2</div></div><div><span>mask</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[False, False,  True, False, False,  True,  True, False, False, False,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>False, False, False, False, False, False, False, False, False, False,</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>True, False, False,  True, False, False, False, False, False, False,</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>False, False, False, False,  True, False, False, False, False, False,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>False,  True, False, False, False,  True, False, False, False, False,</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>False, False, False, False, False, False, False, False, False, False,</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>False, False]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>torch.rand(inputs['input_ids'].shape)</code>：生成一个形状和 <code>input_ids</code> 相同的<strong>随机数矩阵</strong>，每个值在 <code>[0, 1)</code> 之间均匀分布。</li>
</ul><p>因为 <code>input_ids</code> 的形状是 <code>(1, 62)</code>，所以生成的就是 <code>1 行 × 62 列</code> 的随机小数，例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[0.73, 0.02, 0.91, 0.14, ..., 0.88]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>&lt; 0.15</code>：<strong>阈值判断</strong>——将每个随机数与 0.15 比较，小于 0.15 的位置为 <code>True</code>，否则为 <code>False</code>。</li>
</ul><p>结果就是一个<strong>布尔矩阵（Boolean mask）</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[ True, False, False, ..., False]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>True</code>  ≈ 这个位置要被遮住（概率 ≈ 15%）</li>
<li><code>False</code> ≈ 这个位置保持不变（概率 ≈ 85%）</li>
</ul><hr /><p>为什么是 15%？</p><p>这是 BERT 原始论文中的设定——<strong>随机选择 15% 的 token 进行掩码</strong>。</p><p>从 notebook 的输出可以验证，总共 62 个 token，被选中的数量约为 <code>62 × 15% ≈ 9.3</code>，实际输出正好是 14 个（随机波动正常）：</p><hr /><p>完整步骤回顾：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>input_ids: 原始句子</p></span></div><div><span><p>mask: 随机选 15% 位置</p></span></div><div><span><p>被选中的位置 → [MASK]</p></span></div><div><span><p>模型预测这些位置</p></span></div><div><span><p>与 labels 对比算损失</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>input_ids: 原始句子</p></span></div><div><span><p>mask: 随机选 15% 位置</p></span></div><div><span><p>被选中的位置 → [MASK]</p></span></div><div><span><p>模型预测这些位置</p></span></div><div><span><p>与 labels 对比算损失</p></span></div>
</div></div></div><p>这行代码就是上图中的第二步——<strong>决定把哪些词藏起来让模型猜</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sum</span><span>(mask[</span><span>0</span><span>]) </span><span># tensor(8)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mask_arr </span><span>=</span><span> (torch.</span><span>rand</span><span>(inputs[</span></span><span>'input_ids'</span><span><span>].shape) </span><span>&lt;</span><span> </span></span><span>0.15</span><span>) \</span></div></div><div><div><div>2</div></div><div><span><span>            </span></span><span>*</span><span> (inputs[</span><span>'input_ids'</span><span><span>] </span><span>!=</span><span> </span></span><span>101</span><span>) \</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>*</span><span> (inputs[</span><span>'input_ids'</span><span><span>] </span><span>!=</span><span> </span></span><span>102</span><span>)</span></div></div><div><div><div>4</div></div><div><span>mask_arr</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[False, False, False, False, False, False, False, False, False, False,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>False, False,  True, False, False, False, False, False, False,  True,</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>False, False, False, False, False,  True, False, False, False, False,</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>False, False, False, False, False, False, False, False, False, False,</span></div></div><div><div><div>5</div></div><div><span><span>          </span></span><span>True, False, False, False, False, False, False, False,  True, False,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>True, False, False,  True, False, False,  True, False,  True, False,</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>False, False]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这段代码相比上一个 <code>mask</code> 加了两个额外条件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mask_arr </span><span>=</span><span> (torch.</span><span>rand</span><span>(inputs[</span></span><span>'input_ids'</span><span><span>].shape) </span><span>&lt;</span><span> </span></span><span>0.15</span><span>)   </span><span># ① 随机选 15%</span></div></div><div><div><div>2</div></div><div><span><span>            </span></span><span>*</span><span> (inputs[</span><span>'input_ids'</span><span><span>] </span><span>!=</span><span> </span></span><span>101</span><span>)                    </span><span># ② 排除 [CLS]</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>*</span><span> (inputs[</span><span>'input_ids'</span><span><span>] </span><span>!=</span><span> </span></span><span>102</span><span>)                    </span><span># ③ 排除 [SEP]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>torch.rand(...) &lt; 0.15</code>：随机选 15%，和之前一样，随机选择 15% 的 token 位置标记为 <code>True</code>。</li>
<li><code>inputs['input_ids'] != 101</code>：排除 <code>[CLS]</code>，即每个句子的<strong>第一个 token</strong>。
<ul>
<li>这行代码生成一个布尔张量：<code>[CLS]</code> 的位置为 <code>False</code>，其他位置为 <code>True</code>。</li>
</ul>
</li>
<li><code>inputs['input_ids'] != 102</code> — 排除 <code>[SEP]</code>. 同理，<code>102</code> 是 <code>[SEP]</code>（句子的最后一个 token），这个位置也设为 <code>False</code>。</li>
<li><code>*</code>（乘法）：逻辑与（AND）。三个布尔张量逐元素相乘（<code>True = 1</code>，<code>False = 0</code>），相当于做<strong>逻辑与运算</strong>——只有三个条件都为 <code>True</code> 的位置，结果才是 <code>True</code>。</li>
</ul><hr /><section><h2>2.1 为什么要排除 [CLS] 和 [SEP]？<a href="#21-为什么要排除-cls-和-sep"><span>#</span></a></h2><p>这是 BERT 论文中的要求——<strong>特殊 token 不应该被 mask</strong>：</p><ul>
<li><code>[CLS]</code> 是分类标记，不包含有意义的词义</li>
<li><code>[SEP]</code> 是分隔符，也不包含有意义的词义</li>
<li>mask 它们没有意义，模型也学不到什么</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sum</span><span>(mask_arr[</span><span>0</span><span>]) </span><span># tensor(9)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>selection </span><span>=</span><span> torch.</span><span>flatten</span><span>(mask_arr[</span></span><span>0</span><span><span>].</span><span>nonzero</span><span>()).</span><span>tolist</span><span>()</span></span></div></div><div><div><div>2</div></div><div><span>selection</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[12, 19, 25, 40, 48, 50, 53, 56, 58]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就是<strong>把布尔掩码中 <code>True</code> 的位置提取出来，变成一个 easy-to-use 的 Python 列表。</strong></p><ul>
<li><code>mask_arr[0]</code>：取出 <code>mask_arr</code> 的第一行（因为只有 1 句话，batch size = 1）。</li>
</ul><p>之前 <code>mask_arr</code> 的形状是 <code>(1, 62)</code>，所以 <code>mask_arr[0]</code> 变成一维的 <code>(62,)</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([False, False, ..., True, ..., False])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>.nonzero()</code>：找到所有值为 <code>True</code> 的<strong>索引位置</strong>。</li>
</ul><p>输出是一个 2D 张量，每个 <code>True</code> 对应一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[12],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[19],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[25],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>...</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[58]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>torch.flatten(...)</code>：把上面的 2D 张量<strong>展平</strong>成一维：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[12, 19, 25, 40, 48, 50, 53, 56, 58]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>.tolist()</code>：把 PyTorch 张量转成<strong>普通的 Python 列表</strong>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>12</span><span>, </span><span>19</span><span>, </span><span>25</span><span>, </span><span>40</span><span>, </span><span>48</span><span>, </span><span>50</span><span>, </span><span>53</span><span>, </span><span>56</span><span>, </span><span>58</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们可以对比一下数据流转：</p>

<table><thead><tr><th>步骤</th><th>形状</th><th>内容</th></tr></thead><tbody><tr><td><code>mask_arr</code></td><td><code>(1, 62)</code></td><td><code>[[False, False, True, ...]]</code></td></tr><tr><td><code>mask_arr[0]</code></td><td><code>(62,)</code></td><td><code>[False, False, True, ...]</code></td></tr><tr><td><code>.nonzero()</code></td><td><code>(12, 1)</code></td><td><code>[[12], [19], ...]</code></td></tr><tr><td><code>flatten</code></td><td><code>(12,)</code></td><td><code>[12, 19, 25, ...]</code></td></tr><tr><td><code>.tolist()</code></td><td>Python list</td><td><code>[12, 19, 25, 40, 48, 50, 53, 56, 58]</code></td></tr></tbody></table><hr /><p>为什么要转成列表？</p><p>后续代码需要<strong>逐个处理</strong>这些被选中的位置（把对应的 token 换成 <code>[MASK]</code>）。Python 列表比 PyTorch 张量更容易做循环遍历：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> i </span><span>in</span><span> selection:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># 把第 i 个位置的 token 替换成 [MASK]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最终输出 <code>[12, 19, 25, 40, 48, 50, 53, 56, 58]</code> 就是<strong>这 9 个 token 的位置索引</strong>，后续我们要对它们进行 masking 操作。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tokenizer.special_tokens_map</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'unk_token': '[UNK]',</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>'sep_token': '[SEP]',</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>'pad_token': '[PAD]',</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>'cls_token': '[CLS]',</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>'mask_token': '[MASK]'}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tokenizer.vocab[</span><span>'[MASK]'</span><span>] </span><span># 103</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inputs[</span><span>'input_ids'</span><span>][</span><span>0</span><span><span>, selection] </span><span>=</span><span> </span></span><span>103</span></div></div><div><div><div>2</div></div><div><span>inputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[  101,  2044,  8181,  5367,  2180,  1996,  2281,  7313,  4883,  2602,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>2006,  2019,   103,  1011,  8864,  4132,  1010,  2019,  3988,   103,</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>6658,  2163,  4161,  2037, 22965,   103,  1996,  2406,  2000,  2433,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>1996, 18179,  1012,  2162,  3631,  2041,  1999,  2258,  6863,  2043,</span></div></div><div><div><div>5</div></div><div><span><span>           </span></span><span>103,  2923,  2749,  4457,  3481,  7680,  3334,  1999,   103,  3792,</span></div></div><div><div><div>6</div></div><div><span><span>           </span></span><span>103,  2074,  2058,   103,  3204,  2044,   103,  1005,   103, 17331,</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>1012,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'labels': tensor([[  101,  2044,  8181,  5367,  2180,  1996,  2281,  7313,  4883,  2602,</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>2006,  2019,  3424,  1011,  8864,  4132,  1010,  2019,  3988,  2698,</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>6658,  2163,  4161,  2037, 22965,  2013,  1996,  2406,  2000,  2433,</span></div></div><div><div><div>14</div></div><div><span><span>          </span></span><span>1996, 18179,  1012,  2162,  3631,  2041,  1999,  2258,  6863,  2043,</span></div></div><div><div><div>15</div></div><div><span><span>         </span></span><span>22965,  2923,  2749,  4457,  3481,  7680,  3334,  1999,  2148,  3792,</span></div></div><div><div><div>16</div></div><div><span><span>          </span></span><span>1010,  2074,  2058,  1037,  3204,  2044,  5367,  1005,  1055, 17331,</span></div></div><div><div><div>17</div></div><div><span><span>          </span></span><span>1012,   102]])}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里就是我们 MLM 任务数据准备中 <strong>最关键的一步</strong>：我们真正把被选中的 token 替换为 <code>[MASK]</code>.</p><p>代码解析：</p><ul>
<li><code>inputs['input_ids']</code> ：形状为 (1, 62) 的 token 矩阵；</li>
<li><code>[0, selection]</code> ：<strong>高级索引</strong>（fancy indexing）：
<ul>
<li><code>0</code> 表示取第 0 句话，在这里，我们的 batch 中也只有一句；</li>
<li><code>selection</code> 就是我们之前得到的列表 <code>[12, 19, 25, 40, 48, 50, 53, 56, 58]</code>，即所有要遮挡的位置。</li>
</ul>
</li>
<li><code>=103</code> ：我们把这 9 个位置的值全部替换为 103，即 <code>[MASK]</code> 的 id.</li>
</ul><p>例如替换前：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>位置:  ...  18    19    20    21    ...</span></div></div><div><div><div>2</div></div><div><span>值:   ... 3988  2698  6658  2163  ...</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>(anti) (slavery)  (platform)  (,)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>替换后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>位置:  ...  18    19    20    21    ...</span></div></div><div><div><div>2</div></div><div><span>值:   ... 103   2698  103   2163  ...</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>([MASK]) (slavery) ([MASK]) (,)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>别忘了，我们的 <code>labels</code> 还是备份好的 <strong>原始值</strong>。所以，我们的下一步就是：模型看到 <code>[MASK]</code>，需要预测出 <code>anti</code> 和 <code>platform</code>，再和 <code>labels</code> 对比计算 loss.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>' '</span><span><span>.</span><span>join</span><span>(tokenizer.</span><span>convert_ids_to_tokens</span><span>(inputs[</span></span><span>'input_ids'</span><span>][</span><span>0</span><span>]))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"[CLS] after abraham lincoln won the november 1860 presidential election on an [MASK] - slavery platform , an initial [MASK] slave states declared their secession [MASK] the country to form the confederacy . war broke out in april 1861 when [MASK] ##ist forces attacked fort sum ##ter in [MASK] carolina [MASK] just over [MASK] month after [MASK] ' [MASK] inauguration . [SEP]"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们可以很清晰地从上面的输出中看到，这 9 个位置都被 MASK 掉了。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>' '</span><span><span>.</span><span>join</span><span>(tokenizer.</span><span>convert_ids_to_tokens</span><span>(inputs[</span></span><span>'labels'</span><span>][</span><span>0</span><span>]))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"[CLS] after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession ##ist forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . [SEP]"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，我们保存好的 <code>labels</code> 是完整的，这是我们的 ground truth.</p></section></section>
<section><h1>3. forward and calculate loss<a href="#3-forward-and-calculate-loss"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mlm.</span><span>eval</span><span>()</span></span></div></div><div><div><div>2</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>outputs </span><span>=</span><span> </span><span>mlm</span><span>(**inputs)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>outputs.</span><span>keys</span><span>() </span></span><span># odict_keys(['loss', 'logits', 'hidden_states'])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你还记得我们在 11 节中看到的 <code>outputs.keys()</code> 的话，你会发现 11 节中的输出是 <code>odict_keys(['last_hidden_state', 'pooler_output'])</code>，和这里的 <code>odict_keys(['loss', 'logits', 'hidden_states'])</code> 不一样。</p><p>这主要是因为模型不同。在 11 节中，<code>outputs.keys()</code> 来自 BertModel，而这里我们使用的是 BertForMaskedLM. 它的输出结果中：</p><ul>
<li><code>loss</code> ：预测结果与 <code>labels</code> 之间的损失值（标量）；</li>
<li><code>logits</code> ：每个 token 位置对词表中 30522 个子的预测分数 (1, 62, 30522)；</li>
<li><code>hidden_states</code> ：各层的隐藏状态，因为我们加载时传入了 <code>output_hidden_states=True</code> 的参数。</li>
</ul><section><h2>3.1 <code>loss</code> 是怎么来的<a href="#31-loss-是怎么来的"><span>#</span></a></h2><p>首先，前向传播时，BertModel 先输出 <code>last_hidden_state</code> 和 <code>pooler_output</code>，然后 MLM Head 接过 <code>last_hidden_state</code> 做预测，<code>loss</code> 和 <code>logits</code> 都是在这个阶段算出来的。</p><p>具体到 <code>loss</code>，它的出现是因为我们在 <code>inputs</code> 里传入了 <code>labels</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inputs[</span><span>'labels'</span><span><span>] </span><span>=</span><span> inputs[</span></span><span>'input_ids'</span><span><span>].</span><span>detach</span><span>().</span><span>clone</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>BertForMaskedLM 的前向代码中有一个内置的损失函数，当检测到 <code>labels</code> 存在时，会自动计算 <code>CrossEntropyLoss(logits, labels)</code>，把结果放在 <code>outputs['loss']</code> 中。</p><p>而 BertModel 没有这个逻辑——它甚至不知道什么是 MLM 任务，所以永远不会有 <code>loss</code> 和 <code>logits</code>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs.logits</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ -7.2043,  -7.1276,  -7.1729,  ...,  -6.3316,  -6.3432,  -4.3540],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-12.1506, -12.0077, -12.1967,  ..., -11.4132, -10.8127,  -8.9152],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[ -6.2586,  -6.4102,  -5.8441,  ...,  -6.1903,  -6.3061,  -5.1038],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ -3.5310,  -3.7281,  -3.4160,  ...,  -2.7291,  -2.5730,  -6.5398],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[-14.4257, -14.3249, -14.3869,  ..., -11.1762, -11.3302, -10.0136],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-11.3686, -11.7058, -11.4880,  ..., -11.0921,  -9.7356,  -8.3027]]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs.loss </span><span># tensor(0.6281)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>type</span><span>(outputs.hidden_states) </span><span># tuple</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>len</span><span>(outputs.hidden_states) </span><span># 13</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>outputs[</span><span>'hidden_states'</span><span><span>][</span><span>-</span></span><span>1</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[-4.3005e-01,  3.4016e-02, -3.8321e-01,  ..., -3.4883e-01,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>-1.0174e-01,  4.8761e-01],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-6.9344e-01, -3.2490e-04,  2.5159e-01,  ..., -4.7717e-01,</span></div></div><div><div><div>4</div></div><div><span><span>           </span></span><span>1.6140e-01,  6.2177e-01],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[-6.0726e-01,  1.0112e+00, -6.3583e-01,  ..., -6.0174e-01,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>-3.2421e-01,  4.3809e-01],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>[-4.3882e-01,  2.9114e-01, -8.9229e-01,  ..., -3.3631e-01,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>-8.5137e-01,  9.9881e-01],</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>[ 5.0888e-01,  1.2994e-02, -3.8768e-01,  ...,  3.2210e-02,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>-6.3586e-01,  1.6127e-02],</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>[-3.6704e-03,  1.7164e-01, -7.1432e-01,  ..., -4.6810e-01,</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>-8.0063e-01,  2.6163e-02]]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关于 <code>outputs.hidden_states</code> 的长度和类型，我们在 07 节中做了详尽的探讨，这里的 13 包含了第 0 层 embedding 层和 12 个 transformer 层（attention layers）.</p></section></section>
<section><h1>4. from scratch<a href="#4-from-scratch"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mlm.</span><span>cls</span><span>(outputs[</span></span><span>'hidden_states'</span><span><span>][</span><span>-</span></span><span>1</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ -7.2043,  -7.1276,  -7.1729,  ...,  -6.3316,  -6.3432,  -4.3540],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-12.1506, -12.0077, -12.1967,  ..., -11.4132, -10.8127,  -8.9152],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[ -6.2586,  -6.4102,  -5.8441,  ...,  -6.1903,  -6.3061,  -5.1038],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ -3.5310,  -3.7281,  -3.4160,  ...,  -2.7291,  -2.5730,  -6.5398],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[-14.4257, -14.3249, -14.3869,  ..., -11.1762, -11.3302, -10.0136],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-11.3686, -11.7058, -11.4880,  ..., -11.0921,  -9.7356,  -8.3027]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;ViewBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，这里的输出与之前的 <code>outputs.logits</code> 是完全一致的。这是因为 <code>outputs.logits</code> 本身就是由 <code>mlm.cls(outputs['hidden_states'][-1])</code> 计算出来的。当我们执行 <code>mlm(**inputs)</code> 时，内部的数据流是这样的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inputs</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span>mlm.bert(inputs)  ← 12层 Transformer</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>├── outputs['last_hidden_state']   = 最后一层输出  (1, 62, 768)</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>└── outputs['hidden_states']       = 全部 13 层输出</span></div></div><div><div><div>8</div></div><div><span><span>                                          </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span>                                    </span></span><span>hidden_states[-1]  ← 还是最后一层</span></div></div><div><div><div>10</div></div><div><span><span>                                          </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span>                                          </span></span><span>▼</span></div></div><div><div><div>12</div></div><div><span><span>                                    </span></span><span>mlm.cls(...)  ← BertOnlyMLMHead</span></div></div><div><div><div>13</div></div><div><span><span>                                          </span></span><span>│</span></div></div><div><div><div>14</div></div><div><span><span>                                    </span></span><span>┌─────┴──────┐</span></div></div><div><div><div>15</div></div><div><span><span>                                    </span></span><span>│             │</span></div></div><div><div><div>16</div></div><div><span><span>                               </span></span><span>transform     decoder</span></div></div><div><div><div>17</div></div><div><span><span>                               </span></span><span>(Linear+GELU   (Linear</span></div></div><div><div><div>18</div></div><div><span><span>                                </span></span><span>+LayerNorm)    768→30522)</span></div></div><div><div><div>19</div></div><div><span><span>                                    </span></span><span>│             │</span></div></div><div><div><div>20</div></div><div><span><span>                                    </span></span><span>└──────┬──────┘</span></div></div><div><div><div>21</div></div><div><span><span>                                           </span></span><span>▼</span></div></div><div><div><div>22</div></div><div><span><span>                                     </span></span><span>outputs['logits']  (1, 62, 30522)</span></div></div><div><div><div>23</div></div><div><span><span>                                           </span></span><span>│</span></div></div><div><div><div>24</div></div><div><span><span>                                     </span></span><span>CrossEntropyLoss(logits, labels)</span></div></div><div><div><div>25</div></div><div><span><span>                                           </span></span><span>│</span></div></div><div><div><div>26</div></div><div><span><span>                                     </span></span><span>outputs['loss']</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>需要补充的一点是，这里的 <code>cls</code> 指的就是 <strong>分类器</strong>。在 HuggingFace 中，各类任务模型的”任务头”统一命名为 <code>cls</code>，因为本质上它们都是在做分类（classification）：</p>

<table><thead><tr><th>模型</th><th>任务头</th><th>做什么分类</th></tr></thead><tbody><tr><td><code>BertForMaskedLM</code></td><td><code>cls</code></td><td>对 30522 个词分类 → 预测被 mask 的词</td></tr><tr><td><code>BertForSequenceClassification</code></td><td><code>cls</code></td><td>对情感/类别分类 → 判断句子是正面/负面</td></tr><tr><td><code>BertForNextSentencePrediction</code></td><td><code>cls</code></td><td>二分类 → 判断两句话是否连续</td></tr></tbody></table><p>而在 MLM 任务中，<code>cls</code> = MLM Head.</p><p>我们可以从模型结构看到 <code>mlm.cls</code> 的类型：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mlm.cls</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertOnlyMLMHead(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(predictions): BertLMPredictionHead(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(transform): BertPredictionHeadTransform(</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>(transform_act_fn): GELUActivation()</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>(decoder): Linear(in_features=768, out_features=30522, bias=True) &lt;- 真正的分类层</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>10</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它做的就是对一个 mask 位置的上下文向量，从 30522 个候选词中 <strong>分类出</strong> 最可能是哪一个词。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>last_hidden_state </span><span>=</span><span> outputs[</span></span><span>'hidden_states'</span><span><span>][</span><span>-</span></span><span>1</span><span>] </span><span># 最后一个隐藏层的输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mlm.</span><span>eval</span><span>()</span></span></div></div><div><div><div>2</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>transformed </span><span>=</span><span> mlm.cls.predictions.</span><span>transform</span><span>(last_hidden_state)</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(transformed.shape) </span><span># torch.Size([1, 62, 768])</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>logits </span><span>=</span><span> mlm.cls.predictions.</span><span>decoder</span><span>(transformed)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>print</span><span>(logits.shape) </span><span># torch.Size([1, 62, 30522])</span></div></div><div><div><div>7</div></div><div><span>logits</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ -7.2043,  -7.1276,  -7.1729,  ...,  -6.3316,  -6.3432,  -4.3540],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-12.1506, -12.0077, -12.1967,  ..., -11.4132, -10.8127,  -8.9152],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[ -6.2586,  -6.4102,  -5.8441,  ...,  -6.1903,  -6.3061,  -5.1038],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ -3.5310,  -3.7281,  -3.4160,  ...,  -2.7291,  -2.5730,  -6.5398],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[-14.4257, -14.3249, -14.3869,  ..., -11.1762, -11.3302, -10.0136],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-11.3686, -11.7058, -11.4880,  ..., -11.0921,  -9.7356,  -8.3027]]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里，我们就是把 <code>hidden_states[-1]</code>（最后一层的输出，768 维）转换成最终的 logits：</p><ol>
<li>第一步是 <code>transform</code>，它对应我们前面 <code>mlm.cls</code> 结构中的：</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(transform): BertPredictionHeadTransform(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>(dense): Linear(768 → 768)           ← 全连接</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(transform_act_fn): GELUActivation()  ← GELU 激活</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,))        ← LayerNorm</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>做的事情：对每个 token 的 768 维向量做一次非线性变换（Linear → GELU → LayerNorm），输出仍是 (1, 62, 768). 你可以理解为，在输出最终的分类结果前，我们先对特征做一道“精加工”。</p><ol>
<li>第二步是 <code>decoder</code>，它对应模型结构中的：</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(decoder): Linear(in_features=768, out_features=30522, bias=True)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>做的事情：把 768 维映射到 30522 维，输出 (1, 62, 30522).</p><p>整个的数据流如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>last_hidden_state           (1, 62, 768)</span></div></div><div><div><div>2</div></div><div><span><span>       </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>       </span></span><span>▼  transform (Linear + GELU + LayerNorm)</span></div></div><div><div><div>4</div></div><div><span>transformed                 (1, 62, 768)   ← 维度不变，特征被精加工</span></div></div><div><div><div>5</div></div><div><span><span>       </span></span><span>│</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>▼  decoder (Linear 768 → 30522)</span></div></div><div><div><div>7</div></div><div><span>logits                      (1, 62, 30522) ← 每个 token 对 30522 个词的预测分数</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而两者合起来就是完整的 <code>mlm.cls()</code>.</p></section>
<section><h1>5. loss and translate<a href="#5-loss-and-translate"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>cross_entropy </span><span>=</span><span> nn.</span><span>CrossEntropyLoss</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>logits.shape </span><span># torch.Size([1, 62, 30522])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>inputs[</span><span>'labels'</span><span>].shape </span><span># torch.Size([1, 62])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>inputs[</span><span>'labels'</span><span>][</span><span>0</span><span><span>].</span><span>view</span><span>(</span><span>-</span></span><span>1</span><span>).shape </span><span># torch.Size([62])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们主要关注最后一行，它输出的是 <code>labels</code> 展平后的一维形状：</p><ul>
<li><code>inputs['labels']</code> ：形状是 (1, 62)，这是之前备份的原始 <code>input_ids</code>，所以形状就是 <code>(batch_size, seq_len)</code>；</li>
<li><code>[0]</code> ：取第 0 句话，也就是去掉 batch 维度，形状变为 (62,)；</li>
<li><code>.view(-1)</code> ：这是 Pytorch 中“自动推断该维度大小”的展平操作。由于原来的 <code>[0]</code> 已经是 (62,) 一维了，所以 <code>view(-1)</code> 并没有改变形状。</li>
</ul><p>因此，最终的形状就是 62 个 token ID，一个简单的一维序列。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>torch.</span><span>argmax</span><span>(logits[</span></span><span>0</span><span>], </span><span>dim</span><span>=-</span><span>1</span><span>) </span><span># torch.Size([1, 62])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([ 1012,  2044,  8181,  5367,  2180,  1996,  2281,  7313,  4883,  2602,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>2006,  2019,  3424,  1011,  8864,  4132,  1010,  2019,  3988,  2698,</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>6658,  2163,  4161,  2037, 22965,  2013,  1996,  2406,  2000,  2433,</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>1996, 18179,  1012,  2162,  3631,  2041,  1999,  2258,  6863,  2043,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>22965,  8055,  2749,  4457,  3481,  7680,  3334,  1999,  2148,  3792,</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>1010,  2074,  2058,  1037,  3204,  2044,  5367,  1005,  1055, 17331,</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>1012,  1055])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就是 <strong>把模型的预测分数转换成具体的预测词 ID</strong>：</p><ul>
<li><code>logits</code> ：形状 (1, 62, 30522)，表示 <code>logits</code> 中每个 token 的 30522 个分数，代表这个词是词表中每个词的可能性；</li>
<li><code>logits[0]</code> ：去掉 batch 维度，形状变为 (62, 30522)；</li>
<li><code>argmax(logits[0], dim=-1)</code> ：<strong>取最后一个维度的最大值索引</strong>。
<ul>
<li><code>dim=-1</code> 表示沿着最后一个维度（即 30522 词表维度），找分数最高的那个位置；</li>
<li>对每个 token，从 30522 个候选词中挑出分数最高的那个词的 ID；</li>
<li>输出形状为 (62,)，即 62 个位置各自的预测词 ID.</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>logits[0]               (62, 30522)</span></div></div><div><div><div>2</div></div><div><span>┌──────────────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span>│ 位置0: [-6.97, -6.90, ..., -4.24]    │ → argmax → ID=1996</span></div></div><div><div><div>4</div></div><div><span>│ 位置1: [-12.18, -12.09, ..., -9.73]  │ → argmax → ID=2044</span></div></div><div><div><div>5</div></div><div><span>│ 位置2: [-6.56, -6.69, ..., -5.23]    │ → argmax → ID=1055</span></div></div><div><div><div>6</div></div><div><span>│ ...                                  │</span></div></div><div><div><div>7</div></div><div><span>│ 位置61: [-12.45, -12.79, ..., -8.94] │ → argmax → ID=1055</span></div></div><div><div><div>8</div></div><div><span>└──────────────────────────────────────┘</span></div></div><div><div><div>9</div></div><div><span><span>                    </span></span><span>↓</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>tensor([1996, 2044, 1055, ..., 1055])</span></div></div><div><div><div>11</div></div><div><span><span>                         </span></span><span>(62,)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>' '</span><span><span>.</span><span>join</span><span>(tokenizer.</span><span>convert_ids_to_tokens</span><span>(inputs[</span></span><span>'input_ids'</span><span>][</span><span>0</span><span>]))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"[CLS] after abraham lincoln won the november 1860 presidential election on an [MASK] - slavery platform , an initial [MASK] slave states declared their secession [MASK] the country to form the confederacy . war broke out in april 1861 when [MASK] ##ist forces attacked fort sum ##ter in [MASK] carolina [MASK] just over [MASK] month after [MASK] ' [MASK] inauguration . [SEP]"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是我们之前看过的输出，也就是我们要进行“完形填空”的对象。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>' '</span><span><span>.</span><span>join</span><span>(tokenizer.</span><span>convert_ids_to_tokens</span><span>(torch.</span><span>argmax</span><span>(logits[</span></span><span>0</span><span>], </span><span>dim</span><span>=</span><span>1</span><span>)))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>". after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession confederate forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . s"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里就是我们模型对整个句子的预测结果，相当于“做完完形填空后的答卷”。</p><p>我们可以看到，大部分 <code>[MASK]</code> 的位置都填对了，只有个别位置，比如 <code>##ist</code> 前预测了 <code>confederate</code> 而不是 <code>secession</code>.</p><p>不过我们要特别注意的是，预测结果的开头和结尾：</p>

<table><thead><tr><th>位置</th><th>预测</th><th>期望</th><th>原因</th></tr></thead><tbody><tr><td>位置 0</td><td><code>.</code>（句号）</td><td><code>[CLS]</code></td><td>模型不知道 <code>[CLS]</code> 是特殊 token，它只是在 30522 个词中选了分数最高的</td></tr><tr><td>位置 61</td><td><code>s</code></td><td><code>[SEP]</code></td><td>同理</td></tr></tbody></table><p>这不是模型”猜错了”，而是 <code>convert_ids_to_tokens</code> 会把任何 ID 都转成对应的词——包括非 mask 位置。模型对 <code>[CLS]</code> 位置预测了 <code>.</code>，因为 <code>[CLS]</code> 本身不在词表中，模型只能输出词表中存在的 token。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>' '</span><span><span>.</span><span>join</span><span>(tokenizer.</span><span>convert_ids_to_tokens</span><span>(inputs[</span></span><span>'labels'</span><span>][</span><span>0</span><span>]))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"[CLS] after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession ##ist forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . [SEP]"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/skill-01/</id>
      <title type="text">skill 入门</title>
      <published>2026-07-06T00:00:00.000Z</published>
      <updated>2026-07-06T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/skill-01/"/>
      <summary type="text">简要介绍什么是 skills，以及有哪些开放标准</summary>
      <content type="html"><![CDATA[<section><h1>1. 什么是 skill<a href="#1-什么是-skill"><span>#</span></a></h1><p>简单来说，skills 就是一种轻量、开放的格式，用来给 Agent 增加专门的知识和工作流，使其能够更可靠地完成任务。</p><p>skill 的核心结构很简单：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>my-skill/</span></div></div><div><div><div>2</div></div><div><span>├── SKILL.md          # 必填：元数据 + 操作说明</span></div></div><div><div><div>3</div></div><div><span>├── scripts/          # 可选：可执行脚本</span></div></div><div><div><div>4</div></div><div><span>├── references/       # 可选：参考文档</span></div></div><div><div><div>5</div></div><div><span>├── assets/           # 可选：模板、资源</span></div></div><div><div><div>6</div></div><div><span>└── ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从上面的结构可以看出，<code>SKILL.md</code> 必须包含：</p><ul>
<li>元数据：<code>name</code>, <code>description</code> (YAML frontmatter)</li>
<li>正文：Markdown 格式的步骤说明，教 Agent 如何完成某项任务</li>
</ul><div></div><div>YAML frontmatter</div><div></div><div><p>YAML 指的是一种人类可读的配置格式，它采用  <code>键: 值</code> 来写数据；frontmatter则是前置元数据，它就是放在 Markdown 文件最开头，用 <code>---</code> 包起来的一段结构化元数据。</p><p>YAML frontmatter 的作用是，在 Discovery 阶段，只有 frontmatter 里面的字段会被 Agent 读取，Agent 启动时只看这些简短的元数据，判断这个 skill 是否和当前任务有关。</p><p>只有任务匹配时，才会加载元数据下方的完整 Markdown 说明（Activation 阶段）。</p></div><section><h2>1.1 Skill 解决什么问题<a href="#11-skill-解决什么问题"><span>#</span></a></h2><p>当下，Agent 面临的问题之一并不是自身的能力，而是缺少真实工作需要的上下文。Skill 的主要作用就是：将流程性的知识、团队 / 公司 / 个人 的上下文打包成可以版本控制、可移植的文件夹，然后按需加载。</p><p>Skill 的主要价值：</p><ul>
<li>领域专长：如法律审查流程、数据分析、PPT 格式等；</li>
<li>可重复工作流：多步骤任务变成一致、可审计的流程；</li>
<li>跨产品复用：Skill 只需要写一次，就可以在支持该标准的多个 Agent 产品中复用。</li>
</ul></section><section><h2>1.2 Skill 的基本机制<a href="#12-skill-的基本机制"><span>#</span></a></h2><p>Skill 的基本加载机制是<strong>渐进式披露</strong>：</p>

<table><thead><tr><th>阶段</th><th>加载内容</th></tr></thead><tbody><tr><td><strong>Discovery（发现）</strong></td><td>启动时只读每个 skill 的 <code>name</code> 和 <code>description</code></td></tr><tr><td><strong>Activation（激活）</strong></td><td>任务匹配时，才读完整 <code>SKILL.md</code></td></tr><tr><td><strong>Execution（执行）</strong></td><td>按说明执行，必要时运行脚本或加载引用文件</td></tr></tbody></table><p>这样 Agent 可以挂载很多 skills，而不会在上下文里一次性塞满所有内容。</p><p>该格式最初由 <strong>Anthropic</strong> 开发并作为开放标准发布，Cursor 等产品也采用同一套 <code>SKILL.md</code> 结构。</p><hr /></section></section>
<section><h1>2. Skill 的标准<a href="#2-skill-的标准"><span>#</span></a></h1><p>如前文所述，skill 是最初由 Anthropic 开发并开放的格式，虽然没有官方的统一标准，但 <a href="https://agentskills.io/" target="_blank">agentskills.io</a> 无疑是事实上的通用标准。其他的一些 skill 标准大多也兼容 <a href="https://agentskills.io/" target="_blank">agentskills.io</a> 的标准。</p></section>
<section><h1>3. Skill 的本质<a href="#3-skill-的本质"><span>#</span></a></h1><p>虽然 <code>SKILL.md</code> 正文的本质就是一段 prompt，但 skill 本身已经超出了单纯的 prompt engineering 范畴，它更像是：以 prompt 为核心，加上标准格式、按需加载和资源捆绑的能力封装。</p>

<table><thead><tr><th>维度</th><th>单纯 prompt engineering</th><th>Agent Skill</th></tr></thead><tbody><tr><td><strong>结构</strong></td><td>一段文本，常散落在各处</td><td>标准化文件夹 + <code>SKILL.md</code> + 可选资源</td></tr><tr><td><strong>加载方式</strong></td><td>通常一次性全塞进上下文</td><td><strong>渐进式披露</strong>：先读 <code>name</code>/<code>description</code>，匹配后才加载全文</td></tr><tr><td><strong>触发机制</strong></td><td>靠用户手动粘贴或固定 system prompt</td><td><code>description</code> 专门用于 Agent <strong>自动判断</strong>是否激活</td></tr><tr><td><strong>附带能力</strong></td><td>通常只有文字</td><td>可捆绑 <strong>scripts/</strong>、<strong>references/</strong>、<strong>assets/</strong></td></tr><tr><td><strong>可移植性</strong></td><td>绑定某个产品/某次对话</td><td>写一次，多个 skills-compatible 客户端复用</td></tr><tr><td><strong>维护方式</strong></td><td>难版本管理、难评估</td><td>可 git 管理、可写 eval 测触发率和输出质量</td></tr></tbody></table><ul>
<li><strong>Prompt engineering</strong>：教模型「这次对话该怎么想、怎么做」</li>
<li><strong>Agent Skill</strong>：把「某类任务该怎么想、怎么做」做成<strong>可安装、可发现、按需加载的能力模块</strong></li>
</ul><p>Skill 里当然包含 prompt 设计（尤其是 <code>description</code> 的触发优化和正文指令），但还多了：</p><ol>
<li><strong>元数据层</strong>（frontmatter）——给机器读的「目录索引」</li>
<li><strong>运行时协议</strong>（Discovery → Activation → Execution）——给客户端实现的加载逻辑</li>
<li><strong>资源层</strong>（脚本、模板、参考文档）——不只是文字，还能<strong>真正执行</strong></li>
</ol></section>
<section><h1>Skill 的来源<a href="#skill-的来源"><span>#</span></a></h1><p>这里列举一些 skills 的相关资源：</p>

<table><thead><tr><th>网站</th><th>网址</th></tr></thead><tbody><tr><td>Skill 聚合入口</td><td><a href="https://skills.sh/" target="_blank">https://skills.sh/</a></td></tr><tr><td>Skills 市场（中文界面）</td><td><a href="https://skillsmp.com/zh" target="_blank">https://skillsmp.com/zh</a></td></tr><tr><td>腾讯家的 Skills 市场</td><td><a href="https://skillhub.tencent.com/" target="_blank">https://skillhub.tencent.com/</a></td></tr><tr><td>Agent Skills 官方标准站点</td><td><a href="https://agentskills.io/" target="_blank">https://agentskills.io</a></td></tr><tr><td>Anthropic 官方工程文章（Agent Skills 实战理念）</td><td><a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills" target="_blank">https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills</a></td></tr><tr><td>VS Code Copilot Agent Skills 文档</td><td><a href="https://code.visualstudio.com/docs/copilot/customization/agent-skills" target="_blank">https://code.visualstudio.com/docs/copilot/customization/agent-skills</a></td></tr><tr><td>Anthropic 官方 Skills GitHub 仓库</td><td><a href="https://github.com/anthropics/skills" target="_blank">https://github.com/anthropics/skills</a></td></tr><tr><td>Claude 技能精选列表（Awesome 系列）</td><td><a href="https://github.com/ComposioHQ/awesome-claude-skills" target="_blank">https://github.com/ComposioHQ/awesome-claude-skills</a></td></tr><tr><td>软件开发自动化工作流 Skills 集合</td><td><a href="https://github.com/obra/superpowers" target="_blank">https://github.com/obra/superpowers</a></td></tr><tr><td>自动生成 Skill 的 Skill（官方示例）</td><td><a href="https://github.com/anthropics/skills/tree/main/skills/skill-creator" target="_blank">https://github.com/anthropics/skills/tree/main/skills/skill-creator</a></td></tr></tbody></table></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-07/</id>
      <title type="text">CH3_04_multi_milvus</title>
      <published>2026-07-05T00:00:00.000Z</published>
      <updated>2026-07-05T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-07/"/>
      <summary type="text">使用 Milvus 进行多模态检索</summary>
      <content type="html"><![CDATA[<p>本节演示<strong>多模态图文检索</strong>：用 Visual BGE 把图像/文本变成向量，存入 Milvus，再做相似度搜索。</p>
<p>下面的两个 cell 主要是为代码在 Google Colab 上运行设计。</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Google Colab: run this cell first to install notebook dependencies.</span></div></div><div><div><div>2</div></div><div><span>import</span><span> os</span></div></div><div><div><div>3</div></div><div><span>import</span><span> subprocess</span></div></div><div><div><div>4</div></div><div><span>import</span><span> sys</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>REPO_URL</span><span> </span><span>=</span><span> </span></span><span>"https://github.com/datawhalechina/all-in-rag.git"</span></div></div><div><div><div>7</div></div><div><span><span>REPO_DIR</span><span> </span><span>=</span><span> </span></span><span>"/content/all-in-rag"</span></div></div><div><div><div>8</div></div><div><span><span>VISUAL_BGE_DIR</span><span> </span><span>=</span><span> os.path.</span><span>join</span><span>(</span><span>REPO_DIR</span><span>, </span></span><span>"code"</span><span>, </span><span>"C3"</span><span>, </span><span>"visual_bge"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>C3_DIR</span><span> </span><span>=</span><span> os.path.</span><span>join</span><span>(</span><span>REPO_DIR</span><span>, </span></span><span>"code"</span><span>, </span><span>"C3"</span><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>run</span><span>(</span><span>cmd</span><span>:</span><span> list[</span><span>str</span><span>]</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"$"</span><span>, </span><span>" "</span><span><span>.</span><span>join</span><span>(cmd))</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>subprocess.</span><span>check_call</span><span>(cmd)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span># visual_bge ships with this repo; clone it when running on Colab.</span></div></div><div><div><div>18</div></div><div><span>if</span><span> </span><span>not</span><span><span> os.path.</span><span>isdir</span><span>(</span><span>VISUAL_BGE_DIR</span><span>):</span></span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>run</span><span>([</span><span>"git"</span><span>, </span><span>"clone"</span><span>, </span><span>"--depth"</span><span>, </span><span>"1"</span><span><span>, </span><span>REPO_URL</span><span>, </span><span>REPO_DIR</span><span>])</span></span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span># Third-party packages used in this notebook.</span></div></div><div><div><div>22</div></div><div><span># Colab already provides torch, torchvision, numpy, requests, and Pillow.</span></div></div><div><div><div>23</div></div><div><span># milvus-lite must match pymilvus 2.5.x, otherwise search() fails with</span></div></div><div><div><div>24</div></div><div><span># MilvusException: function_score (version mismatch in the local DB backend).</span></div></div><div><div><div>25</div></div><div><span><span>run</span><span>(</span></span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>sys.executable,</span></div></div><div><div><div>28</div></div><div><span>        </span><span>"-m"</span><span>,</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"pip"</span><span>,</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"install"</span><span>,</span></div></div><div><div><div>31</div></div><div><span>        </span><span>"-q"</span><span>,</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"pymilvus==3.0.0"</span><span>,</span></div></div><div><div><div>33</div></div><div><span>        </span><span>"milvus-lite==3.0"</span><span>,</span></div></div><div><div><div>34</div></div><div><span>        </span><span>"opencv-python-headless"</span><span>,</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"tqdm"</span><span>,</span></div></div><div><div><div>36</div></div><div><span>        </span><span>"transformers&gt;=4.40.0"</span><span>,</span></div></div><div><div><div>37</div></div><div><span>        </span><span>"huggingface-hub"</span><span>,</span></div></div><div><div><div>38</div></div><div><span>        </span><span>"timm"</span><span>,</span></div></div><div><div><div>39</div></div><div><span>        </span><span>"einops"</span><span>,</span></div></div><div><div><div>40</div></div><div><span>        </span><span>"ftfy"</span><span>,</span></div></div><div><div><div>41</div></div><div><span>        </span><span>"regex"</span><span>,</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>43</div></div><div><span>)</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span># Install the local visual_bge package.</span></div></div><div><div><div>46</div></div><div><span><span>run</span><span>([sys.executable, </span></span><span>"-m"</span><span>, </span><span>"pip"</span><span>, </span><span>"install"</span><span>, </span><span>"-q"</span><span>, </span><span>"-e"</span><span><span>, </span><span>VISUAL_BGE_DIR</span><span>])</span></span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span># Notebook paths (../../models, ../../data) assume cwd is code/C3.</span></div></div><div><div><div>49</div></div><div><span><span>os.</span><span>chdir</span><span>(</span><span>C3_DIR</span><span>)</span></span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>print</span><span>(</span><span>"Dependencies installed."</span><span>)</span></div></div><div><div><div>52</div></div><div><span>print</span><span>(</span><span>"Working directory:"</span><span><span>, os.</span><span>getcwd</span><span>())</span></span></div></div><div><div><div>53</div></div><div><span>print</span><span>(</span><span>"Next: download model weights with `!python download_model.py` if needed."</span><span>)</span></div></div><div><div><div>54</div></div><div><span>print</span><span>(</span><span>"If you previously hit a Milvus search error, restart the runtime, then re-run all cells."</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>!</span><span>python download_model.py</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> os</span></div></div><div><div><div>2</div></div><div><span>from</span><span> tqdm </span><span>import</span><span> tqdm</span></div></div><div><div><div>3</div></div><div><span>from</span><span> glob </span><span>import</span><span> glob</span></div></div><div><div><div>4</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>5</div></div><div><span>from</span><span> visual_bge.visual_bge.modeling </span><span>import</span><span> Visualized_BGE</span></div></div><div><div><div>6</div></div><div><span>from</span><span> pymilvus </span><span>import</span><span> MilvusClient, FieldSchema, CollectionSchema, DataType</span></div></div><div><div><div>7</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>8</div></div><div><span>import</span><span> cv2</span></div></div><div><div><div>9</div></div><div><span>from</span><span><span> </span><span>PIL</span><span> </span></span><span>import</span><span> Image</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>下面按「库类别 → 简介 → 在本 notebook 中的用途」简要说明这些 import.</p>
<ul>
<li><code>tqdm</code> ：进度条工具，主要用于在循环中显示处理进度，这里我们在批量生成图像嵌入并插入 Milvus 时显示进度；</li>
<li><code>cv2</code> ：计算机视觉库，提供缩放、裁剪、绘制文字、保存图像等操作，这里我们用来在 <code>visualized_results</code> 中缩放图像、加边框、写 Query 和排名序号，最后保存图像；</li>
<li><code>PIL</code> ：图像读写库，用于打开、转换、保存常见格式图像。这里我们用它的 <code>Image.open()</code> 方法读取查询图和检索结果，转为 RGB，最后用 <code>.show()</code> 展示拼接后的检索结果图。</li>
</ul>
<p>这些库如何协作：</p>
<div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>glob + os<br /><br />收集图像路径</p></span></div><div><span><p>Visualized_BGE + torch<br /><br />生成向量</p></span></div><div><span><p>pymilvus<br /><br />存入 Milvus</p></span></div><div><span><p>查询图 + 文本</p></span></div><div><span><p>Visualized_BGE<br /><br />生成查询向量</p></span></div><div><span><p>检索 Top-K 图像路径</p></span></div><div><span><p>PIL + cv2 + numpy<br /><br />可视化结果</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>glob + os<br /><br />收集图像路径</p></span></div><div><span><p>Visualized_BGE + torch<br /><br />生成向量</p></span></div><div><span><p>pymilvus<br /><br />存入 Milvus</p></span></div><div><span><p>查询图 + 文本</p></span></div><div><span><p>Visualized_BGE<br /><br />生成查询向量</p></span></div><div><span><p>检索 Top-K 图像路径</p></span></div><div><span><p>PIL + cv2 + numpy<br /><br />可视化结果</p></span></div>
</div></div></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 初始化设置</span></div></div><div><div><div>2</div></div><div><span><span>MODEL_NAME</span><span> </span><span>=</span><span> </span></span><span>"BAAI/bge-base-en-v1.5"</span></div></div><div><div><div>3</div></div><div><span><span>MODEL_PATH</span><span> </span><span>=</span><span> </span></span><span>"../../models/bge/Visualized_base_en_v1.5.pth"</span></div></div><div><div><div>4</div></div><div><span><span>DATA_DIR</span><span> </span><span>=</span><span> </span></span><span>"../../data/C3"</span></div></div><div><div><div>5</div></div><div><span><span>COLLECTION_NAME</span><span> </span><span>=</span><span> </span></span><span>"multimodal_demo"</span></div></div><div><div><div>6</div></div><div><span># MILVUS_URI = "http://localhost:19530"</span></div></div><div><div><div>7</div></div><div><span><span>MILVUS_URI</span><span> </span><span>=</span><span> </span></span><span>"./milvus_demo.db"</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 2. 定义工具 (编码器和可视化函数)</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>Encoder</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"""编码器类，用于将图像和文本编码为向量。"""</span></div></div><div><div><div>4</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>model_name</span></span><span>:</span><span> </span><span>str</span><span>,</span><span><span> </span><span>model_path</span></span><span>:</span><span> </span><span>str</span><span>):</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.model </span><span>=</span><span> </span><span>Visualized_BGE</span><span>(</span></span><span>model_name_bge</span><span><span>=</span><span>model_name, </span></span><span>model_weight</span><span><span>=</span><span>model_path)</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.model.</span><span>eval</span><span>()</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>def</span><span> </span><span>encode_query</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>image_path</span></span><span>:</span><span> </span><span>str</span><span>,</span><span><span> </span><span>text</span></span><span>:</span><span> </span><span>str</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>9</div></div><div><span>        </span><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>query_emb </span><span>=</span><span> </span><span>self</span><span><span>.model.</span><span>encode</span><span>(</span></span><span>image</span><span><span>=</span><span>image_path, </span></span><span>text</span><span><span>=</span><span>text)</span></span></div></div><div><div><div>11</div></div><div><span>        </span><span>return</span><span><span> query_emb.</span><span>tolist</span><span>()[</span></span><span>0</span><span>] </span><span># 去掉 batch 维度</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>def</span><span> </span><span>encode_image</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>image_path</span></span><span>:</span><span> </span><span>str</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>14</div></div><div><span>        </span><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>query_emb </span><span>=</span><span> </span><span>self</span><span><span>.model.</span><span>encode</span><span>(</span></span><span>image</span><span><span>=</span><span>image_path)</span></span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span><span> query_emb.</span><span>tolist</span><span>()[</span></span><span>0</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<ul>
<li><code>encode_query</code> ：同时传入 <code>image</code> 和 <code>text</code> ，返回一个 tensor 对象 <code>query_emb</code>，它的形状为 <code>[1, 768]</code>，即 1 条查询的 768 维图文融合向量，且已经经过了 L2 归一化，可以直接用于余弦相似度搜索。</li>
<li><code>encode_image</code> ：只传入 <code>image</code>，虽然在底层 <code>Visualized_BGE</code> 里面它也走多模态通路，但文本部分是空字符串，因此只融合了图像。和 <code>encode_query</code> 主要用于检索不同，它主要用于入库，也就是说：
<ul>
<li>文档侧：仅图像</li>
<li>查询侧：图像和文本</li>
</ul>
</li>
</ul>
<p>这样，用户在搜图的时候就能同时给一张图和一句描述，用 <code>encode_query</code> 得到图文联合查询向量，而不是只能以图搜图。</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>visualize_results</span><span>(</span><span>query_image_path</span><span>:</span><span> </span><span>str</span><span>,</span><span><span> </span><span>retrieved_images</span></span><span>:</span><span> </span><span>list</span><span>,</span><span><span> </span><span>img_height</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>300</span></span><span>,</span><span><span> </span><span>img_width</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>300</span></span><span>,</span><span><span> </span><span>row_count</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>3</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""从检索到的图像列表创建一个全景图用于可视化。"""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>panoramic_width </span><span>=</span><span> img_width </span><span>*</span><span> row_count </span><span># 创建两张空白画布</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>panoramic_height </span><span>=</span><span> img_height </span><span>*</span><span> row_count</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>panoramic_image </span><span>=</span><span> np.</span><span>full</span><span>((panoramic_height, panoramic_width, </span><span>3</span><span>), </span><span>255</span><span>, </span><span>dtype</span><span><span>=</span><span>np.uint8) </span></span><span># 左侧结果区</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>query_display_area </span><span>=</span><span> np.</span><span>full</span><span>((panoramic_height, img_width, </span><span>3</span><span>), </span><span>255</span><span>, </span><span>dtype</span><span><span>=</span><span>np.uint8) </span></span><span># 右侧查询区</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 处理查询图像</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>query_pil </span><span>=</span><span> Image.</span><span>open</span><span>(query_image_path).</span><span>convert</span><span>(</span><span>"RGB"</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>query_cv </span><span>=</span><span> np.</span><span>array</span><span>(query_pil)[:, :, ::</span><span>-</span><span>1</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>resized_query </span><span>=</span><span> cv2.</span><span>resize</span><span>(query_cv, (img_width, img_height))</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>bordered_query </span><span>=</span><span> cv2.</span><span>copyMakeBorder</span><span>(resized_query, </span><span>10</span><span>, </span><span>10</span><span>, </span><span>10</span><span>, </span><span>10</span><span><span>, cv2.</span><span>BORDER_CONSTANT</span><span>, </span></span><span>value</span><span><span>=</span><span>(</span></span><span>255</span><span>, </span><span>0</span><span>, </span><span>0</span><span>))</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>query_display_area[img_height </span><span>*</span><span> (row_count </span><span>-</span><span> </span><span>1</span><span><span>):, :] </span><span>=</span><span> cv2.</span><span>resize</span><span>(bordered_query, (img_width, img_height))</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>cv2.</span><span>putText</span><span>(query_display_area, </span><span>"Query"</span><span>, (</span><span>10</span><span><span>, panoramic_height </span><span>-</span><span> </span></span><span>20</span><span><span>), cv2.</span><span>FONT_HERSHEY_SIMPLEX</span><span>, </span></span><span>1</span><span>, (</span><span>255</span><span>, </span><span>0</span><span>, </span><span>0</span><span>), </span><span>2</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 处理检索到的图像</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> i, img_path </span><span>in</span><span> </span><span>enumerate</span><span>(retrieved_images):</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>row, col </span><span>=</span><span> i </span><span>//</span><span> row_count, i </span><span>%</span><span> row_count</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>start_row, start_col </span><span>=</span><span> row </span><span>*</span><span> img_height, col </span><span>*</span><span> img_width</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>retrieved_pil </span><span>=</span><span> Image.</span><span>open</span><span>(img_path).</span><span>convert</span><span>(</span><span>"RGB"</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>retrieved_cv </span><span>=</span><span> np.</span><span>array</span><span>(retrieved_pil)[:, :, ::</span><span>-</span><span>1</span><span>]</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>resized_retrieved </span><span>=</span><span> cv2.</span><span>resize</span><span>(retrieved_cv, (img_width </span><span>-</span><span> </span><span>4</span><span><span>, img_height </span><span>-</span><span> </span></span><span>4</span><span>))</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>bordered_retrieved </span><span>=</span><span> cv2.</span><span>copyMakeBorder</span><span>(resized_retrieved, </span><span>2</span><span>, </span><span>2</span><span>, </span><span>2</span><span>, </span><span>2</span><span><span>, cv2.</span><span>BORDER_CONSTANT</span><span>, </span></span><span>value</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>))</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>panoramic_image[start_row:start_row </span><span>+</span><span> img_height, start_col:start_col </span><span>+</span><span> img_width] </span><span>=</span><span> bordered_retrieved</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>        </span><span># 添加索引号</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>cv2.</span><span>putText</span><span>(panoramic_image, </span><span>str</span><span><span>(i), (start_col </span><span>+</span><span> </span></span><span>10</span><span><span>, start_row </span><span>+</span><span> </span></span><span>30</span><span><span>), cv2.</span><span>FONT_HERSHEY_SIMPLEX</span><span>, </span></span><span>1</span><span>, (</span><span>0</span><span>, </span><span>0</span><span>, </span><span>255</span><span>), </span><span>2</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>hstack</span><span>([query_display_area, panoramic_image])</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p>这里所谓的全景图就是我们最终保存的结果图像，即把多张图拼成一张大图，方便一眼对比查询图和检索结果。</p>
<p>最终输出的图像结构类似：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>最终输出 = 左右拼接（np.hstack）</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>┌─────────┬──────────────────────────────┐</span></div></div><div><div><div>4</div></div><div><span>│         │  检索结果区 (900×900)         │</span></div></div><div><div><div>5</div></div><div><span>│  Query  │  ┌───┬───┬───┐               │</span></div></div><div><div><div>6</div></div><div><span>│  区     │  │ 0 │ 1 │ 2 │  第 0 行      │</span></div></div><div><div><div>7</div></div><div><span>│(300×900)│  ├───┼───┼───┤               │</span></div></div><div><div><div>8</div></div><div><span>│         │  │ 3 │ 4 │   │  第 1 行      │</span></div></div><div><div><div>9</div></div><div><span>│         │  └───┴───┴───┘               │</span></div></div><div><div><div>10</div></div><div><span>│ [Query] │  (3×3 网格，本例只填 5 张)    │</span></div></div><div><div><div>11</div></div><div><span>└─────────┴──────────────────────────────┘</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>300px        900px (3列×300)</span></div></div><div><div><div>13</div></div><div><span>总宽 1200px，高 900px</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 3. 初始化客户端</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"--&gt; 正在初始化编码器和Milvus客户端..."</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>encoder </span><span>=</span><span> </span><span>Encoder</span><span>(</span><span>MODEL_NAME</span><span>, </span><span>MODEL_PATH</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span><span>milvus_client </span><span>=</span><span> </span><span>MilvusClient</span><span>(</span><span>MILVUS_URI</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 4. 创建 Milvus Collection</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>--&gt; 正在创建 Collection '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>'"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>if</span><span><span> milvus_client.</span><span>has_collection</span><span>(</span><span>COLLECTION_NAME</span><span>):</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>milvus_client.</span><span>drop_collection</span><span>(</span><span>COLLECTION_NAME</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"已删除已存在的 Collection: '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>'"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>image_list </span><span>=</span><span> </span><span>glob</span><span>(os.path.</span><span>join</span><span>(</span><span>DATA_DIR</span><span>, </span></span><span>"dragon"</span><span>, </span><span>"*.png"</span><span>))</span></div></div><div><div><div>2</div></div><div><span>if</span><span> </span><span>not</span><span> image_list:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>raise</span><span> </span><span>FileNotFoundError</span><span>(</span><span>f</span><span>"在 </span><span>{</span><span>DATA_DIR</span><span>}</span><span>/dragon/ 中未找到任何 .png 图像。"</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>dim </span><span>=</span><span> </span></span><span>len</span><span><span>(encoder.</span><span>encode_image</span><span>(image_list[</span></span><span>0</span><span>]))</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>fields </span><span>=</span><span> [</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>FieldSchema</span><span>(</span><span>name</span><span>=</span><span>"id"</span><span>, </span><span>dtype</span><span><span>=</span><span>DataType.</span><span>INT64</span><span>, </span></span><span>is_primary</span><span>=</span><span>True</span><span>, </span><span>auto_id</span><span>=</span><span>True</span><span>),</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>FieldSchema</span><span>(</span><span>name</span><span>=</span><span>"vector"</span><span>, </span><span>dtype</span><span><span>=</span><span>DataType.</span><span>FLOAT_VECTOR</span><span>, </span></span><span>dim</span><span><span>=</span><span>dim),</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>FieldSchema</span><span>(</span><span>name</span><span>=</span><span>"image_path"</span><span>, </span><span>dtype</span><span><span>=</span><span>DataType.</span><span>VARCHAR</span><span>, </span></span><span>max_length</span><span>=</span><span>512</span><span>),</span></div></div><div><div><div>10</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 创建集合 Schema</span></div></div><div><div><div>2</div></div><div><span><span>schema </span><span>=</span><span> </span><span>CollectionSchema</span><span>(fields, </span></span><span>description</span><span>=</span><span>"多模态图文检索"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>"Schema 结构:"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(schema)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>输出为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Schema 结构:</span></div></div><div><div><div>2</div></div><div><span>{'auto_id': True, 'description': '多模态图文检索', 'fields': [</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>{'name': 'id', 'description': '', 'type': &lt;DataType.INT64: 5&gt;, 'is_primary': True, 'auto_id': True},</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>{'name': 'vector', 'description': '', 'type': &lt;DataType.FLOAT_VECTOR: 101&gt;, 'params': {'dim': 768}},</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>{'name': 'image_path', 'description': '', 'type': &lt;DataType.VARCHAR: 21&gt;, 'params': {'max_length': 512}}</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>],</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>'enable_dynamic_field': False, 'enable_namespace': False</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>上面的输出详细展示了刚刚创建的 <code>multimodal_demo Collection</code> Schema 的完整结构。其包含了三个核心字段（Field）：一个自增的 <code>id</code> 作为主键，一个 768 维的 <code>vector</code> 向量字段用于存储图像嵌入，以及一个 <code>image_path</code> 标量字段来记录原始图片路径。</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 创建集合</span></div></div><div><div><div>2</div></div><div><span><span>milvus_client.</span><span>create_collection</span><span>(</span></span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>, </span></span><span>schema</span><span><span>=</span><span>schema)</span></span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>f</span><span>"成功创建 Collection: '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>'"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>"Collection 结构:"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>print</span><span><span>(milvus_client.</span><span>describe_collection</span><span>(</span></span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>))</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>输出为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>成功创建 Collection: 'multimodal_demo'</span></div></div><div><div><div>2</div></div><div><span>Collection 结构:</span></div></div><div><div><div>3</div></div><div><span>{'collection_name': 'multimodal_demo', 'auto_id': True, 'num_shards': 1, 'description': '', 'fields': [{'field_id': 0, 'name': 'id', 'description': '', 'type': &lt;DataType.INT64: 5&gt;, 'params': {}, 'auto_id': True, 'is_primary': True}, {'field_id': 0, 'name': 'vector', 'description': '', 'type': &lt;DataType.FLOAT_VECTOR: 101&gt;, 'params': {'dim': 768}}, {'field_id': 0, 'name': 'image_path', 'description': '', 'type': &lt;DataType.VARCHAR: 21&gt;, 'params': {'max_length': 512}}], 'functions': [], 'aliases': [], 'collection_id': 0, 'consistency_level': 0, 'consistency_level_name': 'Strong', 'properties': {}, 'num_partitions': 1, 'enable_dynamic_field': False, 'enable_namespace': False}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>这里则是 <code>multimodal_demo Collection</code> 的完整结构。</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 5. 准备并插入数据</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>--&gt; 正在向 '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>' 插入数据"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>data_to_insert </span><span>=</span><span> []</span></span></div></div><div><div><div>4</div></div><div><span>for</span><span> image_path </span><span>in</span><span><span> </span><span>tqdm</span><span>(image_list, </span></span><span>desc</span><span>=</span><span>"生成图像嵌入"</span><span>):</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>vector </span><span>=</span><span> encoder.</span><span>encode_image</span><span>(image_path)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>data_to_insert.</span><span>append</span><span>({</span><span>"vector"</span><span>: vector, </span><span>"image_path"</span><span>: image_path})</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>if</span><span> data_to_insert:</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>result </span><span>=</span><span> milvus_client.</span><span>insert</span><span>(</span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>, </span></span><span>data</span><span><span>=</span><span>data_to_insert)</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"成功插入 </span><span>{</span><span>result[</span><span>'insert_count'</span><span>]</span><span>}</span><span> 条数据。"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>输出为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>生成图像嵌入: 100%|██████████| 7/7 [00:13&lt;00:00,  1.92s/it]</span></div></div><div><div><div>2</div></div><div><span>成功插入 7 条数据。</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 6. 创建索引</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>--&gt; 正在为 '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>' 创建索引"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>index_params </span><span>=</span><span> milvus_client.</span><span>prepare_index_params</span><span>()</span></span></div></div><div><div><div>4</div></div><div><span><span>index_params.</span><span>add_index</span><span>(</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>field_name</span><span>=</span><span>"vector"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>index_type</span><span>=</span><span>"HNSW"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>metric_type</span><span>=</span><span>"COSINE"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>params</span><span><span>=</span><span>{</span></span><span>"M"</span><span>: </span><span>16</span><span>, </span><span>"efConstruction"</span><span>: </span><span>256</span><span>}</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div><span><span>milvus_client.</span><span>create_index</span><span>(</span></span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>, </span></span><span>index_params</span><span><span>=</span><span>index_params)</span></span></div></div><div><div><div>11</div></div><div><span>print</span><span>(</span><span>"成功为向量字段创建 HNSW 索引。"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>print</span><span>(</span><span>"索引详情:"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>print</span><span><span>(milvus_client.</span><span>describe_index</span><span>(</span></span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>, </span></span><span>index_name</span><span>=</span><span>"vector"</span><span>))</span></div></div><div><div><div>14</div></div><div><span><span>milvus_client.</span><span>load_collection</span><span>(</span></span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>)</span></span></div></div><div><div><div>15</div></div><div><span>print</span><span>(</span><span>"已加载 Collection 到内存中。"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p>输出为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>--&gt; 正在为 'multimodal_demo' 创建索引</span></div></div><div><div><div>2</div></div><div><span>成功为向量字段创建 HNSW 索引。</span></div></div><div><div><div>3</div></div><div><span>索引详情:</span></div></div><div><div><div>4</div></div><div><span>{'index_type': 'HNSW', 'metric_type': 'COSINE', 'field_name': 'vector', 'index_name': 'vector', 'total_rows': 7, 'indexed_rows': 7, 'pending_index_rows': 0, 'state': 'Finished'}</span></div></div><div><div><div>5</div></div><div><span>已加载 Collection 到内存中。</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>可以看出，索引创建成功，在 <code>vector</code> 字段上成功创建了 <code>HNSW</code> 索引，并使用 <code>COSINE</code> 作为距离度量。<code>M: '16'</code> 和 <code>efConstruction: '256'</code> 是 <code>HNSW</code> 索引的两个关键参数，分别控制着图中每个节点的最大连接数和索引构建时的搜索范围，这些参数直接影响检索的性能和准确性。<code>state: 'Finished'</code> 状态表明索引已成功构建。</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 7. 执行多模态检索</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>--&gt; 正在 '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>' 中执行检索"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>query_image_path </span><span>=</span><span> os.path.</span><span>join</span><span>(</span><span>DATA_DIR</span><span>, </span></span><span>"dragon"</span><span>, </span><span>"query.png"</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>query_text </span><span>=</span><span> </span></span><span>"一条龙"</span></div></div><div><div><div>5</div></div><div><span><span>query_vector </span><span>=</span><span> encoder.</span><span>encode_query</span><span>(</span></span><span>image_path</span><span><span>=</span><span>query_image_path, </span></span><span>text</span><span><span>=</span><span>query_text)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>search_results </span><span>=</span><span> milvus_client.</span><span>search</span><span>(</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>,</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>data</span><span><span>=</span><span>[query_vector],</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>output_fields</span><span><span>=</span><span>[</span></span><span>"image_path"</span><span>],</span></div></div><div><div><div>11</div></div><div><span>    </span><span>limit</span><span>=</span><span>5</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>search_params</span><span><span>=</span><span>{</span></span><span>"metric_type"</span><span>: </span><span>"COSINE"</span><span>, </span><span>"params"</span><span>: {</span><span>"ef"</span><span>: </span><span>128</span><span>}}</span></div></div><div><div><div>13</div></div><div><span>)[</span><span>0</span><span>]</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>retrieved_images </span><span>=</span><span> []</span></span></div></div><div><div><div>16</div></div><div><span>print</span><span>(</span><span>"检索结果:"</span><span>)</span></div></div><div><div><div>17</div></div><div><span>for</span><span> i, hit </span><span>in</span><span> </span><span>enumerate</span><span>(search_results):</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"  Top </span><span>{</span><span><span>i</span><span>+</span></span><span>1}</span><span>: ID=</span><span>{</span><span>hit[</span><span>'id'</span><span>]</span><span>}</span><span>, 距离=</span><span>{</span><span>hit[</span><span>'distance'</span><span>]</span><span>:.4f</span><span>}</span><span>, 路径='</span><span>{</span><span>hit[</span><span>'entity'</span><span>][</span><span>'image_path'</span><span>]</span><span>}</span><span>'"</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>retrieved_images.</span><span>append</span><span>(hit[</span><span>'entity'</span><span>][</span><span>'image_path'</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p>输出为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>--&gt; 正在 'multimodal_demo' 中执行检索</span></div></div><div><div><div>2</div></div><div><span>检索结果:</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Top 1: ID=6, 距离=0.0534, 路径='../../data/C3/dragon/query.png'</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Top 2: ID=1, 距离=0.2557, 路径='../../data/C3/dragon/dragon02.png'</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>Top 3: ID=4, 距离=0.3149, 路径='../../data/C3/dragon/dragon06.png'</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>Top 4: ID=2, 距离=0.3951, 路径='../../data/C3/dragon/dragon03.png'</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>Top 5: ID=3, 距离=0.4640, 路径='../../data/C3/dragon/dragon05.png'</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 8. 可视化与清理</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>--&gt; 正在可视化结果并清理资源"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>if</span><span> </span><span>not</span><span> retrieved_images:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>"没有检索到任何图像。"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>else</span><span>:</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>panoramic_image </span><span>=</span><span> </span><span>visualize_results</span><span>(query_image_path, retrieved_images)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>combined_image_path </span><span>=</span><span> os.path.</span><span>join</span><span>(</span><span>DATA_DIR</span><span>, </span><span>"search_result.png"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>cv2.</span><span>imwrite</span><span>(combined_image_path, panoramic_image)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"结果图像已保存到: </span><span>{</span><span>combined_image_path</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>Image.</span><span>open</span><span>(combined_image_path).</span><span>show</span><span>()</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>milvus_client.</span><span>release_collection</span><span>(</span></span><span>collection_name</span><span><span>=</span><span>COLLECTION_NAME</span><span>)</span></span></div></div><div><div><div>13</div></div><div><span>print</span><span>(</span><span>f</span><span>"已从内存中释放 Collection: '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>'"</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>milvus_client.</span><span>drop_collection</span><span>(</span><span>COLLECTION_NAME</span><span>)</span></span></div></div><div><div><div>15</div></div><div><span>print</span><span>(</span><span>f</span><span>"已删除 Collection: '</span><span>{</span><span>COLLECTION_NAME</span><span>}</span><span>'"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p>输出为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>--&gt; 正在可视化结果并清理资源</span></div></div><div><div><div>2</div></div><div><span>结果图像已保存到: ../../data/C3/search_result.png</span></div></div><div><div><div>3</div></div><div><span>已从内存中释放 Collection: 'multimodal_demo'</span></div></div><div><div><div>4</div></div><div><span>已删除 Collection: 'multimodal_demo'</span></div></div></code></pre><div><div></div><div></div></div></figure></div>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-06/</id>
      <title type="text">CH3_03_llamaindex_vector</title>
      <published>2026-06-28T00:00:00.000Z</published>
      <updated>2026-06-28T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-06/"/>
      <summary type="text">实现对 LlamaIndex 存储数据的加载和相似性搜索</summary>
      <content type="html"><![CDATA[<section><h1>1. LlamaIndex 示例<a href="#1-llamaindex-示例"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> llama_index.core </span><span>import</span><span> VectorStoreIndex, Document, Settings</span></div></div><div><div><div>2</div></div><div><span>from</span><span> llama_index.embeddings.huggingface </span><span>import</span><span> HuggingFaceEmbedding</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>llama_index.core</code> ：这是 LlamaIndex 的核心基础包，这里我们导入了三个核心组件：
<ul>
<li><code>VectorStoreIndex</code> ：向量索引类。它是 LlamaIndex 的入口核心，接收文档 → 自动调用嵌入模型转向量 → 构建可检索的索引。类似 LangChain 的 <code>FAISS.from_documents()</code>；</li>
<li><code>Document</code> ：文档数据类。与 LangChain 的 <code>Document</code> 类似，封装文本内容和元数据。注意这里用法是 <code>Document(text=t)</code>，而 LangChain 是 <code>Document(page_content=t)</code>；</li>
<li><code>Settings</code> ：全局配置对象。用来设置整个 LlamaIndex 的共享配置，比如嵌入模型、LLM、分块大小等。一次设置，全局生效。</li>
</ul>
</li>
<li><code>llama_index.embeddings.huggingface</code> ：Hugging Face 嵌入模型的集成包：
<ul>
<li><code>HuggingFaceEmbedding</code> ：将 Hugging Face 上的 Embedding 模型封装成 LlamaIndex 统一的嵌入接口。用法与 LangChain 版的 <code>HuggingFaceEmbeddings</code> 类似，只需指定 <code>model_name</code>.</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 配置全局嵌入模型</span></div></div><div><div><div>2</div></div><div><span><span>Settings.embed_model </span><span>=</span><span> </span><span>HuggingFaceEmbedding</span><span>(</span></span><span>"BAAI/bge-small-zh-v1.5"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 2. 创建示例文档</span></div></div><div><div><div>2</div></div><div><span><span>texts </span><span>=</span><span> [</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>"张三是法外狂徒"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"LlamaIndex是一个用于构建和查询私有或领域特定数据的框架。"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"它提供了数据连接、索引和查询接口等工具。"</span></div></div><div><div><div>6</div></div><div><span>]</span></div></div><div><div><div>7</div></div><div><span><span>docs </span><span>=</span><span> [</span><span>Document</span><span>(</span></span><span>text</span><span><span>=</span><span>t) </span></span><span>for</span><span> t </span><span>in</span><span> texts]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 3. 创建索引并持久化到本地</span></div></div><div><div><div>2</div></div><div><span><span>index </span><span>=</span><span> VectorStoreIndex.</span><span>from_documents</span><span>(docs)</span></span></div></div><div><div><div>3</div></div><div><span><span>persist_path </span><span>=</span><span> </span></span><span>"./llamaindex_index_store"</span></div></div><div><div><div>4</div></div><div><span><span>index.storage_context.</span><span>persist</span><span>(</span></span><span>persist_dir</span><span><span>=</span><span>persist_path)</span></span></div></div><div><div><div>5</div></div><div><span>print</span><span>(</span><span>f</span><span>"LlamaIndex 索引已保存至: </span><span>{</span><span>persist_path</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>LlamaIndex 默认会将数据存储为透明可读的 JSON 格式，运行上述代码会创建以下文件：</p><hr /><section><h2>1.1 文件总览<a href="#11-文件总览"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>llamaindex_index_store/</span></div></div><div><div><div>2</div></div><div><span>├── default__vector_store.json   (35 KB)   ← 核心：向量数据</span></div></div><div><div><div>3</div></div><div><span>├── docstore.json                (3.7 KB)  ← 文档原文</span></div></div><div><div><div>4</div></div><div><span>├── index_store.json             (499 B)   ← 索引元信息</span></div></div><div><div><div>5</div></div><div><span>├── graph_store.json             (18 B)    ← 图关系（空）</span></div></div><div><div><div>6</div></div><div><span>└── image__vector_store.json     (72 B)    ← 图片向量（空）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h2>1.2 逐文件分析<a href="#12-逐文件分析"><span>#</span></a></h2><section><h3>1️⃣ docstore.json — 文档原文存储 ⭐️ 最直观<a href="#1️⃣-docstorejson--文档原文存储-️-最直观"><span>#</span></a></h3><p>这是我们输入的 <strong>3 段原始文本</strong>：</p>

<table><thead><tr><th>Node ID</th><th>文档内容</th></tr></thead><tbody><tr><td><code>fd58b5...</code></td><td><strong>张三是法外狂徒</strong></td></tr><tr><td><code>ee8dc7...</code></td><td><strong>LlamaIndex是一个用于构建和查询私有或特定数据的框架。</strong></td></tr><tr><td><code>62bfb2...</code></td><td><strong>它提供数据连接、索引和查询接口等工具。</strong></td></tr></tbody></table><p>除了文本，它还记录每个文档的 <code>doc_hash</code>（哈希值校验）和 <code>ref_doc_id</code>（父子文档关系）。</p><hr /></section><section><h3>2️⃣ default__vector_store.json — 向量存储 ⭐️ 核心<a href="#2️⃣-default__vector_storejson--向量存储-️-核心"><span>#</span></a></h3><p>存储了 <strong>3 个节点</strong>的嵌入向量，每个向量：</p>

<table><thead><tr><th>属性</th><th>值</th></tr></thead><tbody><tr><td>维度</td><td><strong>512 维</strong>（由 bge-small-zh-v1.5 模型决定）</td></tr><tr><td>数据类型</td><td>浮点数数组</td></tr><tr><td>节点数</td><td>3 个（三句话各一个）</td></tr></tbody></table><p>例如第一句”张三是法外狂徒”的向量前 5 个值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[-0.0185, -0.0261, -0.0099, -0.0121, -0.0539, ...]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>3️⃣ index_store.json — 索引元信息<a href="#3️⃣-index_storejson--索引元信息"><span>#</span></a></h3><p>记录索引的结构信息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"index_id"</span><span>: </span><span>"5edfdf61-..."</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"nodes_dict"</span><span>: {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"fd58b5..."</span><span>: </span><span>"fd58b5..."</span><span>,   </span><span>// 节点 ID → 自身</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"ee8dc7..."</span><span>: </span><span>"ee8dc7..."</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"62bfb2..."</span><span>: </span><span>"62bfb2..."</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>作用是把<strong>向量存储</strong>和<strong>文档存储</strong>关联起来。</p><hr /></section><section><h3>4️⃣ graph_store.json — 图关系存储（空）<a href="#4️⃣-graph_storejson--图关系存储空"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"graph_dict"</span><span>: {}}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为本示例没有使用知识图谱功能，所以为空。</p></section><section><h3>5️⃣ image__vector_store.json — 图片向量存储（空）<a href="#5️⃣-image__vector_storejson--图片向量存储空"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"embedding_dict"</span><span>: {}, </span><span>"text_id_to_ref_doc_id"</span><span>: {}, </span><span>"metadata_dict"</span><span>: {}}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为没有处理图片，所以为空。</p><hr /></section></section><section><h2>1.3 完整数据流转示意图<a href="#13-完整数据流转示意图"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>你输入的3段文本                    LlamaIndex 的持久化</span></div></div><div><div><div>2</div></div><div><span>┌─────────────────────┐</span></div></div><div><div><div>3</div></div><div><span>│ "张三是法外狂徒"     │ ──→  docstore.json      (原始文本)</span></div></div><div><div><div>4</div></div><div><span>├─────────────────────┤</span></div></div><div><div><div>5</div></div><div><span>│ "LlamaIndex是..."    │ ──→  default__vector_store.json  (512维向量)</span></div></div><div><div><div>6</div></div><div><span>├─────────────────────┤</span></div></div><div><div><div>7</div></div><div><span>│ "它提供数据连接..."   │ ──→  index_store.json   (关联映射)</span></div></div><div><div><div>8</div></div><div><span>└─────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>对比 FAISS（上一节）</strong>：FAISS 用 Pickle 二进制保存（不可读、有安全风险），而 <strong>LlamaIndex 用纯 JSON</strong>，你可以直接用文本编辑器打开查看，安全且透明。</p></section></section>
<section><h1>2. 实现对 LlamaIndex 存储数据的加载和相似性搜索<a href="#2-实现对-llamaindex-存储数据的加载和相似性搜索"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 配置全局嵌入模型</span></div></div><div><div><div>2</div></div><div><span><span>Settings.embed_model </span><span>=</span><span> </span><span>HuggingFaceEmbedding</span><span>(</span></span><span>"BAAI/bge-small-zh-v1.5"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>首先我们需要重新设置嵌入模型。虽然我们在前面介绍 <code>Settings</code> 时提到这是一个全局配置，但它只存在于 <strong>当前运行的 Python 进程中</strong>，不会随 JSON 文件一起持久化。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> llama_index.core </span><span>import</span><span> StorageContext</span></div></div><div><div><div>2</div></div><div><span>from</span><span> llama_index.core </span><span>import</span><span> load_index_from_storage</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 2. 加载存储上下文</span></div></div><div><div><div>5</div></div><div><span><span>storage_context </span><span>=</span><span> StorageContext.</span><span>from_defaults</span><span>(</span></span><span>persist_dir</span><span><span>=</span><span>persist_path)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 3. 从存储上下文加载索引</span></div></div><div><div><div>8</div></div><div><span><span>loaded_vector_store </span><span>=</span><span> </span><span>load_index_from_storage</span><span>(storage_context)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>StorageContext</code> ：存储上下文类，相当于统一管理 5 个 JSON 文件的”总管”。它知道你的 <code>docstore.json</code>、<code>default__vector_store.json</code> 等文件在哪，负责读取和恢复这些数据。可以理解为”打开仓库大门的钥匙”；</li>
<li><code>load_index_from_storage</code> ：从存储恢复索引的函数，接收 <code>StorageContext</code>，把 JSON 文件中的数据”还原”成一个可用的 <code>VectorStoreIndex</code> 对象。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 4. 执行相似度搜索</span></div></div><div><div><div>2</div></div><div><span><span>retriever </span><span>=</span><span> loaded_vector_store.</span><span>as_retriever</span><span>(</span></span><span>similarity_top_k</span><span>=</span><span>2</span><span>) </span><span># 设置返回相似度最高的前2条结果</span></div></div><div><div><div>3</div></div><div><span><span>query </span><span>=</span><span> </span></span><span>"什么是LlamaIndex？"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>nodes </span><span>=</span><span> retriever.</span><span>retrieve</span><span>(query)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>as_retriever</code> ：转化为检索器，将 <code>loaded_vector_store</code> 索引转化为检索器模式，等待接收查询语句，返回值类型为 <code>VectorIndexRetriever</code>；</li>
<li><code>retrieve</code> ：执行检索，返回值为 <code>List[NodeWithScore]</code>：
<ul>
<li>每个元素包含 <code>.text</code>（匹配文本）和 <code>.score</code>（相似度分数）；</li>
<li>作用是把查询语句用嵌入模型转成向量 → 去向量库中做相似度匹配 → 返回最相似的文档节点。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>查询: '</span><span>{</span><span>query</span><span>}</span><span>'"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"相似度最高的文档:"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>for</span><span> doc </span><span>in</span><span> nodes:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"- </span><span>{</span><span><span>doc.</span><span>get_text</span><span>()</span></span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-11/</id>
      <title type="text">11_bert_head_pooler_output</title>
      <published>2026-06-25T00:00:00.000Z</published>
      <updated>2026-06-25T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-11/"/>
      <summary type="text">动手写 BERT 系列笔记第十一节</summary>
      <content type="html"><![CDATA[<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer, BertModel</span></div></div><div><div><div>2</div></div><div><span>import</span><span> torch</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>1. load model and tokenize<a href="#1-load-model-and-tokenize"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_type </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>bert </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_type)</span></span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_type)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>text </span><span>=</span><span> </span></span><span>'This is a text sentence.'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div><div><div><div>2</div></div><div><span>inputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[ 101, 2023, 2003, 1037, 3793, 6251, 1012,  102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer.</span><span>convert_ids_to_tokens</span><span>(inputs[</span></span><span>'input_ids'</span><span>][</span><span>0</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['[CLS]', 'this', 'is', 'a', 'text', 'sentence', '.', '[SEP]']</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h1>2. forward and pooler output<a href="#2-forward-and-pooler-output"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>bert.</span><span>eval</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>27</div></div><div><span><span>          </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>(output): BertOutput(</span></div></div><div><div><div>30</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>outputs </span><span>=</span><span> </span><span>bert</span><span>(**inputs)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>outputs.</span><span>keys</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>odict_keys(['last_hidden_state', 'pooler_output'])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为我们没有像在 10 节中那样，往 <code>bert = BertModel.from_pretrained(model_type)</code> 里面传入 <code>output_hidden_states=True</code> 的参数，所以这里只返回两个 keys，没有 <code>hidden_states</code>（参见 10 节）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>'last_hidden_state'</span><span>].shape </span><span># torch.Size([1, 8, 768])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>outputs[</span><span>'pooler_output'</span><span>].shape </span><span># torch.Size([1, 768])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>outputs[</span><span>'pooler_output'</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[-0.9320, -0.4660, -0.7054,  0.8013,  0.5395, -0.2326,  0.8985,  0.3100,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>-0.5941, -1.0000, -0.1055,  0.8209,  0.9858,  0.2604,  0.9526, -0.6847,</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>-0.2648, -0.6366,  0.3471, -0.7114,  0.6518,  0.9998,  0.4752,  0.3604,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>0.5276,  0.9401, -0.6821,  0.9463,  0.9627,  0.7596, -0.7972,  0.2211,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>-0.9909, -0.2564, -0.7488, -0.9928,  0.4347, -0.8085, -0.0717, -0.0137,</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>-0.9286,  0.3592,  1.0000, -0.4739,  0.2884, -0.4205, -1.0000,  0.3234,</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>-0.9190,  0.7232,  0.6715,  0.5167,  0.2301,  0.5236,  0.5461, -0.0622,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>-0.0630,  0.1883, -0.2802, -0.6607, -0.6670,  0.3900, -0.6133, -0.9413,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>0.5821,  0.5472, -0.1604, -0.3769, -0.1576, -0.0250,  0.9041,  0.2955,</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>-0.0418, -0.8321,  0.3900,  0.3109, -0.6546,  1.0000, -0.5625, -0.9819,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>0.6171,  0.5162,  0.6011,  0.0482,  0.2289, -1.0000,  0.5958, -0.2103,</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>-0.9917,  0.1602,  0.5617, -0.2821,  0.4634,  0.6287, -0.4933, -0.3350,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>-0.4028, -0.5878, -0.2872, -0.2631,  0.1988, -0.3540, -0.3882, -0.4115,</span></div></div><div><div><div>14</div></div><div><span><span>          </span></span><span>0.3234, -0.5114, -0.5085,  0.4941, -0.0114,  0.7357,  0.4615, -0.4198,</span></div></div><div><div><div>15</div></div><div><span><span>          </span></span><span>0.4495, -0.9632,  0.6444, -0.4120, -0.9886, -0.6143, -0.9883,  0.7400,</span></div></div><div><div><div>16</div></div><div><span><span>         </span></span><span>-0.0702, -0.2572,  0.9748, -0.0086,  0.3666, -0.1545, -0.7035, -1.0000,</span></div></div><div><div><div>17</div></div><div><span><span>         </span></span><span>-0.4364, -0.4116, -0.1010, -0.2736, -0.9826, -0.9627,  0.7004,  0.9718,</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>0.2619,  0.9998, -0.3054,  0.9478, -0.1006, -0.4293,  0.0960, -0.5103,</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>0.6878,  0.4061, -0.7764,  0.2234, -0.0699,  0.2507, -0.5001, -0.3151,</span></div></div><div><div><div>20</div></div><div><span><span>         </span></span><span>-0.6120, -0.9422, -0.4599,  0.9538, -0.2553, -0.7623,  0.4071, -0.2945,</span></div></div><div><div><div>21</div></div><div><span><span>         </span></span><span>-0.3870,  0.8860,  0.6171,  0.4231, -0.2888,  0.5307,  0.2569,  0.5667,</span></div></div><div><div><div>22</div></div><div><span><span>         </span></span><span>-0.9075,  0.2098,  0.5000, -0.3142, -0.6503, -0.9822, -0.3632,  0.5577,</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>0.9902,  0.7836,  0.4002,  0.6218, -0.4148,  0.5758, -0.9572,  0.9842,</span></div></div><div><div><div>24</div></div><div><span><span>         </span></span><span>-0.2230,  0.3169, -0.1974,  0.3705, -0.9103,  0.0049,  0.8592, -0.4281,</span></div></div><div><div><div>25</div></div><div><span><span>         </span></span><span>-0.8949, -0.1139, -0.5668, -0.4746, -0.5588,  0.5546, -0.4232, -0.4242,</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>         </span></span><span>-0.0918, -0.4266, -0.3062, -0.8243,  0.8984, -0.4192, -0.5862, -0.5356,</span></div></div><div><div><div>28</div></div><div><span><span>          </span></span><span>0.6628,  0.3908,  0.9998, -0.5771, -0.6544, -0.2762, -0.3847,  0.4352,</span></div></div><div><div><div>29</div></div><div><span><span>         </span></span><span>-0.4021, -1.0000,  0.4762, -0.2384,  0.5032, -0.3292,  0.5334, -0.3008,</span></div></div><div><div><div>30</div></div><div><span><span>         </span></span><span>-0.9849, -0.2677,  0.4999,  0.4842, -0.5571, -0.4787,  0.6036,  0.3051,</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>0.8739,  0.9051, -0.1019,  0.2896,  0.6685, -0.6048, -0.7155,  0.9298]])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>outputs</code> 中 <code>last_hidden_state</code> 的形状为 (1, 8, 768)，其中：</p><ul>
<li>1：对应一个 batch，因为我们只有一个句子；</li>
<li>8：对应 8 个 tokens（在我们的测试句子中，每个词恰好为一个 token）；</li>
<li>768：每个 token 被表示为 768 维的向量。</li>
</ul><p>而 <code>pooler_output</code> 的形状变成了 (1, 768) 就是如同我们在 03 节中所介绍的那样，pooler 层只取出编码器输出中第一个位置 <code>[CLS]</code> 的向量来作为整句话的语义表示</p></section>
<section><h1>3. from scratch<a href="#3-from-scratch"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>my_output </span><span>=</span><span> bert.pooler.</span><span>activation</span><span>(bert.pooler.</span><span>dense</span><span>(outputs[</span></span><span>'last_hidden_state'</span><span>][</span><span>0</span><span>][</span><span>0</span><span>, :]))</span></div></div><div><div><div>2</div></div><div><span>my_output.shape </span><span># torch.Size([768])</span></div></div><div><div><div>3</div></div><div><span>my_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([-0.9320, -0.4660, -0.7054,  0.8013,  0.5395, -0.2326,  0.8985,  0.3100,</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>-0.5941, -1.0000, -0.1055,  0.8209,  0.9858,  0.2604,  0.9526, -0.6847,</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>-0.2648, -0.6366,  0.3471, -0.7114,  0.6518,  0.9998,  0.4752,  0.3604,</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>0.5276,  0.9401, -0.6821,  0.9463,  0.9627,  0.7596, -0.7972,  0.2211,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>-0.9909, -0.2564, -0.7488, -0.9928,  0.4347, -0.8085, -0.0717, -0.0137,</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>-0.9286,  0.3592,  1.0000, -0.4739,  0.2884, -0.4205, -1.0000,  0.3234,</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>-0.9190,  0.7232,  0.6715,  0.5167,  0.2301,  0.5236,  0.5461, -0.0622,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>-0.0630,  0.1883, -0.2802, -0.6607, -0.6670,  0.3900, -0.6133, -0.9413,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>0.5821,  0.5472, -0.1604, -0.3769, -0.1576, -0.0250,  0.9041,  0.2955,</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>-0.0418, -0.8321,  0.3900,  0.3109, -0.6546,  1.0000, -0.5625, -0.9819,</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>0.6171,  0.5162,  0.6011,  0.0482,  0.2289, -1.0000,  0.5958, -0.2103,</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>-0.9917,  0.1602,  0.5617, -0.2821,  0.4634,  0.6287, -0.4933, -0.3350,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>-0.4028, -0.5878, -0.2872, -0.2631,  0.1988, -0.3540, -0.3882, -0.4115,</span></div></div><div><div><div>14</div></div><div><span><span>         </span></span><span>0.3234, -0.5114, -0.5085,  0.4941, -0.0114,  0.7357,  0.4615, -0.4198,</span></div></div><div><div><div>15</div></div><div><span><span>         </span></span><span>0.4495, -0.9632,  0.6444, -0.4120, -0.9886, -0.6143, -0.9883,  0.7400,</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>-0.0702, -0.2572,  0.9748, -0.0086,  0.3666, -0.1545, -0.7035, -1.0000,</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>-0.4364, -0.4116, -0.1010, -0.2736, -0.9826, -0.9627,  0.7004,  0.9718,</span></div></div><div><div><div>18</div></div><div><span><span>         </span></span><span>0.2619,  0.9998, -0.3054,  0.9478, -0.1006, -0.4293,  0.0960, -0.5103,</span></div></div><div><div><div>19</div></div><div><span><span>         </span></span><span>0.6878,  0.4061, -0.7764,  0.2234, -0.0699,  0.2507, -0.5001, -0.3151,</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>-0.6120, -0.9422, -0.4599,  0.9538, -0.2553, -0.7623,  0.4071, -0.2945,</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>-0.3870,  0.8860,  0.6171,  0.4231, -0.2888,  0.5307,  0.2569,  0.5667,</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>-0.9075,  0.2098,  0.5000, -0.3142, -0.6503, -0.9822, -0.3632,  0.5577,</span></div></div><div><div><div>23</div></div><div><span><span>         </span></span><span>0.9902,  0.7836,  0.4002,  0.6218, -0.4148,  0.5758, -0.9572,  0.9842,</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>-0.2230,  0.3169, -0.1974,  0.3705, -0.9103,  0.0049,  0.8592, -0.4281,</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>-0.8949, -0.1139, -0.5668, -0.4746, -0.5588,  0.5546, -0.4232, -0.4242,</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>         </span></span><span>0.6628,  0.3908,  0.9998, -0.5771, -0.6544, -0.2762, -0.3847,  0.4352,</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>-0.4021, -1.0000,  0.4762, -0.2384,  0.5032, -0.3292,  0.5334, -0.3008,</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>-0.9849, -0.2677,  0.4999,  0.4842, -0.5571, -0.4787,  0.6036,  0.3051,</span></div></div><div><div><div>30</div></div><div><span><span>         </span></span><span>0.8739,  0.9051, -0.1019,  0.2896,  0.6685, -0.6048, -0.7155,  0.9298],</span></div></div><div><div><div>31</div></div><div><span><span>       </span></span><span>grad_fn=&lt;TanhBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可见，我们得到的结果与之前 <code>outputs['pooler_output']</code> 的结果一模一样，下面也进行了验证。</p><section><h2>3.1 代码分析<a href="#31-代码分析"><span>#</span></a></h2><p>我们从最内层往外逐层看：</p><hr /><section><h3>第 1 层：<code>outputs['last_hidden_state']</code><a href="#第-1-层outputslast_hidden_state"><span>#</span></a></h3><p>形状 <code>(1, 8, 768)</code>，即 <span><span>batch×seq_len×hidden_sizebatch \times seq\_len \times hidden\_size</span><span><span><span></span><span>ba</span><span>t</span><span>c</span><span>h</span><span></span><span>×</span><span></span></span><span><span></span><span>se</span><span>q</span><span>_</span><span>l</span><span>e</span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>hi</span><span>dd</span><span>e</span><span>n</span><span>_</span><span>s</span><span>i</span><span>z</span><span>e</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>token0   token1   ...  token7</span></div></div><div><div><div>2</div></div><div><span>batch0  [768维]  [768维]  ...  [768维]     ← 只有 1 个 batch</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>第 2 层：<code>[0]</code> — 取第一个 batch<a href="#第-2-层0--取第一个-batch"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>'last_hidden_state'</span><span>][</span><span>0</span><span>]   </span><span># shape: (8, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>去掉 batch 维度，剩下 <span><span>8×7688 \times 768</span><span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span> 的矩阵。</p><hr /></section><section><h3>第 3 层：<code>[0, :]</code> — 取 <code>[CLS]</code> token 的向量<a href="#第-3-层0---取-cls-token-的向量"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>'last_hidden_state'</span><span>][</span><span>0</span><span>][</span><span>0</span><span>, :]   </span><span># shape: (768,)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>          </span></span><span>[CLS]   this     is     ...   [SEP]</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>┌───┐  ┌───┐  ┌───┐       ┌───┐</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>(8,768)  │ ● │  │   │  │   │  ...  │   │  ← [0, :] 取第 0 行</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>└───┘  └───┘  └───┘       └───┘</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>↑</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>[CLS] token 的 768 维向量</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>BERT 的 <code>[CLS]</code> token 设计上就是用来<strong>聚合整个句子的语义信息</strong>的——经过 12 层 Transformer 后，<code>[CLS]</code> 的向量可以作为整句话的”摘要表示”。</p><hr /></section><section><h3>第 4 层：<code>bert.pooler.dense(...)</code> — 线性变换<a href="#第-4-层bertpoolerdense--线性变换"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>bert.pooler.</span><span>dense</span><span>(outputs[</span></span><span>'last_hidden_state'</span><span>][</span><span>0</span><span>][</span><span>0</span><span>, :])</span></div></div><div><div><div>2</div></div><div><span># Linear(768 → 768)，再做一次线性映射</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从模型结构可以看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(pooler): BertPooler(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>4</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>第 5 层：<code>bert.pooler.activation(...)</code> — Tanh 激活<a href="#第-5-层bertpooleractivation--tanh-激活"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>bert.pooler.</span><span>activation</span><span>(</span><span>...</span><span>)   </span></span><span># Tanh，把值压缩到 (-1, 1) 区间</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>3.1.6 最终验证<a href="#316-最终验证"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>torch.</span><span>equal</span><span>(my_output, outputs[</span></span><span>'pooler_output'</span><span>][</span><span>0</span><span>])  </span><span># True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>证明手动算出来的和模型给的 <code>pooler_output</code> 完全一致。</p><hr /></section><section><h3>3.1.7 一句话总结<a href="#317-一句话总结"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs['last_hidden_state']          池化器(pooler) 的处理流程</span></div></div><div><div><div>2</div></div><div><span>┌─────────────────────────┐         ┌──────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span>│ [CLS]  this  is  ...    │         │                              │</span></div></div><div><div><div>4</div></div><div><span>│  ●                        │  ───→  │  Linear(768→768) → Tanh    │  ───→  768 维句子向量</span></div></div><div><div><div>5</div></div><div><span>│                          │         │                              │</span></div></div><div><div><div>6</div></div><div><span>└─────────────────────────┘         └──────────────────────────────┘</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>取 [CLS] token 的向量                  dense + activation</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>(第 0 行, 768 维)                     = BertPooler</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>💡 <code>pooler_output</code> 的本质就是：<strong>取 <code>[CLS]</code> token 的最后一层隐藏状态，再过一层 Linear + Tanh，得到整句话的 768 维”句子嵌入”</strong>（通常用于下游分类任务）。</p></blockquote><p><img src="https://www.mccormickml.com/assets/BERT/CLS_token_500x606.png" alt="" /></p></section></section></section>
<section><h1>4. bert head<a href="#4-bert-head"><span>#</span></a></h1><ul>
<li>default BertModel head</li>
<li>BertForMaskedLM</li>
</ul><section><h2>4.1 <code>BertModel</code> head vs <code>BertForMaskedLM</code><a href="#41-bertmodel-head-vs-bertformaskedlm"><span>#</span></a></h2><p>两者共享<strong>同一个 BERT Encoder（12 层 Transformer）</strong>，区别在于 Encoder 上面加的”头”不同：</p><div><div><div><div><span><p>BertForMaskedLM</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>BERT Encoder<br /><br />12层 Transformer<br /><br />与上面完全相同的权重</p></span></div><div><span><p>last_hidden_state</p></span></div><div><span><p>MLM Head<br /><br />Linear 768→30522 + Softmax</p></span></div><div><span><p>每个位置的词表概率分布<br /><br />预测被 Mask 的词</p></span></div><div><span><p>BertModel</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>BERT Encoder<br /><br />12层 Transformer</p></span></div><div><span><p>last_hidden_state<br /><br />每个 token 的 768 维向量</p></span></div><div><span><p>Pooler<br /><br />取 CLS → Linear → Tanh</p></span></div><div><span><p>pooler_output<br /><br />1个 768 维句子向量</p></span></div>
</div><div><div><span><p>BertForMaskedLM</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>BERT Encoder<br /><br />12层 Transformer<br /><br />与上面完全相同的权重</p></span></div><div><span><p>last_hidden_state</p></span></div><div><span><p>MLM Head<br /><br />Linear 768→30522 + Softmax</p></span></div><div><span><p>每个位置的词表概率分布<br /><br />预测被 Mask 的词</p></span></div><div><span><p>BertModel</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>BERT Encoder<br /><br />12层 Transformer</p></span></div><div><span><p>last_hidden_state<br /><br />每个 token 的 768 维向量</p></span></div><div><span><p>Pooler<br /><br />取 CLS → Linear → Tanh</p></span></div><div><span><p>pooler_output<br /><br />1个 768 维句子向量</p></span></div>
</div></div></div><hr /><section><h3>4.1.1 Default <code>BertModel</code> head<a href="#411-default-bertmodel-head"><span>#</span></a></h3><p>就是本笔记一直在用的——<code>pooler</code>（BertPooler）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>last_hidden_state (1, 8, 768)</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>▼ 取 [CLS] 位置</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>(768,) 向量</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>▼ Linear(768→768) + Tanh</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>pooler_output (1, 768)  ← 整个句子的"摘要向量"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>作为”基础模型”，<code>BertModel</code> 没有内置任何任务头，只提供一个 <code>pooler</code> 用于获取句子级表示。下游任务（如分类）需要你自己在它上面加分类层。</p></blockquote><hr /></section><section><h3>4.1.2 <code>BertForMaskedLM</code><a href="#412-bertformaskedlm"><span>#</span></a></h3><p>在 Encoder 之上加了一个 <strong>MLM Head</strong>——一个 Linear 层，把每个 token 的 768 维映射到 30522 维（词表大小）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 伪代码</span></div></div><div><div><div>2</div></div><div><span><span>last_hidden_state </span><span>=</span><span> </span><span>bert_encoder</span><span>(input_ids)   </span></span><span># (batch, seq_len, 768)</span></div></div><div><div><div>3</div></div><div><span><span>logits </span><span>=</span><span> </span><span>mlm_head</span><span>(last_hidden_state)           </span></span><span># (batch, seq_len, 30522)</span></div></div><div><div><div>4</div></div><div><span># 对每个位置的预测 = argmax(logits, dim=-1)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 BERT <strong>预训练任务</strong>之一——随机遮住一些词，让模型预测它们。我们在 02 节中介绍过，并且将在 12 节中深入探索。</p><hr /></section><section><h3>4.1.3 与这篇笔记的联系<a href="#413-与这篇笔记的联系"><span>#</span></a></h3>

<table><thead><tr><th>本笔记的内容</th><th>关联</th></tr></thead><tbody><tr><td><code>outputs['last_hidden_state']</code></td><td>两者都有，是 Encoder 的输出</td></tr><tr><td><code>outputs['pooler_output']</code></td><td><strong>仅 <code>BertModel</code> 有</strong>，BERT 用它做句子分类（如 NSP）</td></tr><tr><td>MLM Head</td><td><code>BertForMaskedLM</code> 用 <code>last_hidden_state</code> 的<strong>每个 token 位置</strong>来预测被 Mask 的词，<strong>不使用</strong> <code>pooler_output</code></td></tr></tbody></table><p>所以，我们目前探索的 <code>pooler_output</code> 只是 BERT <strong>默认的</strong>一个”头”，BERT 还可以接不同的头来干不同的事——MLM head 只是其中一个例子，还有 <code>BertForSequenceClassification</code>（分类头）、<code>BertForQuestionAnswering</code>（问答头）等。</p><blockquote><p>💡 可以把 BERT Encoder 想象成”发动机”，<code>pooler</code>、<code>MLM Head</code>、分类头等是不同车型的”车身”——发动机一样，用途不同。</p></blockquote></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-05/</id>
      <title type="text">CH3_02_langchain_faiss</title>
      <published>2026-06-23T00:00:00.000Z</published>
      <updated>2026-06-23T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-05/"/>
      <summary type="text">使用 LangChain 和 FAISS 完成一个完整的“创建 -&gt; 保存 -&gt; 加载 -&gt; 查询”流程</summary>
      <content type="html"><![CDATA[<p>下面的代码演示了使用 LangChain 和 FAISS 完成一个完整的“创建 -&gt; 保存 -&gt; 加载 -&gt; 查询”流程。</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> langchain_community.vectorstores </span><span>import</span><span><span> </span><span>FAISS</span></span></div></div><div><div><div>2</div></div><div><span>from</span><span> langchain_community.embeddings </span><span>import</span><span> HuggingFaceEmbeddings</span></div></div><div><div><div>3</div></div><div><span>from</span><span> langchain_core.documents </span><span>import</span><span> Document</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>这里我们引入了 LangChain 的强大生态系统：</p>
<ul>
<li><code>langchain_community.vectorstores</code> &amp; <code>FAISS</code> ：这是由社区维护的第三方集成包，<code>FAISS</code> 就是向量数据库的封装类。它将 FAISS 封装为了 LangChain 统一的向量存储接口，提供 <code>from_documents</code>、<code>similarity_search</code>、<code>save_local</code>、<code>load_local</code> 等方法，方便与 LangChain 的其他组件（Chain、Retriever 等）无缝配合。</li>
<li><code>langchain_community.embeddings</code> &amp; <code>HuggingFaceEmbeddings</code> ：这里是将 Hugging Face 上的 Embedding 模型（如我们接下来用的 <code>BAAI/bge-small-zh-v1.5</code>）封装成 LangChain 统一接口。只需指定 <code>model_name</code>，就能把文本转为向量，供 FAISS 存储和检索。</li>
<li><code>langchain_core.documents</code> &amp; <code>Document</code> ：这是 LangChain 的核心基础包，其中 <code>Document</code> 是文档数据类。它是 LangChain 中最基本的数据结构，包含两个字段：<code>page_content</code>（文本内容）和 <code>metadata</code>（元数据字典）。整个 RAG 流程中的数据载体——文本加载、分块、向量化、检索，都以 <code>Document</code> 为单位传递。</li>
</ul>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 示例文本和嵌入模型</span></div></div><div><div><div>2</div></div><div><span><span>texts </span><span>=</span><span> [</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>"张三是法外狂徒"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"FAISS是一个用于高效相似性搜索和密集向量聚类的库。"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"LangChain是一个用于开发由语言模型驱动的应用程序的框架。"</span></div></div><div><div><div>6</div></div><div><span>]</span></div></div><div><div><div>7</div></div><div><span><span>docs </span><span>=</span><span> [</span><span>Document</span><span>(</span></span><span>page_content</span><span><span>=</span><span>t) </span></span><span>for</span><span> t </span><span>in</span><span> texts]</span></div></div><div><div><div>8</div></div><div><span><span>embeddings </span><span>=</span><span> </span><span>HuggingFaceEmbeddings</span><span>(</span></span><span>model_name</span><span>=</span><span>"BAAI/bge-small-zh-v1.5"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 2. 创建向量存储并保存到本地</span></div></div><div><div><div>2</div></div><div><span><span>vectorstore </span><span>=</span><span> </span><span>FAISS</span><span>.</span><span>from_documents</span><span>(docs, embeddings)</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>local_faiss_path </span><span>=</span><span> </span></span><span>"./faiss_index_store"</span></div></div><div><div><div>5</div></div><div><span><span>vectorstore.</span><span>save_local</span><span>(local_faiss_path)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>print</span><span>(</span><span>f</span><span>"FAISS index has been saved to </span><span>{</span><span>local_faiss_path</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<ul>
<li><code>from_documents</code> ：返回一个由 <code>documents</code> 和 <code>embedding</code> 初始化的 VectorStore 对象，接收三个参数：
<ul>
<li><code>documents</code> ：要添加到 VectorStore 的文档列表；</li>
<li><code>embedding</code> ：要使用的嵌入函数；</li>
<li><code>kwargs</code> ：其他关键字参数。</li>
</ul>
</li>
<li><code>save_local</code> ：将 FAISS 索引、文档库和 <code>index_to_docstore_id</code> 保存到磁盘，接收两个参数：
<ul>
<li><code>folder_path</code> ：要保存的文件夹路径；</li>
<li><code>index_name</code> ：用于指定保存的索引文件名。</li>
</ul>
</li>
</ul>
<section><h5>索引创建细节<a href="#索引创建细节"><span>#</span></a></h5><p>通过深入 LangChain 源码，可以发现索引创建是一个分层、解耦的过程，主要涉及以下几个方法的嵌套调用：</p><ol>
<li>
<p><strong><code>from_documents</code> (封装层)</strong>:</p>
<ul>
<li>这是我们直接调用的方法。它的职责很简单：从输入的 <code>Document</code> 对象列表中提取出纯文本内容 (<code>page_content</code>) 和元数据 (<code>metadata</code>)。</li>
<li>然后，它将这些提取出的信息传递给核心的 <code>from_texts</code> 方法。</li>
</ul>
</li>
<li>
<p><strong><code>from_texts</code> (向量化入口)</strong>:</p>
<ul>
<li>这个方法是面向用户的入口。它接收文本列表，并执行关键的第一步：调用 <code>embedding.embed_documents(texts)</code>，将所有文本批量转换为向量。</li>
<li>完成向量化后，它并不直接处理索引构建，而是将生成的向量和其他所有信息（文本、元数据等）传递给一个内部的辅助方法 <code>__from</code>。</li>
</ul>
</li>
<li>
<p><strong><code>__from</code> (构建索引框架)</strong>:</p>
<ul>
<li>一个内部方法，负责搭建 FAISS 向量存储的“空框架”。</li>
<li>它会根据指定的距离策略（默认为 L2 欧氏距离）初始化一个空的 FAISS 索引结构（如 <code>faiss.IndexFlatL2</code>）。</li>
<li>同时，它也准备好了用于存储文档原文的 <code>docstore</code> 和用于连接 FAISS 索引与文档的 <code>index_to_docstore_id</code> 映射。</li>
<li>最后，它调用另一个内部方法 <code>__add</code> 来完成数据的填充。</li>
</ul>
</li>
<li>
<p><strong><code>__add</code> (填充数据)</strong>:</p>
<ul>
<li>真正执行数据添加操作的核心。它接收到向量、文本和元数据后，执行以下关键操作：
<ul>
<li><strong>添加向量</strong>: 将向量列表转换为 FAISS 需要的 <code>numpy</code> 数组，并调用 <code>self.index.add(vector)</code> 将其批量添加到 FAISS 索引中。</li>
<li><strong>存储文档</strong>: 将文本和元数据打包成 <code>Document</code> 对象，存入 <code>docstore</code>。</li>
<li><strong>建立映射</strong>: 更新 <code>index_to_docstore_id</code> 字典，建立起 FAISS 内部的整数 ID（如 0, 1, 2…）到我们文档唯一 ID 的映射关系。</li>
</ul>
</li>
</ul>
</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 3. 加载索引并执行查询</span></div></div><div><div><div>2</div></div><div><span># 加载时需指定相同的嵌入模型，并允许反序列化</span></div></div><div><div><div>3</div></div><div><span><span>loaded_vectorstore </span><span>=</span><span> </span><span>FAISS</span><span>.</span><span>load_local</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>local_faiss_path,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>embeddings,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>allow_dangerous_deserialization</span><span>=</span><span>True</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>load_local</code> ：从磁盘加载 FAISS 索引、文档库和 <code>index_to_docstore_id</code>，接收四个参数：
<ul>
<li><code>folder_path</code> ：要从中加载索引、文档库和 <code>index_to_docstore_id</code> 的文件夹路径；</li>
<li><code>embeddings</code> ：生成查询时使用的嵌入；</li>
<li><code>index_name</code> ：用于保存的特定索引文件名；</li>
<li><code>allow_dangerous_deserialization</code> ：是否允许反序列化数据，这涉及到加载 pickle 文件。恶意攻击者可以修改 pickle 文件，从而传递恶意载荷，导致在您的计算机上执行任意代码。</li>
</ul>
</li>
</ul></section>
<section><h5>序列化与反序列化<a href="#序列化与反序列化"><span>#</span></a></h5><p><strong>序列化</strong>（Serialization）：把内存中的对象（比如 Python 的列表、字典、自定义类实例）转换成可以存储或传输的格式（比如二进制文件、JSON 字符串）。</p><p><strong>反序列化</strong>（Deserialization）：反过来，把存储的二进制数据恢复成内存中的对象。</p><p>当我们调用 <code>FAISS.load_local()</code> 时，如上所述，它需要从磁盘恢复三样东西：</p><ul>
<li><code>index.faiss</code> ：内容为 FAISS 向量索引，二进制格式，安全；</li>
<li><code>index.pkl</code> ：文档库 + <code>index_to_docstore_id</code>，Pickle 格式，<strong>有风险</strong>。</li>
</ul><p>之所以说 <code>index.pkl</code> 是危险的，是因为 Pickle 反序列化<strong>可以执行任意代码</strong>。考虑一个恶意构造的 pickle 文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 攻击者构造的恶意 pickle 数据</span></div></div><div><div><div>2</div></div><div><span>import</span><span> pickle</span></div></div><div><div><div>3</div></div><div><span>import</span><span> os</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 在反序列化时自动执行系统命令</span></div></div><div><div><div>6</div></div><div><span>class</span><span><span> </span><span>Malicious</span></span><span>:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>def</span><span> </span><span>__reduce__</span><span>(</span><span>self</span><span>):</span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span> (os.system, (</span><span>"rm -rf /重要数据"</span><span>,))</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>with</span><span> </span><span>open</span><span>(</span><span>"index.pkl"</span><span>, </span><span>"wb"</span><span>) </span><span>as</span><span> f:</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>pickle.</span><span>dump</span><span>(</span><span>Malicious</span><span>(), f)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，当我们用 <code>pickle.load()</code> 加载它时，这段恶意代码就会在我们的电脑上执行。相当于你捡了一个 U 盘插到自己电脑上，如果 U 盘里有病毒，你就中招了。</p><p>这里我们传入 <code>allow_dangerous_deserialization=True</code>，是因为这里的 <code>.pkl</code> 文件是我们刚刚自己保存的，来源可信，所以安全。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 执行相似性搜索</span></div></div><div><div><div>2</div></div><div><span><span>query </span><span>=</span><span> </span></span><span>"FAISS是做什么的？"</span></div></div><div><div><div>3</div></div><div><span><span>results </span><span>=</span><span> loaded_vectorstore.</span><span>similarity_search</span><span>(query, </span></span><span>k</span><span>=</span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>similarity_search</code> ：返回最符合 query 的 docs，接收四个参数：
<ul>
<li><code>query</code> ：要查找相似 doc 的文本；</li>
<li><code>k</code> ：要返回 docs 的数量，默认为 4；</li>
<li><code>filter</code> ：可选，按元数据过滤，默认为 None；</li>
<li><code>fetch_k</code> ：可选，过滤前要获取的 docs 数量，默认为 20.</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>查询: '</span><span>{</span><span>query</span><span>}</span><span>'"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"相似度最高的文档:"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>for</span><span> doc </span><span>in</span><span> results:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"- </span><span>{</span><span>doc.page_content</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-10/</id>
      <title type="text">10_add_&amp;_norm_residual_conn</title>
      <published>2026-06-22T00:00:00.000Z</published>
      <updated>2026-06-22T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-10/"/>
      <summary type="text">动手写 BERT 系列笔记第十节</summary>
      <content type="html"><![CDATA[<p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/2d3dc8c29d142805229860bf5ec80fa1.png" alt="" /></p>
<p><strong>残差连接与层归一化</strong></p>
<ul>
<li>在 Transformer 的每个编码器和解码器层中，所有子模块（如多头注意力和前馈网络）都被一个 <code>Add &amp; Norm</code> 操作包裹。这个组合是为了保证 Transformer 能够稳定训练。</li>
<li>这个操作由两个部分组成：
<ul>
<li><strong>残差连接 (Add)</strong>：该操作将子模块的输入 <code>x</code> 直接加到该子模块的输出 <code>Sublayer(x)</code> 上。这一结构解决了深度神经网络中的<strong>梯度消失 (Vanishing Gradients)</strong> 问题。在反向传播时，梯度可以绕过子模块直接向前传播，从而保证了即使网络层数很深，模型也能得到有效的训练。其公式可以表示为：<span><span>Output=x+Sublayer(x)Output=x+Sublayer(x)</span><span><span><span></span><span>O</span><span>u</span><span>tp</span><span>u</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>S</span><span>u</span><span>b</span><span>l</span><span>a</span><span>y</span><span>er</span><span>(</span><span>x</span><span>)</span></span></span></span> .</li>
<li><strong>层归一化 (Norm)</strong>：该操作对单个样本的所有特征进行归一化，使其均值为 0 ，方差为 1 。这解决了模型训练过程中的<strong>内部协变量偏移 (Internal Covariate Shift)</strong> 问题，使每一层的输入分布保持稳定，从而加速模型收敛并提高训练的稳定性。</li>
</ul>
</li>
</ul>
<p>我们下面从代码层面看看 add&amp;norm 的实现。</p>
<section><h1>0. 初探<a href="#0-初探"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers.models.bert </span><span>import</span><span> BertModel, BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>2</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_name, </span></span><span>output_hidden_states</span><span>=</span><span>True</span><span>) </span><span># 把每一个 layer 的输出都返回出来</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model.config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertConfig {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>"add_cross_attention": false,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>"architectures": [</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>"BertForMaskedLM"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>"attention_probs_dropout_prob": 0.1,</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>"bos_token_id": null,</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>"classifier_dropout": null,</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>"dtype": "float32",</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>"eos_token_id": null,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>"gradient_checkpointing": false,</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>"hidden_act": "gelu",</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>"hidden_dropout_prob": 0.1,</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>"hidden_size": 768,</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>"initializer_range": 0.02,</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>"intermediate_size": 3072,</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>"is_decoder": false,</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>"layer_norm_eps": 1e-12,</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>"max_position_embeddings": 512,</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>"model_type": "bert",</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>"num_attention_heads": 12,</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>"num_hidden_layers": 12,</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>"output_hidden_states": true,</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>"pad_token_id": 0,</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>"position_embedding_type": "absolute",</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>"transformers_version": "5.9.0",</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>"type_vocab_size": 2,</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>"use_cache": true,</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>"vocab_size": 30522</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里我们需要格外关注 <code>intermediate_size</code>，它指的是 BERT 每层中 <strong>Feed-Forward Network（FFN，前馈网络）</strong> 的<strong>内部扩展维度</strong>。</p><hr /><section><h3>0.1 在 Transformer Encoder 中的位置<a href="#01-在-transformer-encoder-中的位置"><span>#</span></a></h3><p>回顾一下 BERT layer 的结构（我们之后也会看到）：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>输入 768维</p></span></div><div><span><p>多头注意力</p></span></div><div><span><p>Add &amp; Norm</p></span></div><div><span><p>🔶 Feed-Forward Network</p></span></div><div><span><p>Add &amp; Norm</p></span></div><div><span><p>输出 768维</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>输入 768维</p></span></div><div><span><p>多头注意力</p></span></div><div><span><p>Add &amp; Norm</p></span></div><div><span><p>🔶 Feed-Forward Network</p></span></div><div><span><p>Add &amp; Norm</p></span></div><div><span><p>输出 768维</p></span></div>
</div></div></div><p><code>intermediate_size</code> 就是 FFN 那个橙色块内部的”膨胀宽度”。</p><hr /></section><section><h3>0.2 FFN 内部的维度变化<a href="#02-ffn-内部的维度变化"><span>#</span></a></h3><p>FFN 由两层全连接网络组成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入 (768)  ──→  扩展层 (3072)  ──→  压缩层 (768)</span></div></div><div><div><div>2</div></div><div><span><span>              </span></span><span>W₁(768→3072)        W₂(3072→768)</span></div></div><div><div><div>3</div></div><div><span><span>              </span></span><span>+ GELU激活函数</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># FFN 的伪代码</span></div></div><div><div><div>2</div></div><div><span>def</span><span> </span><span>feed_forward</span><span>(</span><span>x</span><span>):</span></div></div><div><div><div>3</div></div><div><span>    </span><span># x: (batch, seq_len, 768)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> x </span><span>@</span><span> W1   </span><span># (batch, seq_len, 3072)  ← intermediate_size</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>gelu</span><span>(h)  </span><span># 非线性激活</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>output </span><span>=</span><span> h </span><span>@</span><span> W2  </span><span># (batch, seq_len, 768)  回到 hidden_size</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>0.3 为什么是 3072 = 768 × 4？<a href="#03-为什么是-3072--768--4"><span>#</span></a></h3><p>BERT 的 FFN 使用 <strong>4 倍扩展</strong>的设计：</p>

<table><thead><tr><th></th><th>BERT-base</th><th>通用规律</th></tr></thead><tbody><tr><td><code>hidden_size</code></td><td>768</td><td><span><span>dmodeld_{model}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><code>intermediate_size</code></td><td><strong>3072</strong></td><td><span><span>4×dmodel4 \times d_{model}</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td></tr><tr><td>扩展比</td><td><strong>4x</strong></td><td>4x</td></tr></tbody></table><p>先”膨胀”到 4 倍宽度（给模型更大的容量做非线性变换），再”压缩”回原宽度，这是 Transformer 的标准设计。</p><hr /></section><section><h3>0.4 一张图总结<a href="#04-一张图总结"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>hidden_size = 768               intermediate_size = 3072</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>┌──────────┐                    ┌──────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>│   词向量  │ ── W₁(768×3072) ──→ │    扩展后的中间表示             │</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>│   768维  │                    │    3072维                    │</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>└──────────┘                    └──────────────────────────────┘</span></div></div><div><div><div>6</div></div><div><span><span>                                            </span></span><span>│ GELU 激活</span></div></div><div><div><div>7</div></div><div><span><span>                                            </span></span><span>↓</span></div></div><div><div><div>8</div></div><div><span><span>                                    </span></span><span>┌──────────────────────────────┐</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>┌──────────┐                    │    非线性变换后的结果           │</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>│   输出    │ ←── W₂(3072×768) ── │    3072维                    │</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>│   768维  │                    └──────────────────────────────┘</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>└──────────┘</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>中间膨胀到 4 倍 → 给模型更大的"思考空间" → 然后压缩回原维度</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>💡 通俗理解：<code>intermediate_size</code> 就是 FFN 的”肚子”——先把 768 维撑大到 3072 维来充分运算，再缩回 768 维传给下一层。</p></blockquote><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>31</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>回顾我们在 03 节中介绍过的关于 BERT 模型的架构，我们知道 BertLayer 的架构如下：</p><ul>
<li>attention: BertAttention
<ul>
<li>self: BertSelfAttention</li>
<li>output: BertSelfOutput</li>
</ul>
</li>
<li>intermediate: BertIntermediate, 768 -&gt; <span><span>4×7684\times 768</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></li>
<li>output: BertOutput, <span><span>4×7684\times 768</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span> -&gt; 768</li>
</ul><p>再看看我们一开头给出的 Transformer 架构图，实际上：</p><ul>
<li><code>attention</code> 中的 <code>self</code> 与 <code>output</code> 共同构成了 encoder 中 Multi-Head Attention + Add &amp; Norm 的部分；</li>
<li>而 <code>intermediate</code> 与 <code>output</code> 共同构成了 encoder 中 Feed Forward + Add &amp; Norm 的部分；</li>
</ul><div><div><div></div><div>MLP</div></div><div><p>MLP(Multi-Layer Perceptron，多层感知机)就是由多层全连接层（Linear / Dense）堆叠而成的前馈神经网络，每层之间夹一个非线性激活函数。</p></div></div><p>这里的 <code>intermediate</code> 和 <code>output</code> 实际上是两层 MLP. 我们可以把它们拆开来看：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>第 1 层: BertIntermediate          第 2 层: BertOutput</span></div></div><div><div><div>2</div></div><div><span>┌──────────────────────┐      ┌──────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span>│ Linear(768 → 3072)   │      │ Linear(3072 → 768)        │</span></div></div><div><div><div>4</div></div><div><span>│        ↓              │      │        ↓                  │</span></div></div><div><div><div>5</div></div><div><span>│ GELU 激活函数         │      │ Dropout                   │</span></div></div><div><div><div>6</div></div><div><span>│        ↓              │      │        ↓                  │</span></div></div><div><div><div>7</div></div><div><span>│ 输出: (batch, 3072)  │ ───→ │ Add (残差: + attention_out)│</span></div></div><div><div><div>8</div></div><div><span>└──────────────────────┘      │        ↓                  │</span></div></div><div><div><div>9</div></div><div><span><span>                              </span></span><span>│ LayerNorm                 │</span></div></div><div><div><div>10</div></div><div><span><span>                              </span></span><span>│        ↓                  │</span></div></div><div><div><div>11</div></div><div><span><span>                              </span></span><span>│ 输出: (batch, 768)        │</span></div></div><div><div><div>12</div></div><div><span><span>                              </span></span><span>└──────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                    </span></span><span>MLP（两层）</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>┌─────────────────────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>│                                             │</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>│  attention_out ──→ Linear(768→3072) ──→ GELU ──→ Linear(3072→768) ──→ ...</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>│   (输入 x)        ↑  BertIntermediate  ↑         ↑  BertOutput  ↑</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>│                   └── 第 1 层 ────────┘         └── 第 2 层 ───┘</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>│                                             │</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>└─────────────────────────────────────────────┘</span></div></div><div><div><div>9</div></div><div><span><span>                            </span></span><span>FFN = 两层 MLP</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>FFN 用两层 MLP 的主要原因是，如果只有一层 Linear(768→768)，那就是一个简单的线性变换，没有任何非线性能力。模型学到的只是输入的线性组合，表达能力很弱。</p><p>而如果用 768 -&gt; 3072 -&gt; 768 的两层结构：</p><ol>
<li>先膨胀：768 → 3072，给模型更大的”容量”去学习复杂模式</li>
<li>加非线性：GELU 激活，让模型能学到非线性关系</li>
<li>再压缩：3072 → 768，把信息压缩回原维度，传给下一层</li>
</ol></section></section>
<section><h1>1. model output<a href="#1-model-output"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>test_sentence </span><span>=</span><span> </span></span><span>'this is a test sentence'</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>model_inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(test_sentence, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model.</span><span>eval</span><span>()</span></span></div></div><div><div><div>2</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>outputs </span><span>=</span><span> </span><span>model</span><span>(**model_inputs)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>outputs.</span><span>keys</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>odict_keys(['last_hidden_state', 'pooler_output', 'hidden_states'])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>2</span><span>][</span><span>0</span><span>] </span><span># embeddings layer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1686, -0.2858, -0.3261,  ..., -0.0276,  0.0383,  0.1640],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.6485,  0.6739, -0.0932,  ...,  0.4475,  0.6696,  0.1820],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.6270, -0.0633, -0.3143,  ...,  0.3427,  0.4636,  0.4594],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.6010, -0.6970, -0.2001,  ...,  0.2960,  0.2060, -1.7181],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.8323,  0.2878,  0.0021,  ...,  0.2628, -1.1310, -1.2708],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.1481, -0.2948, -0.1690,  ..., -0.5009,  0.2544, -0.0700]]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关于 BERT 模型的 outputs，我们在 07 节中进行了详尽的探索，此处不再赘述。总之，<code>outputs[2]</code> 表示的是所有 hidden layers 的输出（包括第 0 层 embedding layer 与 12 个 transformer layers）.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>2</span><span>][</span><span>1</span><span>] </span><span># first transformer layer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1556, -0.0080, -0.0707,  ...,  0.0786,  0.0213,  0.0616],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.5333,  0.5799,  0.1044,  ...,  0.0241,  0.4888,  0.0161],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-1.0609, -0.3058, -0.5043,  ...,  0.1874,  0.2874,  0.4032],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.8206, -0.6656, -0.7054,  ...,  0.1347,  0.1117, -1.9040],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 1.1128,  0.6603, -0.1509,  ...,  0.3253, -1.0006, -1.9106],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.0736,  0.0346,  0.0376,  ..., -0.4506,  0.6585, -0.0502]]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看完了我们获得的结果，下面我们来从零把 BERT layer 串起来。</p></section>
<section><h1>2. from scratch<a href="#2-from-scratch"><span>#</span></a></h1><ul>
<li>BertLayer
<ul>
<li>attention: BertAttention
<ul>
<li>self: BertSelfAttention</li>
<li>output: BertSelfOutput</li>
</ul>
</li>
<li>intermediate: BertIntermediate, 768 -&gt; <span><span>4×7684\times 768</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></li>
<li>output: BertOutput, <span><span>4×7684\times 768</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span> -&gt; 768</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>embeddings </span><span>=</span><span> outputs[</span></span><span>2</span><span>][</span><span>0</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>layer </span><span>=</span><span> model.encoder.layer[</span></span><span>0</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>2.1 第一次 add &amp; norm，发生在 MHA 内部<a href="#21-第一次-add--norm发生在-mha-内部"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>MHA_output</span><span> </span><span>=</span><span> layer.attention.</span><span>self</span><span>(embeddings)</span></span></div></div><div><div><div>2</div></div><div><span>MHA_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(tensor([[[ 0.2979,  0.0801, -0.0037,  ..., -0.0142,  0.1290,  0.0828],</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>[ 0.3935,  0.1356, -0.0920,  ...,  0.0211,  0.1677,  0.0011],</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>[ 0.1696,  0.1449, -0.1039,  ...,  0.1604,  0.2172,  0.0310],</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>          </span></span><span>[-0.0617,  0.1968, -0.0669,  ...,  0.1126,  0.1933, -0.0204],</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>[-0.2835,  0.1495, -0.0021,  ...,  0.0973,  0.1865, -0.0636],</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>[ 0.2575,  0.1120, -0.1008,  ...,  0.0175,  0.1508,  0.0878]]],</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>grad_fn=&lt;ViewBackward0&gt;),</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>None)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的 <code>MHA_output</code> 对应的就是架构图中 encoder 第一个 Multi-Head Self-Attention 的输出。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>attention_output </span><span>=</span><span> layer.attention.</span><span>output</span><span>(</span><span>MHA_output</span><span>[</span></span><span>0</span><span>], embeddings) </span><span># 注意力输出 + 输入的残差连接</span></div></div><div><div><div>2</div></div><div><span>attention_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.6143, -0.2061, -0.5682,  ...,  0.2557,  0.1224,  0.3388],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.6751,  1.0598, -0.2210,  ...,  0.6263,  1.1682,  0.1998],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.5969,  0.1463, -0.5549,  ...,  0.5272,  0.5071,  0.6439],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 1.3291, -1.1190, -0.1960,  ...,  0.6164,  0.4293, -2.4704],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 1.8784,  0.5158,  0.1902,  ...,  0.5298, -1.3812, -2.0257],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[ 0.7376, -0.7000, -0.0992,  ..., -0.9356,  0.5831, -0.4610]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;NativeLayerNormBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><code>MHA_output[0]</code></div></div><div><p><code>MHA_output[0]</code> 实际上是一个 tuple，包含两个元素：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>MHA_output = layer.attention.self(embeddings)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># MHA_output = (</span></div></div><div><div><div>4</div></div><div><span>#     tensor([[...]]),   ← [0] 注意力计算结果，形状 (1, seq_len, 768)</span></div></div><div><div><div>5</div></div><div><span>#     None               ← [1] 注意力权重矩阵，None 因为没请求</span></div></div><div><div><div>6</div></div><div><span># )</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div></div><p>这里的 <code>attention_output</code> 就是结构图中经过第一个 Add &amp; Norm 块后的输出。</p></section><section><h2>2.2 第二次 add &amp; norm，发生在 MLP 内部<a href="#22-第二次-add--norm发生在-mlp-内部"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>MLP1</span><span> </span><span>=</span><span> layer.</span><span>intermediate</span><span>(attention_output)</span></span></div></div><div><div><div>2</div></div><div><span><span>MLP1</span><span>.shape </span></span><span># torch.Size([1, 7, 3072])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>MLP2</span><span> </span><span>=</span><span> layer.</span><span>output</span><span>(</span><span>MLP1</span><span>, attention_output) </span></span><span># MLP 输出 + 输入的残差连接</span></div></div><div><div><div>2</div></div><div><span><span>MLP2</span><span>.shape </span></span><span># torch.Size([1, 7, 768])</span></div></div><div><div><div>3</div></div><div><span>MLP2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1556, -0.0080, -0.0707,  ...,  0.0786,  0.0213,  0.0616],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.5333,  0.5799,  0.1044,  ...,  0.0241,  0.4888,  0.0161],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-1.0609, -0.3058, -0.5043,  ...,  0.1874,  0.2874,  0.4032],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.8206, -0.6656, -0.7054,  ...,  0.1347,  0.1117, -1.9040],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 1.1128,  0.6603, -0.1509,  ...,  0.3253, -1.0006, -1.9106],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.0736,  0.0346,  0.0376,  ..., -0.4506,  0.6585, -0.0502]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;NativeLayerNormBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到这里的结果与之前 <code>outputs[2][1]</code> 的结果是一模一样的。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-09/</id>
      <title type="text">09_attention_02</title>
      <published>2026-06-21T00:00:00.000Z</published>
      <updated>2026-06-21T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-09/"/>
      <summary type="text">动手写 BERT 系列笔记第九节</summary>
      <content type="html"><![CDATA[<span><span><span>Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V</span><span><span><span></span><span>A</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span>(</span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span><span>V</span></span></span></span></span>
<section><h1>1. 单头自注意力的数学计算<a href="#1-单头自注意力的数学计算"><span>#</span></a></h1><p>我们首先来看单头自注意力的数学计算过程。</p><p>以句子 “I am good” 为例，设词嵌入维度 <span><span>dmodel=512d_{model}=512</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>512</span></span></span></span>，<span><span>dk=64d_k=64</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span>.</p><div><div><div></div><div><span><span>dmodeld_{model}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与 <span><span>dkd_k</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></div></div><div><p><span><span>dmodel=512d_{model}=512</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>512</span></span></span></span> 指的是模型维度 / 词嵌入维度，也就是每个 token（词）用一个多长的向量来表示，也是模型的“隐藏层宽度”。在我们举出的例子中，“I am good” 这三个词，每个词就被表示为一个 512 维的向量：</p><span><span><span>X=[Iamgood]⏟3 个词  ⟶  [x1,1x1,2⋯x1,512x2,1x2,2⋯x2,512x3,1x3,2⋯x3,512]⏟3×dmodel=3×512X = \underbrace{\begin{bmatrix}
\text{I} \\
\text{am} \\
\text{good}
\end{bmatrix}}_{3 \text{ 个词}}
\;\longrightarrow\;
\underbrace{\begin{bmatrix}
x_{1,1} &amp; x_{1,2} &amp; \cdots &amp; x_{1,512} \\
x_{2,1} &amp; x_{2,2} &amp; \cdots &amp; x_{2,512} \\
x_{3,1} &amp; x_{3,2} &amp; \cdots &amp; x_{3,512}
\end{bmatrix}}_{3 \times d_{model} = 3 \times 512}</span><span><span><span></span><span>X</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span><span><span> </span><span>个词</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>I</span></span></span></span><span><span></span><span><span><span>am</span></span></span></span><span><span></span><span><span><span>good</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span>⟶</span><span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span><span>×</span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>=</span><span>3</span><span>×</span><span>512</span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>1</span><span>,</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>2</span><span>,</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>3</span><span>,</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>1</span><span>,</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>2</span><span>,</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>3</span><span>,</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>1</span><span>,</span><span>512</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>2</span><span>,</span><span>512</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>3</span><span>,</span><span>512</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p><span><span>dk=64d_k=64</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 就不同了，它指的是每个注意力头 Key / Query 的维度，它主要决定的事每个注意力头“视野”的精细程度。在我们举出的例子中，通过权重矩阵 <span><span>Wq(512×64)W_q(512\times 64)</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>512</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span><span>)</span></span></span></span>，我们把 512 维的词向量投影到一个更小的 64 维空间来计算注意力：</p><span><span><span>X⋅Wq=[3×512]⏟输入⋅[512×64]⏟Wq=[3×64]⏟Q 矩阵X \cdot W_q = 
\underbrace{\begin{bmatrix}
3 \times 512
\end{bmatrix}}_{\text{输入}}
\cdot
\underbrace{\begin{bmatrix}
512 \times 64
\end{bmatrix}}_{W_q}
=
\underbrace{\begin{bmatrix}
3 \times 64
\end{bmatrix}}_{Q \text{ 矩阵}}</span><span><span><span></span><span>X</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>输入</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span><span></span><span>×</span><span></span><span>512</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>512</span><span></span><span>×</span><span></span><span>64</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>Q</span><span><span> </span><span>矩阵</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span><span></span><span>×</span><span></span><span>64</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>两者的关系为：</p><span><span><span>dk=dmodelhd_k=\frac{d_{model}}{h}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>h</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>在 BERT 模型中，<span><span>dmodel=768d_{model}=768</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>768</span></span></span></span>，而 <span><span>h=12h=12</span><span><span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>，所以 <span><span>dk=768/12=64d_k=768/12=64</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>768/12</span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span>.</p><p>也可以用下图总结：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>d_model = 512 (整个向量的宽度)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>┌──────────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>│ ←  head₁ (d_k=64) → │            │</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>│                      │ ← head₂ → │  ...</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>│            ...       │            │</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>└──────────────────────────────────┘</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>d_k = 每个头分到的一小段 = d_model / h</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div></div><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/c29f8459e8db4006122d153f101b8026.png" alt="" />
<img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/be1ee4ddec131a245ad65d101c2dfb1c.png" alt="" /></p><ol>
<li>计算 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 与 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的转置点积 <span><span>QKTQK^T</span><span><span><span></span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>
<ul>
<li>先通过可学习的权重矩阵 <span><span>Wq,Wk,WvW_q,W_k,W_v</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ，将源序列的词嵌入矩阵 <span><span>XX</span><span><span><span></span><span>X</span></span></span></span> （<span><span>n×dmodel=3×512n\times d_{model}=3\times 512</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>512</span></span></span></span>，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 为序列长度）转化为 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 矩阵（均为 <span><span>n×dkn\times d_k</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）.</li>
<li>计算 <span><span>QKTQK^T</span><span><span><span></span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> ，结果为 <span><span>n×nn\times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的相似度矩阵，<strong>每个值代表某一词的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 与另一词的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的匹配程度</strong>，即词间的语义关联度。</li>
</ul>
</li>
</ol><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/0d1a721b4d1179b40de1e76162a9e52a.png" alt="" />
<img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/afe80ab504660b069468c7e1e56f4ae3.png" alt="" /></p><ol>
<li>缩放，除以 <span><span>dk\sqrt{d_k}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>
<ul>
<li>将相似度矩阵的所有元素除以 <span><span>dk\sqrt{d_k}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> .</li>
<li>目的：防止 <span><span>dkd_k</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 过大时，<span><span>QKTQK^T</span><span><span><span></span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的值过大，导致 softmax 后梯度趋于 0 （梯度消失），<strong>保证训练时梯度的稳定性</strong>。</li>
</ul>
</li>
</ol><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/05eb3c02e9a2a46b05ca0ff4076d4a4c.png" alt="" /></p><ol>
<li>Softmax 归一化
<ul>
<li>对缩放后的相似度矩阵做 Softmax 运算，将每个行的数值归一化到 <strong>0~1 的范围</strong>，且<strong>每行求和为 1</strong> ；</li>
<li>归一化后的数值为<strong>注意力得分</strong>，代表某一词对序列中其他词的 “关注程度”。</li>
</ul>
</li>
</ol><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/2b64bf24de6a3373d2d49af5f2eee700.png" alt="" /></p><ol>
<li>与 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 矩阵相乘生成注意力输出 <span><span>ZZ</span><span><span><span></span><span>Z</span></span></span></span>
<ul>
<li>将 Softmax 后的注意力得分矩阵（<span><span>n×nn×n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span>）与 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 矩阵（<span><span>n×dkn×d_k</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）相乘，得到最终的自注意力输出矩阵 <span><span>ZZ</span><span><span><span></span><span>Z</span></span></span></span>（<span><span>n×dkn×d_k</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）；</li>
<li><span><span>ZZ</span><span><span><span></span><span>Z</span></span></span></span> 中每个词的特征向量，是<strong>原 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 矩阵中所有词的特征向量按注意力得分加权求和</strong>的结果，即融合了序列内所有相关词的语义信息。</li>
</ul>
</li>
</ol><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/b7e5b58b591084733febb76bf4e99321.png" alt="" /></p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/6bf33353e729e44b98432f272cbe2747.png" alt="" /></p><ul>
<li>通过上述自注意力机制，我们就能明白句子中每个词与其他词的关联程度。</li>
</ul><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/2026/05/daea4025e322177b6d4efa7041ea0aef.png" alt="" /></p></section>
<section><h1>2. 多头自注意力<a href="#2-多头自注意力"><span>#</span></a></h1><p>而多头自注意力的计算过程如下：</p><ol>
<li>将 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 通过不同的权重矩阵拆分为 <strong><span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 个注意力头</strong>，每个头独立执行上述四步自注意力计算，得到 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 个注意力输出矩阵 <span><span>Z1,Z2,...,ZhZ_1,Z_2,...,Z_h</span><span><span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> .</li>
<li>将 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 个 <span><span>ZiZ_i</span><span><span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> <strong>按列拼接</strong>为一个大矩阵（<span><span>n×h×dkn\times h\times d_k</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>h</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）.</li>
<li>用一个可学习的权重矩阵 <span><span>W0W_0</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> ​对拼接后的矩阵做线性变换，恢复为与原始词嵌入相同的维度 <span><span>n×dmodeln×d_{model}</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，得到多头自注意力的最终输出。</li>
</ol><p>公式：</p><span><span><span>Multi−headattention=Concatenate(Z1,Z2,...,Zh)W0Multi-head\enspace attention=Concatenate(Z_1,Z_2,...,Z_h)W_0</span><span><span><span></span><span>M</span><span>u</span><span>l</span><span>t</span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>h</span><span>e</span><span>a</span><span>d</span><span></span><span>a</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>o</span><span>n</span><span>c</span><span>a</span><span>t</span><span>e</span><span>na</span><span>t</span><span>e</span><span>(</span><span><span>Z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><blockquote><p>BERT Self-layer 实际上是通过 MHA(Multi Head Attention) 分块矩阵实现的。</p></blockquote><section><h2>2.1 什么是 BERT layer？<a href="#21-什么是-bert-layer"><span>#</span></a></h2><p>一个 <strong>BERT layer</strong>（BERT 层）是 BERT 模型的核心构建单元。BERT 模型由多个这样的层<strong>堆叠</strong>而成（BERT-base 有 12 层，BERT-large 有 24 层）。</p><p>每一层包含两个主要部分：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>输入向量</p></span></div><div><span><p>🔷 Multi-Head Attention<br /><br />多头注意力</p></span></div><div><span><p>相加 + LayerNorm</p></span></div><div><span><p>🔶 Feed-Forward Network<br /><br />前馈网络</p></span></div><div><span><p>相加 + LayerNorm</p></span></div><div><span><p>输出向量</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>输入向量</p></span></div><div><span><p>🔷 Multi-Head Attention<br /><br />多头注意力</p></span></div><div><span><p>相加 + LayerNorm</p></span></div><div><span><p>🔶 Feed-Forward Network<br /><br />前馈网络</p></span></div><div><span><p>相加 + LayerNorm</p></span></div><div><span><p>输出向量</p></span></div>
</div></div></div><p>可以通俗理解：<strong>Attention 负责”看上下文”</strong>（这个词和其他词有什么关系？），<strong>FFN 负责”思考加工”</strong>（把信息做非线性变换）。</p><hr /></section><section><h2>2.2 什么是 MHA（Multi-Head Attention）？<a href="#22-什么是-mhamulti-head-attention"><span>#</span></a></h2><p>MHA 就是<strong>多头注意力</strong>。先理解”单头”：</p><ul>
<li>
<p>单头注意力：一个词只从<strong>一种角度</strong>去看其他词。比如”bank”这个词，可能只看金融角度的关联。</p>
</li>
<li>
<p><strong>多头注意力（MHA）</strong>：同时从<strong>多个角度</strong>去看。比如：</p>
<ul>
<li>🟡 头 1：看语法关系（“bank”和”the”的关系）</li>
<li>🟢 头 2：看语义关系（“bank”和”money”的关系）</li>
<li>🔵 头 3：看指代关系（“bank”和”it”的关系）</li>
<li>… 共 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 个头（BERT 中 <span><span>h=12h=12</span><span><span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>）</li>
</ul>
</li>
</ul><p><strong>实现方式</strong>：每个头有自己独立的 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 权重矩阵，<strong>并行</strong>计算，最后拼接起来：</p><span><span><span>headi=Attention(QWiQ,KWiK,VWiV)\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)</span><span><span><span></span><span><span><span>head</span></span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Attention</span></span><span>(</span><span>Q</span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>Q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>K</span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>K</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>V</span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><span><span><span>MHA=Concat(head1,head2,...,headh)⋅WO\text{MHA} = \text{Concat}(\text{head}_1, \text{head}_2, ..., \text{head}_h) \cdot W^O</span><span><span><span></span><span><span>MHA</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Concat</span></span><span>(</span><span><span><span>head</span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span><span>head</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span><span>head</span></span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>O</span></span></span></span></span></span></span></span></span></span></span></span><hr /></section><section><h2>2.3 什么是分块矩阵？<a href="#23-什么是分块矩阵"><span>#</span></a></h2><p><strong>分块矩阵</strong>就是把一个大矩阵看成由若干小矩阵（“块”）拼接而成。</p><p>举个例子，一个 <span><span>4×64 \times 6</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 的大矩阵可以分成 2 个 <span><span>4×34 \times 3</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的小块：</p><span><span><span>[a11a12a13∣a14a15a16a21a22a23∣a24a25a26a31a32a33∣a34a35a36a41a42a43∣a44a45a46]⏟一个大矩阵 4×6=[A1∣A2]⏟看成 2 个 4×3 的小块拼在一起\underbrace{\begin{bmatrix}
a_{11} &amp; a_{12} &amp; a_{13} &amp; | &amp; a_{14} &amp; a_{15} &amp; a_{16} \\
a_{21} &amp; a_{22} &amp; a_{23} &amp; | &amp; a_{24} &amp; a_{25} &amp; a_{26} \\
a_{31} &amp; a_{32} &amp; a_{33} &amp; | &amp; a_{34} &amp; a_{35} &amp; a_{36} \\
a_{41} &amp; a_{42} &amp; a_{43} &amp; | &amp; a_{44} &amp; a_{45} &amp; a_{46}
\end{bmatrix}}_{\text{一个大矩阵 } 4\times6}
=
\underbrace{\begin{bmatrix}
\mathbf{A}_1 &amp; | &amp; \mathbf{A}_2
\end{bmatrix}}_{\text{看成 2 个 } 4\times3 \text{ 的小块拼在一起}}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>一个大矩阵</span><span> </span></span><span>4</span><span>×</span><span>6</span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>41</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>42</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>23</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>33</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>43</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>∣</span></span></span><span><span></span><span><span>∣</span></span></span><span><span></span><span><span>∣</span></span></span><span><span></span><span><span>∣</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>24</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>34</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>44</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>25</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>35</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>45</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>16</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>26</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>36</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>46</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>看成</span><span> 2 </span><span>个</span><span> </span></span><span>4</span><span>×</span><span>3</span><span><span> </span><span>的小块拼在一起</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>∣</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><hr /></section><section><h2>2.4 为什么说 BERT layer 是由 MHA 分块矩阵实现的？<a href="#24-为什么说-bert-layer-是由-mha-分块矩阵实现的"><span>#</span></a></h2><p>这是关键的理解点！它说的是 <strong>MHA 的 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 权重矩阵在实现上使用了分块矩阵的技巧</strong>。</p><section><h3>2.4.1 直观理解<a href="#241-直观理解"><span>#</span></a></h3><p>BERT 有 12 个注意力头，每个头都需要自己的 <span><span>WiQ,WiK,WiVW_i^Q, W_i^K, W_i^V</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>Q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>K</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>V</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。与其分别做 12 次小矩阵乘法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># ❌ 低效做法：逐个计算每个头</span></div></div><div><div><div>2</div></div><div><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>12</span><span>):</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Qi </span><span>=</span><span> Q </span><span>@</span><span> Wq_i   </span><span># 每个 Wq_i 是 [768, 64]</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>Ki </span><span>=</span><span> K </span><span>@</span><span> Wk_i</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>Vi </span><span>=</span><span> V </span><span>@</span><span> Vk_i</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不如把它们<strong>拼成一个大矩阵，一次性计算</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># ✅ 高效做法：分块矩阵</span></div></div><div><div><div>2</div></div><div><span><span>Wq_big </span><span>=</span><span> torch.</span><span>cat</span><span>([Wq_1, Wq_2, </span><span>...</span><span>, Wq_12], </span></span><span>dim</span><span>=-</span><span>1</span><span>)  </span><span># [768, 768]</span></div></div><div><div><div>3</div></div><div><span><span>Q_big </span><span>=</span><span> Q </span><span>@</span><span> Wq_big   </span></span><span># 一次性计算所有头的 Q！</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2.4.2 用图来理解<a href="#242-用图来理解"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>             </span></span><span>每个头独立的小 W_q              拼成大矩阵 W_q</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>┌──────┐ ┌──────┐     ┌──────┐    ┌─────────────────────┐</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>│ W_q¹ │ │ W_q² │ ... │ W_q¹²│    │ W_q¹ │ W_q² │...│W_q¹²│</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>│768×64│ │768×64│     │768×64│    │        768×768        │</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>└──────┘ └──────┘     └──────┘    └─────────────────────┘</span></div></div><div><div><div>6</div></div><div><span><span>              </span></span><span>↑                    ↑</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>12次矩阵乘法           1次矩阵乘法搞定！</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是<strong>分块矩阵</strong>的思想：12 个小的 <span><span>WqW_q</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（每个 <span><span>768×64768 \times 64</span><span><span><span></span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span></span></span></span>）横向拼接成一个大的 <span><span>WqW_q</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（<span><span>768×768768 \times 768</span><span><span><span></span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span>），一次矩阵乘法就能得到所有 12 个头的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>，然后按块拆分给各个头。</p></section><section><h3>2.4.3 总结一句话<a href="#243-总结一句话"><span>#</span></a></h3><blockquote><p>不是说 BERT layer 物理上被”切开”了，而是说 <strong>MHA 的实现利用分块矩阵的技巧，把多个注意力头的权重拼成一个大矩阵，用一次大矩阵乘法代替多次小矩阵乘法，在 GPU 上高效并行计算。</strong></p></blockquote><p>这正是 PyTorch 中 <code>nn.MultiheadAttention</code> 和 HuggingFace BERT 源码中实际采用的做法！我们可以在之前学过的 05_BERT+embedding_source_code 中看到源码验证这一点。</p></section></section><section><h2>2.5 例子<a href="#25-例子"><span>#</span></a></h2><p>我们最后用一个例子来说明 BERT 中 Self-layer MHA 矩阵分块的实现过程。假设序列长度 <span><span>n=10n=10</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>，标准 BERT 模型 <span><span>dmodel=768d_{model}=768</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>m</span><span>o</span><span>d</span><span>e</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>768</span></span></span></span>, <span><span>h=12h=12</span><span><span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>, <span><span>dk=64d_k=64</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span>.</p><ol>
<li>对于每个头，我们首先将词嵌入矩阵 <span><span>X(10×768)X(10\times 768)</span><span><span><span></span><span>X</span><span>(</span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span><span>)</span></span></span></span> 乘以权重矩阵 <span><span>W(768×64)W(768\times 64)</span><span><span><span></span><span>W</span><span>(</span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span><span>)</span></span></span></span> 来得到 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span>：
<ul>
<li><span><span>X@Wq=Q(10×64)X @ W_q=Q(10\times 64)</span><span><span><span></span><span>X</span><span>@</span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>(</span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span><span>)</span></span></span></span></li>
<li><span><span>X@Wk=K(10×64)X @ W_k=K(10\times 64)</span><span><span><span></span><span>X</span><span>@</span><span><span>W</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>(</span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span><span>)</span></span></span></span></li>
<li><span><span>X@Wv=V(10×64)X @ W_v=V(10\times 64)</span><span><span><span></span><span>X</span><span>@</span><span><span>W</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span>(</span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span><span>)</span></span></span></span></li>
</ul>
</li>
<li>计算注意力得分矩阵：<span><span>softmax(Q⋅KTdk)softmax(\frac{Q\cdot K^T}{\sqrt{d_k}})</span><span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span>⋅</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>，得到 <span><span>10×1010\times 10</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>10</span></span></span></span> 的矩阵（每个词对每个词的关注度）。</li>
<li>与 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 相乘得到该头的输出：<span><span>Zi=softmax(QKTdk)⋅VZ_i = softmax(\frac{QK^T}{\sqrt{d_k}}) \cdot V</span><span><span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span><span></span><span>⋅</span><span></span></span><span><span></span><span>V</span></span></span></span>，维度为 <span><span>10×6410\times 64</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span></span></span></span>。</li>
<li>对 12 个头分别执行步骤 1~3，得到 <span><span>Z1,Z2,...,Z12Z_1, Z_2, ..., Z_{12}</span><span><span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（每个都是 <span><span>10×6410\times 64</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span></span></span></span>）。</li>
<li><strong>按列拼接</strong>所有 <span><span>ZiZ_i</span><span><span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：<span><span>Concat(Z1,Z2,...,Z12)Concat(Z_1, Z_2, ..., Z_{12})</span><span><span><span></span><span>C</span><span>o</span><span>n</span><span>c</span><span>a</span><span>t</span><span>(</span><span><span>Z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>Z</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，得到 <span><span>10×76810\times 768</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span> 的大矩阵。</li>
<li>乘以输出权重矩阵 <span><span>WO(768×768)W_O(768\times 768)</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>O</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span><span>)</span></span></span></span>，得到最终 MHA 输出：<span><span>10×76810\times 768</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span>。</li>
</ol><section><h3>2.5.1 维度变化总览<a href="#251-维度变化总览"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>X (10×768)</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>├─[头1]──→ Q₁(10×64), K₁(10×64), V₁(10×64) ──→ Z₁(10×64)</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>├─[头2]──→ Q₂(10×64), K₂(10×64), V₂(10×64) ──→ Z₂(10×64)</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>├─ ...                                             ...</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>└─[头12]─→ Q₁₂(10×64), K₁₂(10×64), V₁₂(10×64) ──→ Z₁₂(10×64)</span></div></div><div><div><div>7</div></div><div><span><span>                                                      </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span><span>                                          </span></span><span>按列拼接 Concat</span></div></div><div><div><div>9</div></div><div><span><span>                                                      </span></span><span>↓</span></div></div><div><div><div>10</div></div><div><span><span>                                               </span></span><span>(10×768)</span></div></div><div><div><div>11</div></div><div><span><span>                                                      </span></span><span>│</span></div></div><div><div><div>12</div></div><div><span><span>                                                 </span></span><span>× W_O(768×768)</span></div></div><div><div><div>13</div></div><div><span><span>                                                      </span></span><span>↓</span></div></div><div><div><div>14</div></div><div><span><span>                                               </span></span><span>MHA输出 (10×768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2.5.2 分块矩阵实现（实际代码的做法）<a href="#252-分块矩阵实现实际代码的做法"><span>#</span></a></h3><p>上述”逐个计算 12 个头”只是为了理解，实际代码中不会用 for 循环，而是利用<strong>分块矩阵</strong>一次性完成。</p><p><strong>核心技巧</strong>：把 12 个 <span><span>(768×64)(768\times 64)</span><span><span><span></span><span>(</span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span><span>)</span></span></span></span> 的小 <span><span>WqW_q</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 横向拼接成一个 <span><span>(768×768)(768\times 768)</span><span><span><span></span><span>(</span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span><span>)</span></span></span></span> 的大 <span><span>WqW_q</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Wqbig=[Wq1∣Wq2∣⋯∣Wq12]⏟768×768W_q^{\text{big}} = \underbrace{[W_q^1 \mid W_q^2 \mid \cdots \mid W_q^{12}]}_{768 \times 768}</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span><span><span></span><span><span><span><span>big</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>768</span><span>×</span><span>768</span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>[</span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span></span><span>⋯</span><span></span><span>∣</span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>然后一行代码算出所有头的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>Q_big </span><span>=</span><span> X </span><span>@</span><span> W_q_big   </span></span><span># (10×768) @ (768×768) = (10×768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>得到的 <span><span>QbigQ_{big}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>bi</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>10×76810\times 768</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span>，但这个 768 列其实是 12 个 <span><span>dk=64d_k=64</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 的块拼起来的：</p><span><span><span>Qbig=[Q1∣Q2∣⋯∣Q12]⏟10×768Q_{big} = \underbrace{[Q_1 \mid Q_2 \mid \cdots \mid Q_{12}]}_{10\times 768}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>bi</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>10</span><span>×</span><span>768</span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>[</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span></span><span>⋯</span><span></span><span>∣</span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>接下来用 <code>reshape</code> + <code>transpose</code> 把它拆成多头格式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># (10, 768) → (10, 12, 64) → (12, 10, 64)</span></div></div><div><div><div>2</div></div><div><span><span>Q </span><span>=</span><span> Q_big.</span><span>view</span><span>(</span></span><span>10</span><span>, </span><span>12</span><span>, </span><span>64</span><span><span>).</span><span>transpose</span><span>(</span></span><span>0</span><span>, </span><span>1</span><span>) </span><span># 把 batch 和 head 互换</span></div></div><div><div><div>3</div></div><div><span><span>K </span><span>=</span><span> K_big.</span><span>view</span><span>(</span></span><span>10</span><span>, </span><span>12</span><span>, </span><span>64</span><span><span>).</span><span>transpose</span><span>(</span></span><span>0</span><span>, </span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>V </span><span>=</span><span> V_big.</span><span>view</span><span>(</span></span><span>10</span><span>, </span><span>12</span><span>, </span><span>64</span><span><span>).</span><span>transpose</span><span>(</span></span><span>0</span><span>, </span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><code>.transpose()</code></div></div><div><p><code>.transpose(a,b)</code> 就是交换张量的第 a 维和第 b 维。</p></div></div><p>这里互换 batch 和 head 的原因是我们后续要批量计算注意力，所以需要把“头数”放在第 0 维。这样，当我们调用 Pytorch 的矩阵乘法时，就会自动在 12 个头上 <strong>并行广播</strong>。</p><p>此时 <span><span>Q,K,VQ,K,V</span><span><span><span></span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span></span></span></span> 都是 <span><span>(12,10,64)(12, 10, 64)</span><span><span><span></span><span>(</span><span>12</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>64</span><span>)</span></span></span></span>，即 <strong>12 个头 × 10 个词 × 64 维</strong>，可以直接批量计算注意力：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 批量计算所有头的注意力：(12, 10, 64) @ (12, 64, 10) = (12, 10, 10)</span></div></div><div><div><div>2</div></div><div><span><span>attn_scores </span><span>=</span><span> </span><span>softmax</span><span>(Q </span><span>@</span><span> K.</span><span>transpose</span><span>(</span><span>-</span></span><span>2</span><span><span>, </span><span>-</span></span><span>1</span><span><span>) </span><span>/</span><span> </span><span>sqrt</span><span>(</span></span><span>64</span><span>)) </span><span># 转置每个头的 K 矩阵，即 K^T</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># (12, 10, 10) @ (12, 10, 64) = (12, 10, 64)</span></div></div><div><div><div>5</div></div><div><span><span>Z </span><span>=</span><span> attn_scores </span><span>@</span><span> V</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后把多头拼回去：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># (12, 10, 64) → (10, 12, 64) → (10, 768)</span></div></div><div><div><div>2</div></div><div><span><span>Z_concat </span><span>=</span><span> Z.</span><span>transpose</span><span>(</span></span><span>0</span><span>, </span><span>1</span><span><span>).</span><span>reshape</span><span>(</span></span><span>10</span><span>, </span><span>768</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>output </span><span>=</span><span> Z_concat </span><span>@</span><span> </span><span>W_O</span><span>   </span></span><span># (10, 768) @ (768, 768) = (10, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2.5.3 对比总结<a href="#253-对比总结"><span>#</span></a></h3>

<table><thead><tr><th></th><th>概念上的做法（逐头循环）</th><th>实际代码的做法（分块矩阵）</th></tr></thead><tbody><tr><td><span><span>WqW_q</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>q</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 形状</td><td>12 个 <span><span>768×64768\times 64</span><span><span><span></span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span></span></span></span></td><td>1 个 <span><span>768×768768\times 768</span><span><span><span></span><span>768</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></td></tr><tr><td>矩阵乘法次数</td><td>12 次</td><td><strong>1 次</strong></td></tr><tr><td>中间形状</td><td><span><span>(10,64)×12(10,64)\times 12</span><span><span><span></span><span>(</span><span>10</span><span>,</span><span></span><span>64</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>12</span></span></span></span></td><td>先 <span><span>(10,768)(10,768)</span><span><span><span></span><span>(</span><span>10</span><span>,</span><span></span><span>768</span><span>)</span></span></span></span>，再 <code>view</code> 拆成 <span><span>(12,10,64)(12,10,64)</span><span><span><span></span><span>(</span><span>12</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>64</span><span>)</span></span></span></span></td></tr><tr><td>GPU 效率</td><td>低（串行）</td><td><strong>高（并行）</strong></td></tr></tbody></table><blockquote><p>💡 <strong>一句话</strong>：分块矩阵就是把 12 个”小水管”焊成 1 根”大水管”，数据一次性流过，然后在出口处再分流给 12 个头。</p></blockquote></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-08/</id>
      <title type="text">08_attention_01</title>
      <published>2026-06-20T00:00:00.000Z</published>
      <updated>2026-06-20T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-08/"/>
      <summary type="text">动手写 BERT 系列笔记第八节</summary>
      <content type="html"><![CDATA[<span><span><span>Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V</span><span><span><span></span><span>A</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span>(</span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span><span>V</span></span></span></span></span>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>from</span><span> bertviz.transformers_neuron_view </span><span>import</span><span> BertModel, BertConfig</span></div></div><div><div><div>4</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<p>这里我们首次导入了 <code>bertviz</code> 库。</p>
<p><code>bertviz</code> 是一个专门用来<strong>可视化 BERT 注意力机制</strong>的第三方库。它的核心功能就是把 BERT 模型每层每个注意力头的注意力权重画出来，让你”看到”模型在关注哪些词。</p>
<p>比如下面这种图就是 bertviz 生成的（注意力连线图）：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>[CLS] ────╮─────────────╮</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>After     ╰──╮──────────╮│</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>stealing    ╰─╮────────╮╮│</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>money      ╰──╮─────╮╰╯</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>from        ╰─╮────╯</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>the          ╰─╮──╯</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>bank    ←──←──←──╯    ← "bank" 和前面的词有很强的注意力</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<hr />
<p><strong><code>BertModel</code> 为什么从 <code>bertviz</code> 而不是 <code>transformers</code> 导入</strong>？</p>
<p>这是关键点。对比一下两条 <code>import</code>：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertModel, BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> bertviz.transformers_neuron_view </span><span>import</span><span> BertModel, BertConfig</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>原因：bertviz 对原版 <code>BertModel</code> 做了”<strong>偷梁换柱</strong>”</p>
<p><code>bertviz.transformers_neuron_view</code> 里面的 <code>BertModel</code> <strong>并不是</strong> HuggingFace <code>transformers</code> 库的原始 <code>BertModel</code>，而是 bertviz <strong>自己魔改过的版本</strong>。</p>
<p>你可以理解为：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>transformers.BertModel       → 标准的 BERT 模型（普通版）</span></div></div><div><div><div>2</div></div><div><span>bertviz.BertModel            → 被 bertviz 改造过的 BERT 模型（特制版✨）</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>这个”特制版”在原始 <code>BertModel</code> 的基础上做了两件事：</p>
<ol>
<li><strong>注册了前向钩子（forward hooks）</strong>——在模型计算注意力时，自动把注意力权重矩阵<strong>截取并保存下来</strong></li>
<li><strong>整理成可视化友好的格式</strong>——把原始的注意力权重整理成 bertviz 画图函数可以识别的数据结构</li>
</ol>
<p>用一张图说明：</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>原始 transformers 库</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>┌─────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>│  BertModel              │  ← 标准的 BERT，注意力权重藏在内部</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>│  forward() → 输出各种   │     不主动暴露细节</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>│  hidden states          │</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>└─────────────────────────┘</span></div></div><div><div><div>7</div></div><div><span><span>               </span></span><span>↑</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>bertviz 做了"包装"</span></div></div><div><div><div>9</div></div><div><span><span>               </span></span><span>↑</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>┌─────────────────────────┐</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>│  bertviz.transformers_  │</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>│  neuron_view.BertModel  │  ← 继承/包装了标准 BertModel</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>│                         │</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>│  forward() → 除了原有   │</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>│  输出，还自动捕获了     │</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>│  attention weights ✨   │</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>└─────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p><strong>为什么 <code>bertviz</code> 不直接用原版</strong>？</p>
<p>因为原版 <code>transformers.BertModel</code> 虽然可以设置 <code>output_attentions=True</code> 输出注意力权重，但输出的格式是<strong>嵌套的 tensor</strong>（12层 × 12头 × 序列长度 × 序列长度），直接拿来可视化需要额外处理。</p>
<p><code>bertviz</code> 为了方便用户一键可视化，直接把自己包装过的 <code>BertModel</code> 放在 <code>bertviz.transformers_neuron_view</code> 模块下，让你<strong>导入它的模型 → 正常 forward → 直接调用可视化函数</strong>，一气呵成。</p>
<p><strong>一个有趣的细节</strong></p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> bertviz.transformers_neuron_view </span><span>import</span><span> BertModel, BertConfig</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<ul>
<li><code>BertModel</code> 和 <code>BertConfig</code> → 从 <strong>bertviz</strong> 导入（因为要捕获注意力权重）</li>
<li><code>BertTokenizer</code> → 从 <strong>transformers</strong> 导入（分词器不需要捕获注意力，用原版即可）</li>
</ul>
<p>所以简单说：<strong>bertviz 是一个可视化工具包，它为了让你能方便地看到注意力权重，自己包装了一个”特制版”的 BertModel，你需要用它的版本来代替原版。</strong></p>

<section><h1>1. model config and load<a href="#1-model-config-and-load"><span>#</span></a></h1><section><h2>1.1 配置模型<a href="#11-配置模型"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>max_length </span><span>=</span><span> </span></span><span>256</span><span> </span><span># 设置最大输入长度</span></div></div><div><div><div>2</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>3</div></div><div><span><span>config </span><span>=</span><span> BertConfig.</span><span>from_pretrained</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>model_name,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>output_attentions</span><span>=</span><span>True</span><span>, </span><span># 让模型输出注意力权重（后面可视化用）</span></div></div><div><div><div>6</div></div><div><span>    </span><span>output_hidden_states</span><span>=</span><span>True</span><span>, </span><span># 让模型输出隐藏状态（后面可视化用）</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return_dict</span><span>=</span><span>True</span><span> </span><span># 以字典形式返回输出，更方便使用</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div><div><div><div>9</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>10</div></div><div><span><span>config.max_position_embeddings </span><span>=</span><span> max_length</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>model </span><span>=</span><span> </span><span>BertModel</span><span>(config).</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>13</div></div><div><span><span>model </span><span>=</span><span> model.</span><span>eval</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们首先配置模型，为理解注意力机制计算做准备：</p><section><h3>1.1.1 <code>BertConfig</code><a href="#111-bertconfig"><span>#</span></a></h3><ul>
<li><code>BertConfig.from_pretrained()</code> ：从 HuggingFace 下载/加载模型的配置文件（不是模型本身）。</li>
</ul><p>在之前的笔记中，我们直接使用类似 <code>model=BertModel.from_pretrained()</code> 创建模型并加载权重。实际上，当我们调用 <code>BertModel.from_pretrained()</code> 时，它的内部自动帮我们做了两件事：</p><ol>
<li>创建了一个默认的 <code>BertConfig</code>，并加载预训练模型的配置；</li>
<li>创建模型并加载权重。</li>
</ol><p>而在这里，我们是用 <code>config=BertConfig.from_pretrained()</code> 先手动创建了一个 <code>BertConfig</code>，再通过 <code>model=BertModel(config).from_pretrained()</code> 来初始化模型骨架，最后加载权重。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(config)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>"architectures": [                           # 模型架构名称（用于自动加载对应模型类）</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>"BertForMaskedLM"</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>"attention_probs_dropout_prob": 0.1,  # 注意力层的 Dropout 概率（防止过拟合）</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>"finetuning_task": null,              # 微调任务类型（null 表示预训练阶段，未指定下游任务）</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>"hidden_act": "gelu",                 # 隐藏层的激活函数（GELU：高斯误差线性单元）</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>"hidden_dropout_prob": 0.1,           # 全连接层的 Dropout 概率</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>"hidden_size": 768,                   # 隐藏层维度（每个 token 被编码成 768 维向量）</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>"initializer_range": 0.02,            # 参数初始化时权重的随机范围（±0.02 内均匀分布）</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>"intermediate_size": 3072,            # FFN 中间层维度（768 → 3072 → 768，先放大再缩小）</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>"layer_norm_eps": 1e-12,              # LayerNorm 的分母防零小常数</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>"max_position_embeddings": 256,       # 最大序列长度（此处被我们从 512 改为了 256）</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>"model_type": "bert",                 # 模型类型标识（用于 HuggingFace 自动路由）</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>"num_attention_heads": 12,            # 每层注意力头数（每个头负责关注不同的特征）</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>"num_hidden_layers": 12,              # Transformer 编码器层数</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>"num_labels": 2,                      # 分类任务标签数（预训练默认为 2，用于 NSP 任务）</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>"output_attentions": true,            # 是否输出注意力权重（此处设为 true，用于可视化）</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>"output_hidden_states": true,         # 是否输出所有隐藏层状态</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>"pad_token_id": 0,                    # Padding 填充符的 token ID（[PAD] = 0）</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>"torchscript": false,                 # 是否启用 TorchScript（用于模型部署优化）</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>"type_vocab_size": 2,                 # 句子类型数量（单句=0，句对=1，用于 token_type_ids）</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>"vocab_size": 30522                   # 词表大小（BERT 共能识别 30522 个不同的词/子词）</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可以看到，<code>BertConfig</code> 其实就是一个<strong>配置类</strong>，里面存着 BERT 模型的<strong>所有超参数</strong>。</p></section><section><h3>1.1.2 什么时候必须用 <code>BertConfig</code>？<a href="#112-什么时候必须用-bertconfig"><span>#</span></a></h3><p>当你<strong>需要修改默认配置</strong>时，手动创建 <code>Config</code> 就很有必要了。</p><p>之前的代码拆分来看就是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel(config)              → 拿配置单造一个"空壳模型"（参数随机初始化）</span></div></div><div><div><div>2</div></div><div><span><span>                    </span></span><span>.from_pretrained(model_name)  → 把预训练好的"参数"填进去</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.1.3 那 <code>BertModel.from_pretrained()</code> 能修改配置吗？<a href="#113-那-bertmodelfrom_pretrained-能修改配置吗"><span>#</span></a></h3><p><strong>可以！</strong> <code>model=BertModel.from_pretrained()</code> 也支持传参数，但<strong>只能改部分参数</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 也能修改部分配置</span></div></div><div><div><div>2</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>model_name,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>output_hidden_states</span><span>=</span><span>True</span><span>,    </span><span># ✅ 可以</span></div></div><div><div><div>5</div></div><div><span>    </span><span>output_attentions</span><span>=</span><span>True</span><span>,       </span><span># ✅ 可以</span></div></div><div><div><div>6</div></div><div><span>    </span><span>max_position_embeddings</span><span>=</span><span>256</span><span>   </span><span># ❌ 这个就不行！</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>像 <code>max_position_embeddings</code> 这种<strong>结构性的参数</strong>（会影响模型内部张量形状的），你必须在 <code>BertConfig</code> 里先改好，再传给 <code>BertModel</code>，否则会报错。</p><hr /></section></section><section><h2>1.2 Transformer 层<a href="#12-transformer-层"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>att_head_size </span><span>=</span><span> </span></span><span>int</span><span><span>(model.config.hidden_size </span><span>/</span><span> model.config.num_attention_heads)</span></span></div></div><div><div><div>2</div></div><div><span>att_head_size </span><span># 64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里其实就是每个注意力头的维度大小，也就是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入 (768维)</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>┌───── 拆分成 12 个注意力头 ────┐</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>│        │        │             │</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>头1(64维) 头2(64维) 头3(64维) ... 头12(64维)</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>│        │        │             │</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>每个头独立计算注意力（关注不同位置的特征）</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>│        │        │             │</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>└───── 拼接回去 (64×12 = 768) ──┘</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>输出 (768维)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从数学上来看，在 Self-Attention 中，Q、K、V 矩阵的形状变化是：</p><ol>
<li><strong>原始矩阵</strong>：<code>Q, K, V</code> 形状 = <code>(batch, seq_len, 768)</code></li>
<li><strong>拆分为多头</strong>后重塑为：<code>(batch, seq_len, 12, 64)</code>，然后转置为 <code>(batch, 12, seq_len, 64)</code></li>
<li>每个头的注意力分数计算：<span><span>‘Attention(Qi,Ki)=softmax(QiKiTdk)Vi‘`\text{Attention}(Q_i, K_i) = \text{softmax}\left(\frac{Q_i K_i^T}{\sqrt{d_k}}\right)V_i`</span><span><span><span></span><span>‘</span><span><span>Attention</span></span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>softmax</span></span><span></span><span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>Q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span></span><span></span><span><span>V</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>‘</span></span></span></span>，其中 <span><span>dk=att_head_size=64d_k = \text{att\_head\_size} = 64</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>att_head_size</span></span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span></li>
</ol><p>这里的 <span><span>dk=64=8\sqrt{d_k} = \sqrt{64} = 8</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>64</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>8</span></span></span></span> 就是 <strong>scaling factor（缩放因子）</strong>，用于防止 softmax 后的梯度太小。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model.encoder</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>model.encoder.layer</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>model.encoder.layer[</span><span>0</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertEncoder(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>8</div></div><div><span><span>          </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>14</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>(output): BertOutput(</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>24</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>27</div></div><div><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>ModuleList(</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>32</div></div><div><span><span>      </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>37</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>      </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>42</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>45</div></div><div><span><span>      </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>(output): BertOutput(</span></div></div><div><div><div>48</div></div><div><span><span>      </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>49</div></div><div><span><span>      </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>50</div></div><div><span><span>      </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>53</div></div><div><span>)</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>BertLayer(</span></div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>58</div></div><div><span><span>      </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>59</div></div><div><span><span>      </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>60</div></div><div><span><span>      </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>61</div></div><div><span><span>      </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>64</div></div><div><span><span>      </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>65</div></div><div><span><span>      </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>66</div></div><div><span><span>      </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>  </span></span><span>(output): BertOutput(</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>76</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>77</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出解析：</p><ul>
<li><code>model.encoder</code> ：还记得我们在 03 中看过 <code>model</code> 本身的输出吗？<code>encoder</code> 就是其中的一个属性，包含了所有的 Transformer 层（也就是我们在 07 中看到的那些层）。我们可以通过 <code>model.encoder.layer</code> 来访问这些层。</li>
<li><code>model.encoder.layer</code> ：当我们用 <code>model.encoder.layer</code> 获取了所有 Transformer 层后，我们就可以索引这些层了。</li>
<li><code>model.encoder.layer[0]</code> ：比如，我们可以取出第 0 层。</li>
</ul><section><h3>1.2.1 每个 Transformer 层的架构<a href="#121-每个-transformer-层的架构"><span>#</span></a></h3><p>从第 0 层的输出中，我们可以管窥每个 Transformer 层的架构。</p><section><h4>整体结构<a href="#整体结构"><span>#</span></a></h4><p>每个 <code>BertLayer</code> 由 <strong>3 大模块</strong>组成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertLayer</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├── attention          ← 多头注意力层</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├── intermediate       ← FFN 中间层（升维）</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>└── output             ← FFN 输出层（降维 + 残差连接 + LayerNorm）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这和经典 Transformer 论文中的架构完全一致：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>┌─────────────────────┐</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>│     Add &amp; Norm      │  ← output (LayerNorm + 残差)</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>├─────────────────────┤</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>│   Feed Forward      │  ← intermediate (3072) → output (768)</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>├─────────────────────┤</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>│     Add &amp; Norm      │  ← BertSelfOutput (LayerNorm + 残差)</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>├─────────────────────┤</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>│  Multi-Head Self-Attention │  ← BertSelfAttention</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>└─────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>① <code>attention</code> — 注意力子层<a href="#-attention--注意力子层"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(attention): BertAttention(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(self): BertSelfAttention(          ← 核心：多头自注意力</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(query): Linear(768 → 768)        ← Q 映射</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(key):   Linear(768 → 768)        ← K 映射</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(value): Linear(768 → 768)        ← V 映射</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1)</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>(output): BertSelfOutput(            ← Norm + 残差连接</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>(dense):   Linear(768 → 768)       ← 多头输出拼接后的投影</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(LayerNorm): BertLayerNorm()       ← 层归一化</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>(dropout):  Dropout(p=0.1)</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键点：</p><ul>
<li><strong>Q/K/V 都是 <code>Linear(768→768)</code></strong>：注意，这里输出是 <strong>768 维</strong>，即 12×64=768 . 实际上这 768 维的 Linear 层内部被”隐式拆成”了 12 个头——权重矩阵形状是 <code>(768, 768)</code>，但计算时会被 reshape 成 <code>(12, 64)</code> 做并行计算。</li>
<li><strong><code>BertSelfOutput</code></strong> 做了一件重要的事：<strong>残差连接 + LayerNorm</strong>，即 <span><span>LayerNorm(x+Attention(x))\text{LayerNorm}(x + \text{Attention}(x))</span><span><span><span></span><span><span>LayerNorm</span></span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>Attention</span></span><span>(</span><span>x</span><span>))</span></span></span></span></li>
</ul></section><section><h4>② <code>intermediate</code> — FFN 升维层<a href="#-intermediate--ffn-升维层"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(dense): Linear(768 → 3072)    ← 把 768 维放大到 3072 维</span></div></div><div><div><div>3</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这对应 Transformer 论文中的 <strong>Feed Forward</strong> 的第一半：先 <strong>4 倍升维</strong>（768 → 3072），激活函数为 <strong>GELU</strong>（在代码中不显示，在 <code>config.hidden_act</code> 里）。</p></section><section><h4>③ <code>output</code> — FFN 降维 + 残差<a href="#-output--ffn-降维--残差"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(output): BertOutput(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(dense):   Linear(3072 → 768)   ← 从 3072 维降回 768 维</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(dropout):  Dropout(p=0.1)</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 FFN 的第二半：<strong>降维回 768</strong>，然后再次做 <strong>残差连接 + LayerNorm</strong>。</p><hr /><p>一张图总结单个 Transformer 层中完整的数据流：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入 x (768维) (上一层的输出)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>├──→ BertSelfAttention (Q/K/V 各 768→768, 再拆12头)</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│      ┌─────────────────────────────────────┐</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│      │  768 → 12个头×64维 → 注意力计算 → 拼接 → 768  │</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│      └─────────────────────────────────────┘</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>│    输出 att_out (768维)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>├──→ BertSelfOutput (残差连接 + LayerNorm)</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>│     output = LayerNorm(x + att_out)</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>├──→ BertIntermediate</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>│     Linear(768 → 3072) + GELU</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>│    输出 3072维</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>├──→ BertOutput</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>│     Linear(3072 → 768) + 残差连接 + LayerNorm</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>│     output = LayerNorm(intermediate_out + self_output)</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>└──→ 输出 (768维) → 进入下一层</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section><section><h3>1.2.2 “多头”在哪？—— 看 reshape 拆解过程<a href="#122-多头在哪-看-reshape-拆解过程"><span>#</span></a></h3><p>上面我们已经拆解了 encoder 中每一个 Transformer 层的结构。但这里我们发现，上述结构中似乎并没有体现“多头注意力”中的“多头”。即使我们进一步索引：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention.self</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention.self.query</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertAttention(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(LayerNorm): BertLayerNorm()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>BertSelfAttention(</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>20</div></div><div><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>Linear(in_features=768, out_features=768, bias=True)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>从表面上看，<code>query</code> 是一个 <code>Linear(768→768)</code>，完全看不出”12 个头”的存在。秘密藏在<strong>实际计算时的 reshape 操作</strong>中：</p><section><h4>原理<a href="#原理"><span>#</span></a></h4><p>模型不是创建 12 个独立的 Linear 层，而是用一个 <code>Linear(768→768)</code> 一次性算出<strong>所有头的 Q</strong>，然后通过 tensor reshape 拆成 12 份：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Linear(768→768) 的输出:  (batch, seq_len, 768)</span></div></div><div><div><div>2</div></div><div><span><span>                                       </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>                          </span></span><span>reshape 成 (batch, seq_len, 12, 64)</span></div></div><div><div><div>4</div></div><div><span><span>                                       </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span><span>                          </span></span><span>transpose 成 (batch, 12, seq_len, 64)</span></div></div><div><div><div>6</div></div><div><span><span>                                       </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>                          </span></span><span>┌──── 头0 ── 头1 ── ... ── 头11 ──┐</span></div></div><div><div><div>8</div></div><div><span><span>                          </span></span><span>│  64维   64维          64维       │</span></div></div><div><div><div>9</div></div><div><span><span>                          </span></span><span>└──────────────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面用代码演示这个拆解过程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention.self.query.weight.shape </span><span># torch.Size([768, 768])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention.self.query.weight[:</span><span>64</span><span>, :] </span><span># 第一个头的权重矩阵</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention.self.query.weight[:</span><span>64</span><span>, :].shape </span><span># torch.Size([64, 768])</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>model.encoder.layer[</span><span>0</span><span>].attention.self.query.weight[</span><span>64</span><span>:</span><span>128</span><span>, :] </span><span># 第二个头的权重矩阵</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>torch.Size([768, 768])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>tensor([[-0.0164,  0.0261, -0.0263,  ..., -0.0545,  0.0607,  0.0398],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>[-0.0326,  0.0346, -0.0423,  ...,  0.0586, -0.0306, -0.0412],</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[ 0.0105,  0.0334,  0.0109,  ..., -0.0573, -0.0118, -0.0147],</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>[-0.0085,  0.0514,  0.0555,  ..., -0.0607,  0.0823,  0.0531],</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[-0.0198,  0.0944,  0.0617,  ...,  0.0934, -0.0044, -0.0301],</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>[ 0.0015, -0.0952,  0.0099,  ...,  0.0719,  0.0701,  0.0175]],</span></div></div><div><div><div>10</div></div><div><span><span>       </span></span><span>grad_fn=&lt;SliceBackward0&gt;)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>torch.Size([64, 768])</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>tensor([[-0.0112,  0.0260, -0.0169,  ...,  0.1083, -0.0436, -0.0611],</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>[-0.0324, -0.0067,  0.0232,  ...,  0.0056, -0.1032,  0.0224],</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>[-0.0615, -0.0616,  0.0068,  ...,  0.0968, -0.1035, -0.0320],</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>[-0.0383,  0.1097,  0.0124,  ...,  0.0188,  0.0138,  0.0376],</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>[ 0.0031,  0.0029, -0.0168,  ..., -0.0171, -0.0488,  0.0186],</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>[ 0.0059, -0.0540,  0.0301,  ...,  0.0141, -0.0453, -0.0482]],</span></div></div><div><div><div>21</div></div><div><span><span>       </span></span><span>grad_fn=&lt;SliceBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出解析：</p><ul>
<li><code>.weight.shape</code> ：这里我们取出的是第 0 层的 query 权重矩阵形状。</li>
<li><code>[:64, :]</code> ：这里我们切出的是第一个头的 query 矩阵。</li>
</ul><p>在 Pytorch 中，<code>nn.Linear</code> 构造函数的参数顺序是 <code>(in_features, out_features)</code>，但要注意在这里我们内部存储的权重矩阵形状是 <code>(out_features, in_features)=(768,768)</code>，即：行 = 输出维度，列 = 输入维度</p><p>用一个简单的图示说明：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>in_features=768 (列)</span></div></div><div><div><div>2</div></div><div><span><span>       </span></span><span>┌────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>头0   │ ███████████████████████████ │ ← [:64, :]  这 64 行 = 头 0</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>64行  │ ███████████████████████████ │</span></div></div><div><div><div>5</div></div><div><span><span>       </span></span><span>├────────────────────────────┤</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>头1   │ ·························· │ ← 下一个 64 行 = 头 1</span></div></div><div><div><div>7</div></div><div><span><span>       </span></span><span>│ ·························· │</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>├────────────────────────────┤</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>...  │            ...             │</span></div></div><div><div><div>10</div></div><div><span><span>       </span></span><span>├────────────────────────────┤</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>头11  │ ·························· │ ← 最后 64 行 = 头 11</span></div></div><div><div><div>12</div></div><div><span><span>       </span></span><span>│ ·························· │</span></div></div><div><div><div>13</div></div><div><span><span>       </span></span><span>└────────────────────────────┘</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>out_features=768 (行)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里并不是 Pytorch 一定要反着来，而是由它的数学实现方式决定的。PyTorch 的 Linear 实际做的计算是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>y = x @ W.T + b    # ← 注意这里有个 .T（转置）！</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其中：</p><ul>
<li><span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的形状：<code>(*, in_features)</code>，比如 <code>(batch, 768)</code></li>
<li><span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 的<strong>存储形状</strong>：<code>(out_features, in_features) = (768, 768)</code></li>
<li><span><span>WTW^T</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的形状：<code>(in_features, out_features) = (768, 768)</code></li>
<li>结果 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 的形状：<code>(*, out_features)</code></li>
</ul><p>如果存成 <code>(in_features, out_features)</code>，计算就是 <span><span>y=xWy = xW</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span>W</span></span></span></span>（不需要转置）。</p><p>用一张图对比</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>构造参数:        Linear(in_features=768, out_features=768)</span></div></div><div><div><div>2</div></div><div><span><span>                         </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>存储的 W:        shape = (768, 768) = (out, in)</span></div></div><div><div><div>4</div></div><div><span><span>                         </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>计算时:         y = x @ W.T</span></div></div><div><div><div>6</div></div><div><span><span>                   </span></span><span>x: (*, 768)   W.T: (768, 768) (in, out)</span></div></div><div><div><div>7</div></div><div><span><span>                   </span></span><span>结果: (*, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>[64:128, :]</code> ：这里再往后 64 维就是顺理成章地第二个头的 query 矩阵。</li>
</ul></section></section></section></section>
<section><h1>2. data<a href="#2-data"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> sklearn.datasets </span><span>import</span><span> fetch_20newsgroups</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>newsgroups_train </span><span>=</span><span> </span><span>fetch_20newsgroups</span><span>(</span></span><span>subset</span><span>=</span><span>'train'</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>inputs_tests </span><span>=</span><span> </span><span>tokenizer</span><span>(</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>newsgroups_train.data[:</span><span>1</span><span>], </span><span># 取前一条文本数据进行测试</span></div></div><div><div><div>6</div></div><div><span>    </span><span>padding</span><span>=</span><span>True</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>truncation</span><span>=</span><span>True</span><span>, </span><span># 超过最大长度则截断</span></div></div><div><div><div>8</div></div><div><span>    </span><span>max_length</span><span><span>=</span><span>max_length, </span></span><span># 最大长度</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return_tensors</span><span>=</span><span>'pt'</span><span> </span><span># 返回PyTorch张量</span></div></div><div><div><div>10</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>inputs_tests.</span><span>keys</span><span>() </span></span><span># dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>inputs_tests[</span><span>'input_ids'</span><span>].shape </span><span># torch.Size([1, 201])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>torch.Size([1, 201])</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h1>3. model output<a href="#3-model-output"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_output </span><span>=</span><span> </span><span>model</span><span>(**inputs_tests)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面是 BERT 模型 forward 时可能返回的 4 种输出：</p><ul>
<li><code>last_hidden_state(batch_size, seq_length, hidden_size)</code>: the last hidden state where outputted from the last BERT layer.</li>
<li><code>pooler_output(batch_size, hidden_size)</code>: output of the Pooler layer.</li>
<li><code>hidden_states(batch_size, seq_length, hidden_size)</code>: hidden states of the model at the output of each BERT layer + the initial embedding.</li>
<li><code>attentions(batch_size, num_heads, seq_length, seq_length)</code>: one for each BERT layer. Attentions weights after the attention softmax.</li>
</ul>

<table><thead><tr><th>输出</th><th>形状</th><th>含义</th><th>在我们的输出中？</th></tr></thead><tbody><tr><td><code>last_hidden_state</code></td><td><code>(1, 201, 768)</code></td><td>所有 201 个 token 经过 <strong>最后一层 BERT 层</strong> 后的隐藏状态</td><td>✅ <code>[0]</code></td></tr><tr><td><code>pooler_output</code></td><td><code>(1, 768)</code></td><td><code>[CLS]</code> token 经过一个 <strong>Linear + Tanh</strong> 后的句子级表示</td><td>✅ <code>[1]</code></td></tr><tr><td><code>hidden_states</code></td><td>元组，13 个 <code>(1, 201, 768)</code></td><td><strong>每一层</strong> 的输出 + 最开始的 embedding 层输出（共 13 个）</td><td>❌ 未开启</td></tr><tr><td><code>attentions</code></td><td>元组，12 个 <code>(1, 12, 201, 201)</code></td><td><strong>每一层</strong> 的注意力权重（softmax 之后）</td><td>✅ <code>[2]</code></td></tr></tbody></table><p>如果开启 <code>output_hidden_states=True</code>，<code>model_output</code> 就会变成 4 个：</p>

<table><thead><tr><th>索引</th><th>名称</th></tr></thead><tbody><tr><td><code>[0]</code></td><td><code>last_hidden_state</code></td></tr><tr><td><code>[1]</code></td><td><code>pooler_output</code></td></tr><tr><td><code>[2]</code></td><td><strong><code>hidden_states</code></strong>（新增）</td></tr><tr><td><code>[3]</code></td><td><code>attentions</code></td></tr></tbody></table><p><strong>hidden_states 是什么</strong>？</p><p><code>hidden_states</code> 是 <strong>每一层 BERT 层的输出 + 最开始的 embedding 输出</strong>，共 13 个：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hidden_states = (</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>embedding_output,     # [0] 初始词嵌入 + 位置嵌入 + 句子类型嵌入</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>layer_0_output,       # [1] 第 0 层输出</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>layer_1_output,       # [2] 第 1 层输出</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>...</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>layer_11_output,      # [12] 第 11 层输出 = last_hidden_state</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它可以用来做<strong>中间层特征提取</strong>或<strong>分析模型各层学到了什么</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len</span><span>(model_output) </span><span># 3</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>len</span><span><span>(model_output[</span><span>-</span></span><span>1</span><span>]) </span><span># 12</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>model_output[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span><span>].</span><span>keys</span><span>() </span></span><span># dict_keys(['attn', 'queries', 'keys'])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>model_output</code> ：长度是 <strong>3</strong>，它对应 BERT 模型返回的 <strong>3 个输出组件</strong>：</li>
</ul>

<table><thead><tr><th>索引</th><th>名称</th><th>形状</th><th>含义</th></tr></thead><tbody><tr><td><code>[0]</code></td><td><code>last_hidden_state</code></td><td><code>(1, 201, 768)</code></td><td>所有 token 经过 12 层编码后的最终隐藏状态</td></tr><tr><td><code>[1]</code></td><td><code>pooler_output</code></td><td><code>(1, 768)</code></td><td><code>[CLS]</code> token 再经过一个 Linear+Tanh 后的句子级表示</td></tr><tr><td><code>[2]</code></td><td><code>attentions</code></td><td><code>tuple</code> 包含 12 层</td><td>每层的注意力权重（因设置了 <code>output_attentions=True</code>）</td></tr></tbody></table><p>具体到每一层的注意力形状为 <code>(batch, 12 heads, seq_len, seq_len)</code>。</p><p>当然，如果你没有在 <code>config</code> 中设置 <code>output_attentions=True</code>，那么 <code>model_output</code> 的长度就只有 <strong>2</strong>（没有 <code>attentions</code>）。</p><ul>
<li><code>model_output[-1]</code> ：也就是 attentions 的部分（最后一部分），总共 12 层，所以长度为 12.</li>
<li><code>model_output[-1][0].keys()</code> ：我们可以看到 attention 部分值的 keys.</li>
</ul><p>这里的 <code>attn</code>、<code>queries</code>、<code>keys</code> 是 <strong>bertviz 魔改版 BertModel</strong> 通过前向钩子额外捕获的<strong>注意力计算中间产物</strong>，对应注意力公式的三个核心部分：</p>

<table><thead><tr><th>key</th><th>含义</th><th>对应公式中的</th></tr></thead><tbody><tr><td><code>queries</code></td><td>第 0 层的 <strong>Q 向量</strong>（每个 token 的 query 表示）</td><td><span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span></td></tr><tr><td><code>keys</code></td><td>第 0 层的 <strong>K 向量</strong>（每个 token 的 key 表示）</td><td><span><span>KK</span><span><span><span></span><span>K</span></span></span></span></td></tr><tr><td><code>attn</code></td><td>第 0 层的<strong>注意力权重</strong>（softmax 后的概率分布）</td><td><span><span>softmax(QKTdk)softmax(\frac{QK^T}{\sqrt{d_k}})</span><span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span></td></tr></tbody></table><p>它们的关系：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>queries 和 keys 做点积 → 缩放 → softmax → attn</span></div></div><div><div><div>2</div></div><div><span><span>                                            </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span><span>                                       </span></span><span>attn 再和 V 相乘 → 输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也就是说：</p><ol>
<li><strong><code>queries</code></strong>：当前 token “想关注别人” 的表示</li>
<li><strong><code>keys</code></strong>：当前 token “被关注” 的表示（别人来查它）</li>
<li><strong><code>attn</code></strong>：<span><span>Q⋅KTQ \cdot K^T</span><span><span><span></span><span>Q</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 经过 softmax 后的结果，即<strong>每个 token 对其他所有 token 的注意力分布</strong></li>
</ol><p><strong>为什么只存了 queries 和 keys，没有 values</strong>？</p><p>因为 bertviz 的<strong>可视化只需要看”谁在关注谁”</strong>，而注意力权重由 <strong>Q 和 K 的点积</strong> 决定，不需要 V。所以它只在钩子里捕获了 Q 和 K 以及算好的 <code>attn</code>，V 没有被保存——这也解释了为什么字典里只有三个键，没有 <code>values</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_output[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span>][</span><span>'attn'</span><span>].shape </span><span># torch.Size([1, 12, 201, 201])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>正如我们之前所说，<code>attentions</code> 的形状是 <code>(batch_size, num_heads, seq_length, seq_length)</code>. 现在，让我们好好拆解一下每个输出，每个维度的来源。</p><section><h2>3.1 维度是怎么来的？<a href="#31-维度是怎么来的"><span>#</span></a></h2><section><h3>3.1.1 <code>last_hidden_state</code> — shape <code>(1, 201, 768)</code><a href="#311-last_hidden_state--shape-1-201-768"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len</span><span>(model_output)         </span><span># 3</span></div></div><div><div><div>2</div></div><div><span>model_output[</span><span>0</span><span>].shape     </span><span># (1, 201, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个维度的含义：</p>

<table><thead><tr><th>维度</th><th>值</th><th>来源</th></tr></thead><tbody><tr><td><code>batch_size</code></td><td><strong>1</strong></td><td>我们只传了 1 条文本 <code>newsgroups_train.data[:1]</code></td></tr><tr><td><code>seq_len</code></td><td><strong>201</strong></td><td>这条文本被 tokenizer 分词后的长度（<code>max_length=256</code>，截断前为 201）</td></tr><tr><td><code>hidden_size</code></td><td><strong>768</strong></td><td><code>config.hidden_size</code> —— BERT 把每个 token 编码成 768 维向量</td></tr></tbody></table><p><strong>怎么来的：</strong> 输入 <code>(1, 201)</code> 的 token IDs 经过 embedding 层变成 <code>(1, 201, 768)</code>，再经过 12 层 Transformer 编码器，<strong>每层输入输出形状不变</strong>，最后输出仍然是 <code>(1, 201, 768)</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>token IDs             embedding           12层 Transformer</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(1, 201)    ──→     (1, 201, 768)    ──→    (1, 201, 768)</span></div></div><div><div><div>3</div></div><div><span><span>                                               </span></span><span>↑</span></div></div><div><div><div>4</div></div><div><span><span>                                          </span></span><span>last_hidden_state</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>3.1.2 <code>pooler_output</code> — shape <code>(1, 768)</code><a href="#312-pooler_output--shape-1-768"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model_output[</span><span>1</span><span>].shape     </span><span># (1, 768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>维度</th><th>值</th><th>来源</th></tr></thead><tbody><tr><td><code>batch_size</code></td><td><strong>1</strong></td><td>同上</td></tr><tr><td><code>hidden_size</code></td><td><strong>768</strong></td><td>经过 Linear + Tanh 后的向量维度</td></tr></tbody></table><p><strong>怎么来的：</strong> 从 <code>last_hidden_state</code> 中取出 <code>[CLS]</code> 位置（第 0 个 token）的向量，再过一个 Linear(768→768) + Tanh：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>last_hidden_state          取出 [CLS] 位置      Linear + Tanh</span></div></div><div><div><div>2</div></div><div><span>(1, 201, 768)    ──→       (1, 768)       ──→      (1, 768)</span></div></div><div><div><div>3</div></div><div><span><span>                           </span></span><span>↑                       ↑</span></div></div><div><div><div>4</div></div><div><span><span>                     </span></span><span>位置 [:, 0, :]          pooler_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>3.1.3 <code>attentions</code> — tuple of 12, each shape <code>(1, 12, 201, 201)</code><a href="#313-attentions--tuple-of-12-each-shape-1-12-201-201"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len</span><span><span>(model_output[</span><span>-</span></span><span>1</span><span>])                 </span><span># 12（12 层）</span></div></div><div><div><div>2</div></div><div><span><span>model_output[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span>].shape             </span><span># 等一下，这里是 dict，我们来查</span></div></div><div><div><div>3</div></div><div><span><span>model_output[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span>][</span><span>'attn'</span><span>].shape     </span><span># (1, 12, 201, 201)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>维度</th><th>值</th><th>来源</th></tr></thead><tbody><tr><td><code>batch_size</code></td><td><strong>1</strong></td><td>同上</td></tr><tr><td><code>num_heads</code></td><td><strong>12</strong></td><td><code>config.num_attention_heads</code> —— 每层 12 个注意力头</td></tr><tr><td><code>seq_len</code> (query)</td><td><strong>201</strong></td><td>query 侧的 token 数量</td></tr><tr><td><code>seq_len</code> (key)</td><td><strong>201</strong></td><td>key 侧的 token 数量</td></tr></tbody></table><p><strong>怎么来的：</strong> 这是每个头算出的注意力权重矩阵，形状来源是：</p><p><span><span>Attention(Qi,Ki)=softmax(QiKiTdk)Attention(Q_i, K_i) = softmax\left(\frac{Q_i K_i^T}{\sqrt{d_k}}\right)</span><span><span><span></span><span>A</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span></span><span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>Q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span></span></span></span></span></p><ul>
<li>第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个头：<span><span>QiQ_i</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 形状 <code>(1, 201, 64)</code>，<span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 形状 <code>(1, 201, 64)</code></li>
<li><span><span>Qi⋅KiTQ_i \cdot K_i^T</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> → <code>(1, 201, 201)</code> — 每个 query token 对所有 key token 的注意力分数</li>
<li>12 个头堆叠在一起 → <code>(1, 12, 201, 201)</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Q 拆成 12 个头          K 拆成 12 个头</span></div></div><div><div><div>2</div></div><div><span>(1, 12, 201, 64)        (1, 12, 201, 64)</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>\                    /</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>╲   ╱  Q @ K^T</span></div></div><div><div><div>5</div></div><div><span><span>           </span></span><span>╲ ╱</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>(1, 12, 201, 201)      ← 注意力权重</span></div></div><div><div><div>7</div></div><div><span><span>             </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span><span>          </span></span><span>softmax</span></div></div><div><div><div>9</div></div><div><span><span>             </span></span><span>│</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>(1, 12, 201, 201)      ← attn（保存下来的值）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p><strong>注意</strong>：bertviz 魔改版保存的 <code>attn</code> 已经是 softmax 之后的值，形状正是 <code>(1, 12, 201, 201)</code>。</p></blockquote><section><h4>这里 12 个头具体是如何堆叠的<a href="#这里-12-个头具体是如何堆叠的"><span>#</span></a></h4><p>这里<strong>既不是按行拼接，也不是按列拼接</strong>，而是通过 <strong>reshape（view）+ transpose（转置）</strong> 在张量的”维度”层面实现的。</p><p>整个过程分三步走：</p><section><h5>第 1 步：Linear 输出 → 所有头”首尾相连”排成一个长向量<a href="#第-1-步linear-输出--所有头首尾相连排成一个长向量"><span>#</span></a></h5><p>Linear(768→768) 的输出是 <code>(1, 201, 768)</code>。对于<strong>每个位置</strong>的 token，它的 768 维向量是这样排列的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>每个位置的 768 维向量:</span></div></div><div><div><div>2</div></div><div><span>┌─────────────────────────────────────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span>│  头0的64维  │  头1的64维  │  头2的64维  │ ... │ 头11的64维 │</span></div></div><div><div><div>4</div></div><div><span>│    0~63     │   64~127    │   128~191   │     │  704~767   │</span></div></div><div><div><div>5</div></div><div><span>└─────────────────────────────────────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这 12 个 64 维向量是<strong>首尾相连、连续存储</strong>在 768 维中的。</p></section><section><h5>第 2 步：<code>view</code>（reshape）→ 把”连着的”拆成单独一维<a href="#第-2-步viewreshape-把连着的拆成单独一维"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># (1, 201, 768)  →  (1, 201, 12, 64)</span></div></div><div><div><div>2</div></div><div><span><span>q </span><span>=</span><span> q.</span><span>view</span><span>(</span></span><span>1</span><span>, </span><span>201</span><span>, </span><span>12</span><span>, </span><span>64</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>view</code> 不会移动任何数据，它只是<strong>重新解释</strong>这 768 个数——把每连续的 64 个数看作一个”头”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>原始内存排列（一维视角）:</span></div></div><div><div><div>2</div></div><div><span>[ 头0_0, 头0_1, ..., 头0_63, 头1_0, ..., 头1_63, ..., 头11_63 ]</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>▲                      ▲                           ▲</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>连续的64个数             下一个连续的64个数           最后64个数</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>view(1, 201, 12, 64) 后:</span></div></div><div><div><div>7</div></div><div><span>(1, 201, 12, 64)</span></div></div><div><div><div>8</div></div><div><span><span>          </span></span><span>↑</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>dim=2 是"头编号"（0~11）</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h5>第 3 步：<code>transpose</code> → 把头编号提到前面<a href="#第-3-步transpose--把头编号提到前面"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># (1, 201, 12, 64)  →  (1, 12, 201, 64)</span></div></div><div><div><div>2</div></div><div><span><span>q </span><span>=</span><span> q.</span><span>transpose</span><span>(</span></span><span>1</span><span>, </span><span>2</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>交换 dim=1（seq_len）和 dim=2（num_heads），这样头的维度就变成 dim=1，方便后续做批量的矩阵乘法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>view 后:    (1, 201, 12, 64)    ← 头在 dim=2</span></div></div><div><div><div>2</div></div><div><span><span>             </span></span><span>↑    ↑    ↑</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>batch seq  head</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>transpose:  (1, 12, 201, 64)    ← 头提到 dim=1</span></div></div><div><div><div>6</div></div><div><span><span>             </span></span><span>↑    ↑    ↑</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>batch head seq</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h5>第 4 步：<code>Q @ K^T</code> → 12 个头<strong>并行</strong>计算<a href="#第-4-步q--kt--12-个头并行计算"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Q: (1, 12, 201, 64),  K: (1, 12, 201, 64)</span></div></div><div><div><div>2</div></div><div><span># Q @ K.transpose(-2, -1)  →  (1, 12, 201, 64) @ (1, 12, 64, 201)</span></div></div><div><div><div>3</div></div><div><span>#                                     ↓</span></div></div><div><div><div>4</div></div><div><span><span>attn </span><span>=</span><span> Q </span><span>@</span><span> K.</span><span>transpose</span><span>(</span><span>-</span></span><span>2</span><span><span>, </span><span>-</span></span><span>1</span><span>)    </span><span># (1, 12, 201, 201)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>PyTorch <strong>把 dim=1 的 12 当作 batch 维度</strong>，一次性做了 12 个并行的 <code>(201, 64) @ (64, 201) → (201, 201)</code> 矩阵乘法。</p><hr /></section><section><h5>一张图总结整个过程<a href="#一张图总结整个过程"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>原始输出:         (1, 201, 768)</span></div></div><div><div><div>2</div></div><div><span><span>                        </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>view(1, 201, 12, 64)  ← 把 768 重新解释为 12×64（不移动数据）</span></div></div><div><div><div>4</div></div><div><span><span>                        </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span><span>                   </span></span><span>(1, 201, 12, 64)</span></div></div><div><div><div>6</div></div><div><span><span>                        </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>transpose(1, 2)      ← 交换 seq 和 head 维度</span></div></div><div><div><div>8</div></div><div><span><span>                        </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span>                   </span></span><span>(1, 12, 201, 64)</span></div></div><div><div><div>10</div></div><div><span><span>                        </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>Q @ K^T             ← 12 个头并行矩阵乘法</span></div></div><div><div><div>12</div></div><div><span><span>                        </span></span><span>│</span></div></div><div><div><div>13</div></div><div><span><span>                   </span></span><span>(1, 12, 201, 201)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>核心理解</strong>：这不是”把 12 个矩阵拼起来”，而是一个<strong>4 维张量</strong>——第 1 维（dim=1）的 12 代表 12 个”通道”，每个通道里存着一个 <code>(201, 201)</code> 的注意力矩阵。PyTorch 把这 12 个通道当成独立批次一次性算完。</p><hr /></section></section></section><section><h3>3.1.4 总结<a href="#314-总结"><span>#</span></a></h3>

<table><thead><tr><th>输出</th><th>形状</th><th>通俗理解</th></tr></thead><tbody><tr><td><code>last_hidden_state</code></td><td><code>(1, 201, 768)</code></td><td>每个 token 的最终表示向量</td></tr><tr><td><code>pooler_output</code></td><td><code>(1, 768)</code></td><td>整个句子的浓缩特征向量</td></tr><tr><td><code>attentions[0]['attn']</code></td><td><code>(1, 12, 201, 201)</code></td><td>第 0 层每个头中，“谁在看谁”的注意力分布</td></tr></tbody></table><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_output[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span>][</span><span>'attn'</span><span>][</span><span>0</span><span>, </span><span>0</span><span>, :, :]</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section></section>
<section><h1>4. from scratch<a href="#4-from-scratch"><span>#</span></a></h1><p>这一节，我们来从零完成一次 Q, K, V 的计算。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>embeddings_output </span><span>=</span><span> model.</span><span>embeddings</span><span>(inputs_tests[</span></span><span>'input_ids'</span><span>], inputs_tests[</span><span>'token_type_ids'</span><span>])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>embeddings_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1686, -0.2858, -0.3261,  ..., -0.0276,  0.0383,  0.1640],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.1172,  0.6055,  0.0487,  ...,  0.5867,  0.8167,  0.4067],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.7412,  0.3854, -0.7550,  ...,  0.5425,  0.5629,  0.6106],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.0679,  0.2560,  0.3443,  ...,  0.5042,  0.4860,  0.3145],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.1079,  0.0740,  0.4233,  ...,  0.2864,  0.5379,  0.1220],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.0594, -0.0563,  0.2673,  ..., -0.7952, -0.0813, -0.6690]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;AddBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一步我们拿到的是 BERT 的 embedding 层输出，即：<strong>词嵌入 + 位置嵌入 + 句子类型嵌入</strong>三者相加的结果。在下面我们可以看到，它的形状是 <code>(1,201,768)</code>：</p><ul>
<li>1 = batch size（1 条文本）</li>
<li>201 = 序列长度</li>
<li>768 = 隐藏层维度</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>embeddings_output.shape </span><span># torch.Size([1, 201, 768])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>Q_first_head_first_layer </span><span>=</span><span> embeddings_output[</span></span><span>0</span><span><span>] </span><span>@</span><span> model.encoder.layer[</span></span><span>0</span><span><span>].attention.self.query.weight[:att_head_size, :].T </span><span>+</span><span> model.encoder.layer[</span></span><span>0</span><span>].attention.self.query.bias[:att_head_size]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>Q_first_head_first_layer.shape </span><span># torch.Size([201, 64])</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Q_first_head_first_layer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[ 0.7090, -0.1532, -0.0324,  ..., -0.1861, -1.1897, -0.3917],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[ 0.9676,  0.0748,  0.2025,  ...,  0.7521,  0.4138,  0.1224],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[ 0.8280, -0.0809,  0.5322,  ...,  0.1444,  0.3582, -0.0987],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[ 0.9873, -0.7626,  0.3701,  ..., -0.7065,  0.7049, -0.3984],</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>[ 1.0845, -0.9601,  0.4284,  ..., -0.8086,  0.8632, -0.7072],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>[ 0.3943, -0.4631, -0.3239,  ..., -0.1489, -0.8662, -0.1141]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;AddBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就手动计算出了第一个注意力头的 Query 向量，各部分含义如下：</p>

<table><thead><tr><th>部分</th><th>含义</th></tr></thead><tbody><tr><td><code>embeddings_output[0]</code></td><td>形状 <code>(201, 768)</code>，去掉 batch 维度，拿到这 201 个 token 的嵌入向量</td></tr><tr><td><code>model.encoder.layer[0]</code></td><td>第 <strong>0</strong> 层（第一层）Transformer</td></tr><tr><td><code>.attention.self.query</code></td><td>该层的 <strong>Q 映射层</strong>，一个 <code>Linear(768 → 768)</code></td></tr><tr><td><code>.weight</code></td><td>Q 的权重矩阵，形状 <code>(768, 768)</code></td></tr><tr><td><code>.weight[:att_head_size, :]</code></td><td><strong>前 64 行</strong>，即第一个注意力头的权重（<span><span>att_head_size=64\text{att\_head\_size} = 64</span><span><span><span></span><span><span>att_head_size</span></span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span>）</td></tr><tr><td><code>.T</code></td><td>转置，形状变为 <code>(768, 64)</code>，为了满足矩阵乘法 <code>x @ W.T</code></td></tr><tr><td><code>.bias[:att_head_size]</code></td><td>偏置的前 64 个元素（第一个头的偏置）</td></tr></tbody></table><p>计算流程如图：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>embeddings_output[0]          query.weight[:64, :].T</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>(201, 768)      @            (768, 64)       +  bias[:64]</span></div></div><div><div><div>3</div></div><div><span><span>                                                    </span></span><span>(64,)</span></div></div><div><div><div>4</div></div><div><span><span>                        </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span><span>                 </span></span><span>Q_first_head_first_layer</span></div></div><div><div><div>6</div></div><div><span><span>                      </span></span><span>(201, 64)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以最终得到的是：<strong>201 个 token，每个 token 用 64 维向量表示</strong>——这正是第一个注意力头的 Query 矩阵。</p><p>这样，我们就直观验证了”多头注意力中的多头是通过权重矩阵的行切片实现的”这一核心概念。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>K_first_head_first_layer </span><span>=</span><span> embeddings_output[</span></span><span>0</span><span><span>] </span><span>@</span><span> model.encoder.layer[</span></span><span>0</span><span><span>].attention.self.key.weight[:att_head_size, :].T </span><span>+</span><span> model.encoder.layer[</span></span><span>0</span><span>].attention.self.key.bias[:att_head_size]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>K_first_head_first_layer.shape </span><span># torch.Size([201, 64])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>K_first_head_first_layer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[ 1.2797,  0.2204,  0.2408,  ...,  0.5843, -0.4191,  0.9194],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[-0.5851, -0.5868, -0.1607,  ...,  1.0473,  0.0725, -0.3103],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[-0.5147, -0.5780,  0.0156,  ...,  0.4617,  0.3585, -0.3786],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[-0.6989,  1.2598,  0.1314,  ...,  0.0398,  0.6206, -1.6981],</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>[-1.0770,  1.2705, -0.0528,  ..., -0.2308,  0.5101, -1.7183],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>[-1.3563,  1.7390, -0.0489,  ..., -0.6310,  0.2154,  0.9688]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;AddBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Key 矩阵与 Query 矩阵同理，唯一不同的就是我们把 <code>query</code> 换成了 <code>key</code>.</p><p>接着我们来看注意力分数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># (201, 64) @ (64, 201) -&gt; (201, 201)</span></div></div><div><div><div>2</div></div><div><span><span>attention_scores </span><span>=</span><span> torch.nn.</span><span>Softmax</span><span>(</span></span><span>dim</span><span>=-</span><span>1</span><span><span>)(Q_first_head_first_layer </span><span>@</span><span> K_first_head_first_layer.T </span><span>/</span><span> (att_head_size </span><span>**</span><span> </span></span><span>0.5</span><span>))</span></div></div><div><div><div>3</div></div><div><span>attention_scores.shape </span><span># torch.Size([201, 201])</span></div></div><div><div><div>4</div></div><div><span>attention_scores</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[0.0053, 0.0109, 0.0052,  ..., 0.0039, 0.0036, 0.0144],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[0.0086, 0.0041, 0.0125,  ..., 0.0045, 0.0041, 0.0071],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[0.0051, 0.0043, 0.0046,  ..., 0.0043, 0.0045, 0.0031],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[0.0010, 0.0023, 0.0055,  ..., 0.0012, 0.0018, 0.0011],</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>[0.0010, 0.0023, 0.0057,  ..., 0.0012, 0.0017, 0.0007],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>[0.0022, 0.0056, 0.0063,  ..., 0.0045, 0.0048, 0.0015]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;SoftmaxBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，这与我们之前 <code>model_output[-1][0]['attn'][0, 0, :, :]</code> 的输出是一模一样的。</p><p>此外，要注意这里的 <code>dim=-1</code> ，它是在 PyTorch 里指定 <strong>Softmax 沿着哪一个维度做归一化</strong>的参数。</p><p><code>Q @ K.T</code> 的形状是 <code>(201, 201)</code>，它是个 <strong>2 维矩阵</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>           </span></span><span>key token 0  key token 1  ...  key token 200</span></div></div><div><div><div>2</div></div><div><span>query 0   [   0.3          0.1       ...     0.05    ]</span></div></div><div><div><div>3</div></div><div><span>query 1   [   0.05         0.4       ...     0.2     ]</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>...</span></div></div><div><div><div>5</div></div><div><span>query 200 [   0.1          0.15      ...     0.6     ]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 PyTorch 中，<code>dim</code> 可以用<strong>正数</strong>或<strong>负数</strong>指定：</p><ul>
<li><code>dim=0</code> = 第 0 维（行）</li>
<li><code>dim=1</code> = 第 1 维（列）</li>
<li><strong><code>dim=-1</code></strong> = <strong>最后一个维度</strong>——对于 2 维矩阵就是 <code>dim=1</code>（列）</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>形状 (201, 201)</span></div></div><div><div><div>2</div></div><div><span><span>       </span></span><span>↑       ↑</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>dim=0   dim=1</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>dim=-1  👈 就是这个</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在注意力分数矩阵中：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>K_token_0  K_token_1  ...  K_token_200</span></div></div><div><div><div>2</div></div><div><span>Q_token_0  [  ↗       ↗               ↗    ]</span></div></div><div><div><div>3</div></div><div><span>Q_token_1  [  ↗       ↗               ↗    ]   ← 对每个 query 行</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>...     [                               ]</span></div></div><div><div><div>5</div></div><div><span>Q_token_200[  ↗       ↗               ↗    ]</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>↑</span></div></div><div><div><div>7</div></div><div><span><span>           </span></span><span>dim=-1：沿最后一维（列方向）做 softmax</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong><code>dim=-1</code> 的含义是：对每一行（每个 query token），把该行中所有列（所有 key token）的分数做 softmax，使得该行的值加起来等于 1。</strong></p><p>为什么用 <code>dim=-1</code> 而不是 <code>dim=0</code>？</p><p>假设我们有一个 query 在关注 “bank” 这个词，它应该把注意力分配给句子中相关的词：</p>

<table><thead><tr><th></th><th>the</th><th>bank</th><th>near</th><th>the</th><th>river</th></tr></thead><tbody><tr><td><strong>bank (query)</strong></td><td>0.05</td><td><strong>0.60</strong></td><td>0.15</td><td>0.05</td><td>0.15</td></tr></tbody></table><p>沿 <strong>dim=-1（行）</strong> softmax → 对 <strong>“bank”这一行</strong>归一化，所有列加起为 1，表示 <strong>“bank”这个词把注意力分散到了哪些词上</strong> ✅ 合理</p><p>如果错误地用了 <strong>dim=0（列）</strong> softmax → 对 <strong>“bank”这一列</strong>归一化，表示 <strong>“哪些词在关注 bank”</strong>——这就不是 Self-Attention 的标准语义了。</p><p>总结一下：</p>

<table><thead><tr><th>写法</th><th>等价的</th><th>效果</th></tr></thead><tbody><tr><td><code>Softmax(dim=-1)</code></td><td><code>Softmax(dim=1)</code></td><td><strong>每一行</strong>变成概率分布，总和为 1</td></tr><tr><td><code>Softmax(dim=0)</code></td><td>—</td><td><strong>每一列</strong>变成概率分布，总和为 1</td></tr></tbody></table><p>在注意力公式 <span><span>softmax(QKTdk)softmax(\frac{QK^T}{\sqrt{d_k}})</span><span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span> 中，我们希望对<strong>每个 query token 去关注所有 key token</strong> 的概率做归一化，所以用 <code>dim=-1</code>（即最后一个维度）——这是注意力机制的标准写法。</p><blockquote><p>使用 <code>dim=-1</code> 比 <code>dim=1</code> 的好处是：<strong>如果你的张量变成 3 维、4 维（比如加了 batch 维度和 head 维度），<code>dim=-1</code> 始终指向”最后一个维度”（序列长度所在的维度），代码更具通用性。</strong></p></blockquote><p>最后我们来看 Value 矩阵：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>V_first_head_first_layer </span><span>=</span><span> embeddings_output[</span></span><span>0</span><span><span>] </span><span>@</span><span> model.encoder.layer[</span></span><span>0</span><span><span>].attention.self.value.weight[:att_head_size, :].T </span><span>+</span><span> model.encoder.layer[</span></span><span>0</span><span>].attention.self.value.bias[:att_head_size]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>V_first_head_first_layer.shape </span><span># torch.Size([201, 64])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>V_first_head_first_layer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[ 2.3786e+00, -9.7945e-02, -2.8436e-01,  ...,  9.6968e-02,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>-1.8537e-01,  2.1132e-01],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[ 9.8886e-02,  2.2942e-01, -3.9613e-01,  ...,  2.0846e-01,</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>-1.1224e-01, -2.1420e-01],</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[-6.3356e-01,  1.1083e+00, -5.2857e-04,  ..., -3.1870e-01,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>7.2633e-02, -1.0239e-01],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[-7.9659e-01, -4.9400e-01, -4.9216e-02,  ..., -3.6446e-01,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>3.1565e-01, -7.1713e-01],</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>[-9.2551e-01, -5.0348e-01, -1.0398e-01,  ..., -2.1418e-01,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>1.6604e-01, -5.9637e-01],</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>[ 1.2915e-01, -7.5899e-03,  1.8397e-01,  ...,  2.6980e-01,</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>1.3651e-01,  1.9180e-01]], grad_fn=&lt;AddBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>attention_embeddings </span><span>=</span><span> attention_scores </span><span>@</span><span> V_first_head_first_layer</span></span></div></div><div><div><div>2</div></div><div><span>attention_embeddings.shape </span><span># torch.Size([201, 64])</span></div></div><div><div><div>3</div></div><div><span>attention_embeddings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[-4.5640e-01,  4.6211e-02,  4.3913e-02,  ..., -2.0099e-02,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>-1.2756e-02,  6.4255e-03],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[-4.5674e-01,  3.4322e-02,  3.2707e-02,  ..., -4.9206e-02,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>1.4976e-02, -3.0628e-02],</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[-4.9474e-01, -2.9539e-04, -7.5374e-04,  ..., -2.0035e-02,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>1.7146e-02, -3.0126e-02],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[-3.7991e-01,  5.2831e-02,  2.2534e-02,  ..., -1.8338e-02,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>-6.9508e-02,  2.1317e-02],</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>[-3.8071e-01,  4.0900e-02,  2.8770e-02,  ..., -2.1192e-02,</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>-5.2893e-02,  1.9734e-02],</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>[-4.7131e-01,  1.0947e-01,  1.1631e-02,  ..., -3.4542e-02,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>-2.3752e-02, -5.0506e-03]], grad_fn=&lt;MmBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就完成了注意力公式的<strong>最后一步</strong>：</p><p><span><span>Attention(Q,K,V)=softmax(QKTdk)⋅VAttention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}}) \cdot V</span><span><span><span></span><span>A</span><span>tt</span><span>e</span><span>n</span><span>t</span><span>i</span><span>o</span><span>n</span><span>(</span><span>Q</span><span>,</span><span></span><span>K</span><span>,</span><span></span><span>V</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>so</span><span>f</span><span>t</span><span>ma</span><span>x</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Q</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span><span></span><span>⋅</span><span></span></span><span><span></span><span>V</span></span></span></span></p><p>具体来说：</p>

<table><thead><tr><th>部分</th><th>形状</th><th>含义</th></tr></thead><tbody><tr><td><code>attention_scores</code></td><td><code>(201, 201)</code></td><td>softmax 后的注意力权重——<strong>每个 query token 关注所有 key token 的概率分布</strong></td></tr><tr><td><code>V_first_head_first_layer</code></td><td><code>(201, 64)</code></td><td><strong>每个 token 的 Value 向量</strong>——每个 token 带有的”内容信息”</td></tr><tr><td><code>attention_embeddings</code></td><td><code>(201, 64)</code></td><td><strong>加权求和后的结果</strong>——每个 token 的注意力输出</td></tr></tbody></table><p>本质上，它其实就是<strong>加权求和</strong>——对于每个 query token，用它的注意力权重去加权聚合所有 token 的 Value 信息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>对第 i 个 query token:</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>attention_embeddings[i] = Σ_j attention_scores[i, j] × V[j]</span></div></div><div><div><div>4</div></div><div><span><span>                            </span></span><span>↑                          ↑</span></div></div><div><div><div>5</div></div><div><span><span>                        </span></span><span>权重(概率)                 Value 向量</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>举个例子，如果 “bank” 这个 token 的注意力分布是：</p><ul>
<li><strong>bank → the</strong>: 0.05</li>
<li><strong>bank → near</strong>: 0.15</li>
<li><strong>bank → river</strong>: 0.15</li>
<li><strong>bank → bank</strong>: 0.60</li>
<li>…其他 ≈ 0.05</li>
</ul><p>那么 “bank” 的输出向量 = <code>0.05×V(the) + 0.15×V(near) + 0.15×V(river) + 0.60×V(bank) + ...</code></p><p>总结一下：</p><p>**<code>attention_embeddings</code> 就是第 0 层中第 1 个注意力头的原始输出，我们总的下一步应该是：</p><ol>
<li>重复同样的操作，计算出头 1 - 头 11 的 <code>attention_embeddings</code>；</li>
<li>然后拼接这 12 个注意力头 -&gt; <code>(201,768)</code>；</li>
<li><code>BertSelfOutput.dense</code> 会做一次 Linear 投影 -&gt; <code>(201,768)</code>；</li>
<li>残差连接 + LayerNorm -&gt; 得到注意力子层的正式输出；</li>
<li>继续经过 FFN (Intermediate + Output) -&gt; 得到这一层的完整输出。</li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-04/</id>
      <title type="text">CH3_01_bge_visualized</title>
      <published>2026-06-19T00:00:00.000Z</published>
      <updated>2026-06-20T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-04/"/>
      <summary type="text">常用多模态嵌入模型，以 bge-visualized-m3 为例</summary>
      <content type="html"><![CDATA[<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> visual_bge.visual_bge.modeling </span><span>import</span><span> Visualized_BGE</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<ul>
<li><code>visual_bge</code> 库：BAAI（北京智源研究院）开源的<strong>多模态检索模型套件</strong>，它扩展了传统的 BGE 文本嵌入模型，使其能够同时编码图像和文本。</li>
<li><code>Visualized_BGE</code> 类：核心模型类，它融合了：
<ul>
<li><strong>视觉编码器</strong>（如 ViT）：提取图像特征。</li>
<li><strong>文本编码器</strong>（BGE 系列，如 <code>bge-base-en-v1.5</code>）：提取文本特征。</li>
<li><strong>对齐映射层</strong>：将图像和文本特征映射到同一个向量空间，使得图文可以直接计算相似度。</li>
</ul>
</li>
</ul>
<p>这个模型可以做：</p>

<table><thead><tr><th>输入类型</th><th>示例</th><th>输出</th></tr></thead><tbody><tr><td>纯文本</td><td><code>"datawhale开源组织的logo"</code></td><td>文本向量</td></tr><tr><td>纯图像</td><td><code>logo.png</code></td><td>图像向量</td></tr><tr><td>图文混合</td><td><code>logo.png</code> + <code>"datawhale开源组织的logo"</code></td><td>融合向量</td></tr></tbody></table>
<p>三种输出都在<strong>同一个语义空间</strong>中，所以你可以：</p>
<ul>
<li>用图像搜图像</li>
<li>用文本搜图像</li>
<li>用图像搜文本</li>
<li>做图文混合检索</li>
</ul>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model </span><span>=</span><span> </span><span>Visualized_BGE</span><span>(</span></span><span>model_name_bge</span><span>=</span><span>"BAAI/bge-base-en-v1.5"</span><span>,</span></div></div><div><div><div>2</div></div><div><span>                      </span><span>model_weight</span><span>=</span><span>"../../models/bge/Visualized_base_en_v1.5.pth"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>model.</span><span>eval</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<p>输出为：</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Visualized_BGE(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(bge_encoder): BertEncoder(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>(self): BertSdpaSelfAttention(</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>16</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>20</div></div><div><span><span>          </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>(output): BertOutput(</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>24</div></div><div><span><span>          </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>25</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>(bge_embeddings): BertEmbeddings(</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>(bge_pooler): BertPooler(</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>(model_visual): CustomCLIP(</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>(visual): EVAVisionTransformer(</span></div></div><div><div><div>43</div></div><div><span><span>      </span></span><span>(patch_embed): PatchEmbed(</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>(proj): Conv2d(3, 768, kernel_size=(16, 16), stride=(16, 16))</span></div></div><div><div><div>45</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>46</div></div><div><span><span>      </span></span><span>(pos_drop): Dropout(p=0.0, inplace=False)</span></div></div><div><div><div>47</div></div><div><span><span>      </span></span><span>(rope): VisionRotaryEmbeddingFast()</span></div></div><div><div><div>48</div></div><div><span><span>      </span></span><span>(blocks): ModuleList(</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>(0-11): 12 x Block(</span></div></div><div><div><div>50</div></div><div><span><span>          </span></span><span>(norm1): LayerNorm((768,), eps=1e-06, elementwise_affine=True)</span></div></div><div><div><div>51</div></div><div><span><span>          </span></span><span>(attn): Attention(</span></div></div><div><div><div>52</div></div><div><span><span>            </span></span><span>(q_proj): Linear(in_features=768, out_features=768, bias=False)</span></div></div><div><div><div>53</div></div><div><span><span>            </span></span><span>(k_proj): Linear(in_features=768, out_features=768, bias=False)</span></div></div><div><div><div>54</div></div><div><span><span>            </span></span><span>(v_proj): Linear(in_features=768, out_features=768, bias=False)</span></div></div><div><div><div>55</div></div><div><span><span>            </span></span><span>(attn_drop): Dropout(p=0.0, inplace=False)</span></div></div><div><div><div>56</div></div><div><span><span>            </span></span><span>(inner_attn_ln): LayerNorm((768,), eps=1e-06, elementwise_affine=True)</span></div></div><div><div><div>57</div></div><div><span><span>            </span></span><span>(proj): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>58</div></div><div><span><span>            </span></span><span>(proj_drop): Dropout(p=0.0, inplace=False)</span></div></div><div><div><div>59</div></div><div><span><span>            </span></span><span>(rope): VisionRotaryEmbeddingFast()</span></div></div><div><div><div>60</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>61</div></div><div><span><span>          </span></span><span>(drop_path): Identity()</span></div></div><div><div><div>62</div></div><div><span><span>          </span></span><span>(norm2): LayerNorm((768,), eps=1e-06, elementwise_affine=True)</span></div></div><div><div><div>63</div></div><div><span><span>          </span></span><span>(mlp): SwiGLU(</span></div></div><div><div><div>64</div></div><div><span><span>            </span></span><span>(w1): Linear(in_features=768, out_features=2048, bias=True)</span></div></div><div><div><div>65</div></div><div><span><span>            </span></span><span>(w2): Linear(in_features=768, out_features=2048, bias=True)</span></div></div><div><div><div>66</div></div><div><span><span>            </span></span><span>(act): SiLU()</span></div></div><div><div><div>67</div></div><div><span><span>            </span></span><span>(ffn_ln): LayerNorm((2048,), eps=1e-06, elementwise_affine=True)</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>(w3): Linear(in_features=2048, out_features=768, bias=True)</span></div></div><div><div><div>69</div></div><div><span><span>            </span></span><span>(drop): Dropout(p=0.0, inplace=False)</span></div></div><div><div><div>70</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>71</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>73</div></div><div><span><span>      </span></span><span>(norm): LayerNorm((768,), eps=1e-06, elementwise_affine=True)</span></div></div><div><div><div>74</div></div><div><span><span>      </span></span><span>(head): Linear(in_features=768, out_features=512, bias=True)</span></div></div><div><div><div>75</div></div><div><span><span>      </span></span><span>(patch_dropout): PatchDropout()</span></div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>(text): None</span></div></div><div><div><div>78</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>79</div></div><div><span><span>  </span></span><span>(visual_proj): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>80</div></div><div><span><span>  </span></span><span>(cross_entropy): CrossEntropyLoss()</span></div></div><div><div><div>81</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<section><h1>1. <code>Visualized_BGE</code> 模型的架构<a href="#1-visualized_bge-模型的架构"><span>#</span></a></h1><p>BGE 模型实际上是 BERT 的衍生，所以我们可以看到在<a href="https://hac.he.cn/posts/BERT-01/">动手写 BERT 系列</a>中与 BERT 模型类似的架构。</p><div><div><div><div><span><p>🖼️ 图像分支</p></span></div><div><span><p>📝 文本分支</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>输入文本</p></span></div><div><span><p>bge_embeddings</p></span></div><div><span><p>bge_encoder</p></span></div><div><span><p>bge_pooler<br /><br />768维</p></span></div><div><span><p>输入图片</p></span></div><div><span><p>PatchEmbed</p></span></div><div><span><p>12层 EVAViT</p></span></div><div><span><p>LayerNorm</p></span></div><div><span><p>image_head<br /><br />输出 512维</p></span></div><div><span><p>text_proj<br /><br />Linear 768→768</p></span></div><div><span><p>visual_proj<br /><br />Linear 512→768</p></span></div><div><span><p>输出向量<br /><br />768维统一空间</p></span></div>
</div><div><div><span><p>🖼️ 图像分支</p></span></div><div><span><p>📝 文本分支</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>输入文本</p></span></div><div><span><p>bge_embeddings</p></span></div><div><span><p>bge_encoder</p></span></div><div><span><p>bge_pooler<br /><br />768维</p></span></div><div><span><p>输入图片</p></span></div><div><span><p>PatchEmbed</p></span></div><div><span><p>12层 EVAViT</p></span></div><div><span><p>LayerNorm</p></span></div><div><span><p>image_head<br /><br />输出 512维</p></span></div><div><span><p>text_proj<br /><br />Linear 768→768</p></span></div><div><span><p>visual_proj<br /><br />Linear 512→768</p></span></div><div><span><p>输出向量<br /><br />768维统一空间</p></span></div>
</div></div></div><section><h3>1️⃣ <code>bge_embeddings</code> — 文本嵌入层<a href="#1️⃣-bge_embeddings--文本嵌入层"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>BertEmbeddings</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>word_embeddings:       </span><span>Embedding</span><span>(</span><span>30522</span><span>, </span><span>768</span><span>)   </span><span># 词表大小 30522，每个词映射为 768 维</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>position_embeddings:   </span><span>Embedding</span><span>(</span><span>512</span><span>, </span><span>768</span><span>)     </span><span># 位置编码，最长 512 token</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>token_type_embeddings: </span><span>Embedding</span><span>(</span><span>2</span><span>, </span><span>768</span><span>)       </span><span># 区分两个句子的类型编码</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>LayerNorm</span><span>(</span><span>768</span><span>)                                 </span><span># 层归一化</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>Dropout</span><span>(</span><span>p</span><span>=</span><span>0.1</span><span>)                                 </span><span># 防止过拟合</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>作用</strong>：把输入的文本 token ID 转换为 768 维的向量，并加上位置信息和层归一化。</p><hr /></section><section><h3>2️⃣ <code>bge_encoder</code> — 文本编码器（核心）<a href="#2️⃣-bge_encoder--文本编码器核心"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertEncoder(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>layer: ModuleList(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(0-11): 12 × BertLayer  # 12 层 Transformer</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每层 <code>BertLayer</code> 包含：</p>

<table><thead><tr><th>子模块</th><th>组件</th><th>说明</th></tr></thead><tbody><tr><td><strong>attention</strong></td><td><code>BertSdpaSelfAttention</code></td><td>自注意力 (<code>Scaled Dot-Product Attention</code>)</td></tr><tr><td></td><td><code>query/Key/Value: Linear(768→768)</code></td><td>Q、K、V 线性投影</td></tr><tr><td></td><td><code>BertSelfOutput</code></td><td>残差连接 + LayerNorm + Dropout</td></tr><tr><td><strong>intermediate</strong></td><td><code>Linear(768→3072) + GELU</code></td><td>前馈网络放大 4 倍</td></tr><tr><td><strong>output</strong></td><td><code>Linear(3072→768)</code></td><td>缩回 768 维</td></tr></tbody></table><p><strong>作用</strong>：这 12 层 Transformer 对文本进行上下文建模，每个 token 都能”看到”句子中的其他 token。</p><p>这里可以看到，BGE 模型的隐藏层架构与 BERT 是一致的，都是第 0 层 embedding 层 + 12 层 Transformer 层。</p><hr /></section><section><h3>3️⃣ <code>bge_pooler</code> — 池化层<a href="#3️⃣-bge_pooler--池化层"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>BertPooler</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>dense: </span><span>Linear</span><span>(</span><span>768</span><span>→</span><span>768</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>activation: </span><span>Tanh</span><span>()</span></div></div><div><div><div>4</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>作用</strong>：取出 <code>[CLS]</code> 位置的向量，经过一个全连接层 + Tanh 激活，得到整句话的<strong>句向量</strong>。</p><hr /></section><section><h3>4️⃣ <code>model_visual</code> — 图像编码器（Visual 分支）<a href="#4️⃣-model_visual--图像编码器visual-分支"><span>#</span></a></h3><p>这是 <strong>EVA-CLIP</strong> 的视觉 Transformer（ViT），具体是 <code>EVAVisionTransformer</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>CustomCLIP</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>visual: </span><span>EVAVisionTransformer</span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>patch_embed: </span><span>PatchEmbed</span><span>(</span><span>proj</span><span><span>=</span><span>Conv2d</span><span>(</span></span><span>3</span><span>, </span><span>768</span><span>, </span><span>16</span><span>×</span><span>16</span><span>))</span></div></div><div><div><div>4</div></div><div><span>    </span><span># 将 224×224 图片切成 14×14 个 16×16 的 patch，投影为 768 维</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内部结构：</p>

<table><thead><tr><th>组件</th><th>说明</th></tr></thead><tbody><tr><td><code>patch_embed</code></td><td><code>Conv2d(3→768, kernel=16×16, stride=16×16)</code> 将图片切成非重叠 patch，每个 patch → 768 维向量</td></tr><tr><td><code>pos_drop</code></td><td><code>Dropout(p=0.0)</code> 占位，实际未使用</td></tr><tr><td><code>rope</code></td><td><code>VisionRotaryEmbeddingFast()</code> — <strong>旋转位置编码</strong>（RoPE），比传统绝对位置编码效果更好</td></tr><tr><td><code>blocks</code></td><td><strong>12 层 Block（EVAViT）</strong>，每层包含：</td></tr><tr><td>├─ <code>norm1</code></td><td><code>LayerNorm(768)</code></td></tr><tr><td>├─ <code>attn</code></td><td><code>Attention</code> — 自注意力</td></tr><tr><td>│ ├─ <code>q/k/v_proj</code></td><td><code>Linear(768→768, bias=False)</code></td></tr><tr><td>│ ├─ <code>inner_attn_ln</code></td><td><code>LayerNorm(768)</code> — 注意力内部归一化</td></tr><tr><td>│ ├─ <code>proj</code></td><td><code>Linear(768→768)</code> 输出投影</td></tr><tr><td>│ └─ <code>rope</code></td><td>注意力内的 RoPE</td></tr><tr><td>├─ <code>drop_path</code></td><td><code>Identity()</code> — 随机深度，此处未生效</td></tr><tr><td>├─ <code>norm2</code></td><td><code>LayerNorm(768)</code></td></tr><tr><td>└─ <code>mlp</code></td><td><strong>SwiGLU</strong> — 比传统 FFN 更先进的 MLP</td></tr><tr><td>│ ├─ <code>w1</code>/<code>w2</code></td><td><code>Linear(768→2048)</code> — 两个门控分支</td></tr><tr><td>│ ├─ <code>act</code></td><td><code>SiLU()</code> — 激活函数</td></tr><tr><td>│ ├─ <code>ffn_ln</code></td><td><code>LayerNorm(2048)</code></td></tr><tr><td>│ └─ <code>w3</code></td><td><code>Linear(2048→768)</code> 输出</td></tr><tr><td><code>norm</code></td><td><code>LayerNorm(768)</code> — 最后归一化</td></tr><tr><td><code>head</code></td><td><code>Linear(768→512)</code> — 映射到 512 维视觉特征</td></tr><tr><td><code>patch_dropout</code></td><td><code>PatchDropout()</code> — 训练时随机丢弃 patch 做数据增强</td></tr></tbody></table><blockquote><p>💡 <strong>SwiGLU</strong> = <code>SiLU(x·W1) ⊗ (x·W2)</code>，是 Meta 在 LLaMA 中使用的先进 MLP 结构，效果优于传统 ReLU+GELU。</p></blockquote><hr /></section><section><h3>5️⃣ <code>visual_proj</code> — 视觉投影层<a href="#5️⃣-visual_proj--视觉投影层"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>Linear</span><span>(</span></span><span>in_features</span><span>=</span><span>768</span><span>, </span><span>out_features</span><span>=</span><span>768</span><span>, </span><span>bias</span><span>=</span><span>True</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>作用</strong>：将图像分支输出的 512 维向量通过 head 映射到 768 维后，再经过此投影层，与文本分支的 768 维向量<strong>对齐到同一空间</strong>。</p><hr /></section><section><h3>6️⃣ <code>cross_entropy</code> — 损失函数<a href="#6️⃣-cross_entropy--损失函数"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>CrossEntropyLoss</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>作用</strong>：训练时用于计算图文对比损失（Contrastive Loss），让匹配的图文对距离更近，不匹配的拉远。</p><hr /></section><section><h3>1.7 整体数据流总结<a href="#17-整体数据流总结"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>文本:  token → [bge_embeddings → 12层 BERT → bge_pooler] → 768维文本向量</span></div></div><div><div><div>2</div></div><div><span>图片:  pixels → [PatchEmbed → 12层 EVAViT → head → visual_proj] → 768维图像向量</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个分支最终都输出 <strong>768 维向量</strong>，处在同一语义空间，所以可以直接计算相似度。训练时通过 <code>CrossEntropyLoss</code> 做对比学习，让图文对对齐。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>text_emb </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>text</span><span>=</span><span>"datawhale开源组织的logo"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>img_emb_1 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale01.png"</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>multi_emb_1 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale01.png"</span><span>, </span><span>text</span><span>=</span><span>"datawhale开源组织的logo"</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>img_emb_2 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale02.png"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>multi_emb_2 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale02.png"</span><span>, </span><span>text</span><span>=</span><span>"datawhale开源组织的logo"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 计算相似度</span></div></div><div><div><div>2</div></div><div><span><span>sim_1 </span><span>=</span><span> img_emb_1 </span><span>@</span><span> img_emb_2.T</span></span></div></div><div><div><div>3</div></div><div><span><span>sim_2 </span><span>=</span><span> img_emb_1 </span><span>@</span><span> multi_emb_1.T</span></span></div></div><div><div><div>4</div></div><div><span><span>sim_3 </span><span>=</span><span> text_emb </span><span>@</span><span> multi_emb_1.T</span></span></div></div><div><div><div>5</div></div><div><span><span>sim_4 </span><span>=</span><span> multi_emb_1 </span><span>@</span><span> multi_emb_2.T</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h1>2. 相似度的数学计算<a href="#2-相似度的数学计算"><span>#</span></a></h1><p>代码中的 <code>@</code> 运算符是 <strong>矩阵乘法</strong>（即点积 / 内积）。在 PyTorch 中，每个 <code>encode()</code> 的输出形状都是 <code>(1, 768)</code>——即 1 个 768 维的向量。<code>.T</code> 是转置操作，把行向量变成列向量。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>img_emb_1    形状: (1, 768)</span></div></div><div><div><div>2</div></div><div><span>img_emb_2.T  形状: (768, 1)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>img_emb_1 @ img_emb_2.T  → 形状: (1, 1)   ← 一个标量相似度值</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><section><h2>2.1 数学公式<a href="#21-数学公式"><span>#</span></a></h2><p>对于两个 768 维的向量 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 和 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>：</p><span><span><span>a=[a1,a2,...,a768],b=[b1,b2,...,b768]\mathbf{a} = [a_1, a_2, ..., a_{768}], \quad
\mathbf{b} = [b_1, b_2, ..., b_{768}]</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span>,</span><span></span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span></span><p>它们的<strong>点积</strong>（内积）定义为：</p><span><span><span>sim(a,b)=a⋅b=∑i=1768ai×bi\text{sim}(\mathbf{a}, \mathbf{b}) = \mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{768} a_i \times b_i</span><span><span><span></span><span><span>sim</span></span><span>(</span><span>a</span><span>,</span><span></span><span>b</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>⋅</span><span></span></span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>用矩阵乘法的方式写就是：</p><span><span><span>a@bT=[a1,a2,...,a768]⋅[b1b2⋮b768]=a1b1+a2b2+⋯+a768b768\mathbf{a} @ \mathbf{b}^T = [a_1, a_2, ..., a_{768}] \cdot \begin{bmatrix} b_1 \\ b_2 \\ \vdots \\ b_{768} \end{bmatrix} = a_1b_1 + a_2b_2 + \cdots + a_{768}b_{768}</span><span><span><span></span><span>a</span><span>@</span><span><span>b</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>768</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><hr /></section><section><h2>2.2 向量点积的几何意义<a href="#22-向量点积的几何意义"><span>#</span></a></h2><p>点积在几何上的含义是：</p><span><span><span>a⋅b=∥a∥∥b∥cos⁡θ\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\| \|\mathbf{b}\| \cos\theta</span><span><span><span></span><span>a</span><span></span><span>⋅</span><span></span></span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>a</span><span>∥∥</span><span>b</span><span>∥</span><span></span><span>cos</span><span></span><span>θ</span></span></span></span></span><p>其中：</p><ul>
<li><span><span>∥a∥\|\mathbf{a}\|</span><span><span><span></span><span>∥</span><span>a</span><span>∥</span></span></span></span> 是向量 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 的<strong>长度</strong>（模）</li>
<li><span><span>∥b∥\|\mathbf{b}\|</span><span><span><span></span><span>∥</span><span>b</span><span>∥</span></span></span></span> 是向量 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 的<strong>长度</strong></li>
<li><span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 是两向量之间的<strong>夹角</strong></li>
</ul><p>如果两个向量<strong>方向一致</strong>（夹角接近 <span><span>0∘0^\circ</span><span><span><span></span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>），<span><span>cos⁡θ≈1\cos\theta \approx 1</span><span><span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span></span></span></span>，点积最大 → 相似度高。</p><p>如果两个向量<strong>方向相反</strong>（夹角接近 <span><span>180∘180^\circ</span><span><span><span></span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>），<span><span>cos⁡θ≈−1\cos\theta \approx -1</span><span><span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>，点积最小 → 相似度低。</p><p>如果两个向量<strong>垂直</strong>（夹角 <span><span>90∘90^\circ</span><span><span><span></span><span>9</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>），<span><span>cos⁡θ=0\cos\theta = 0</span><span><span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，点积为 0 → 毫不相关。</p><hr /></section><section><h2>2.3 结合代码具体分析<a href="#23-结合代码具体分析"><span>#</span></a></h2><p>上面的四组计算可以这样理解：</p>

<table><thead><tr><th>代码</th><th>含义</th><th>几何理解</th></tr></thead><tbody><tr><td><code>sim_1 = img_emb_1 @ img_emb_2.T</code></td><td>图1 vs 图2 的相似度</td><td>两张图片的向量夹角</td></tr><tr><td><code>sim_2 = img_emb_1 @ multi_emb_1.T</code></td><td>图1 vs 图文结合1</td><td>纯图片 vs 融合了文本信息的图片</td></tr><tr><td><code>sim_3 = text_emb @ multi_emb_1.T</code></td><td>纯文本 vs 图文结合1</td><td>纯文本 vs 融合向量</td></tr><tr><td><code>sim_4 = multi_emb_1 @ multi_emb_2.T</code></td><td>结合1 vs 结合2</td><td>两个不同图片的融合向量</td></tr></tbody></table><p>所有向量都处于 <strong>768 维的统一语义空间</strong>，所以任意两者都能直接计算点积。</p><hr /></section><section><h2>2.4 一个直观的小例子<a href="#24-一个直观的小例子"><span>#</span></a></h2><p>假设二维空间中有两个向量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>a = [3, 4]</span></div></div><div><div><div>2</div></div><div><span>b = [1, 2]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>a · b = 3×1 + 4×2 = 3 + 8 = 11</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>维度越高（这里是 768 维），计算方式完全一样——只是求和项更多而已。</p><hr /></section><section><h2>2.5 补充：点积 vs 余弦相似度<a href="#25-补充点积-vs-余弦相似度"><span>#</span></a></h2><p>你可能会想：为什么不用更常见的<strong>余弦相似度</strong>？</p><span><span><span>cos⁡θ=a⋅b∥a∥∥b∥\cos\theta = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|}</span><span><span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∥</span><span>a</span><span>∥∥</span><span>b</span><span>∥</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>⋅</span><span></span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>因为 Visualized_BGE 输出的向量已经<strong>做过归一化</strong>（L2 normalize），即 <span><span>∥a∥=∥b∥=1\|\mathbf{a}\| = \|\mathbf{b}\| = 1</span><span><span><span></span><span>∥</span><span>a</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>b</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，所以：</p><span><span><span>a⋅b=1×1×cos⁡θ=cos⁡θ\mathbf{a} \cdot \mathbf{b} = 1 \times 1 \times \cos\theta = \cos\theta</span><span><span><span></span><span>a</span><span></span><span>⋅</span><span></span></span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>cos</span><span></span><span>θ</span></span></span></span></span><p><strong>点积 = 余弦相似度</strong>，二者等价。所以代码中直接算点积就够了，不需要再除以模长。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>"=== 相似度计算结果 ==="</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"纯图像 vs 纯图像: </span><span>{</span><span>sim_1</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>f</span><span>"图文结合1 vs 纯图像: </span><span>{</span><span>sim_2</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>f</span><span>"图文结合1 vs 纯文本: </span><span>{</span><span>sim_3</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>print</span><span>(</span><span>f</span><span>"图文结合1 vs 图文结合2: </span><span>{</span><span>sim_4</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=== 相似度计算结果 ===</span></div></div><div><div><div>2</div></div><div><span>纯图像 vs 纯图像: tensor([[0.8318]])</span></div></div><div><div><div>3</div></div><div><span>图文结合1 vs 纯图像: tensor([[0.8291]])</span></div></div><div><div><div>4</div></div><div><span>图文结合1 vs 纯文本: tensor([[0.7627]])</span></div></div><div><div><div>5</div></div><div><span>图文结合1 vs 图文结合2: tensor([[0.9058]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 向量信息分析</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"</span><span>\n</span><span>=== 嵌入向量信息 ==="</span><span>)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>f</span><span>"多模态向量维度: </span><span>{</span><span>multi_emb_1.shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>f</span><span>"图像向量维度: </span><span>{</span><span>img_emb_1.shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>print</span><span>(</span><span>f</span><span>"多模态向量示例 (前10个元素): </span><span>{</span><span>multi_emb_1[</span><span>0</span><span>][:</span><span>10</span><span>]</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(</span><span>f</span><span>"图像向量示例 (前10个元素):   </span><span>{</span><span>img_emb_1[</span><span>0</span><span>][:</span><span>10</span><span>]</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=== 嵌入向量信息 ===</span></div></div><div><div><div>2</div></div><div><span>多模态向量维度: torch.Size([1, 768])</span></div></div><div><div><div>3</div></div><div><span>图像向量维度: torch.Size([1, 768])</span></div></div><div><div><div>4</div></div><div><span>多模态向量示例 (前10个元素): tensor([ 0.0360, -0.0032, -0.0377,  0.0240,  0.0140,  0.0340,  0.0148,  0.0292,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>0.0060, -0.0145])</span></div></div><div><div><div>6</div></div><div><span>图像向量示例 (前10个元素):   tensor([ 0.0407, -0.0606, -0.0037,  0.0073,  0.0305,  0.0318,  0.0132,  0.0442,</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>-0.0380, -0.0270])</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h1>3. 对比实验<a href="#3-对比实验"><span>#</span></a></h1><p>我们可以尝试把 <code>datawhale开源组织的logo</code> 替换为 <code>blue whale</code>，看看结果有什么不同。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>text_emb </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>text</span><span>=</span><span>"blue whale"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>img_emb_1 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale01.png"</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>multi_emb_1 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale01.png"</span><span>, </span><span>text</span><span>=</span><span>"blue whale"</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>img_emb_2 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale02.png"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>multi_emb_2 </span><span>=</span><span> model.</span><span>encode</span><span>(</span><span>image</span><span>=</span><span>"../../data/C3/imgs/datawhale02.png"</span><span>, </span><span>text</span><span>=</span><span>"blue whale"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 计算相似度</span></div></div><div><div><div>2</div></div><div><span><span>sim_1 </span><span>=</span><span> img_emb_1 </span><span>@</span><span> img_emb_2.T</span></span></div></div><div><div><div>3</div></div><div><span><span>sim_2 </span><span>=</span><span> img_emb_1 </span><span>@</span><span> multi_emb_1.T</span></span></div></div><div><div><div>4</div></div><div><span><span>sim_3 </span><span>=</span><span> text_emb </span><span>@</span><span> multi_emb_1.T</span></span></div></div><div><div><div>5</div></div><div><span><span>sim_4 </span><span>=</span><span> multi_emb_1 </span><span>@</span><span> multi_emb_2.T</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>"=== 相似度计算结果 ==="</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"纯图像 vs 纯图像: </span><span>{</span><span>sim_1</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>f</span><span>"图文结合1 vs 纯图像: </span><span>{</span><span>sim_2</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>f</span><span>"图文结合1 vs 纯文本: </span><span>{</span><span>sim_3</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>print</span><span>(</span><span>f</span><span>"图文结合1 vs 图文结合2: </span><span>{</span><span>sim_4</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=== 相似度计算结果 ===</span></div></div><div><div><div>2</div></div><div><span>纯图像 vs 纯图像: tensor([[0.8318]])</span></div></div><div><div><div>3</div></div><div><span>图文结合1 vs 纯图像: tensor([[0.9218]])</span></div></div><div><div><div>4</div></div><div><span>图文结合1 vs 纯文本: tensor([[0.7572]])</span></div></div><div><div><div>5</div></div><div><span>图文结合1 vs 图文结合2: tensor([[0.8719]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 向量信息分析</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"</span><span>\n</span><span>=== 嵌入向量信息 ==="</span><span>)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>f</span><span>"多模态向量维度: </span><span>{</span><span>multi_emb_1.shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>f</span><span>"图像向量维度: </span><span>{</span><span>img_emb_1.shape</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>print</span><span>(</span><span>f</span><span>"多模态向量示例 (前10个元素): </span><span>{</span><span>multi_emb_1[</span><span>0</span><span>][:</span><span>10</span><span>]</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(</span><span>f</span><span>"图像向量示例 (前10个元素):   </span><span>{</span><span>img_emb_1[</span><span>0</span><span>][:</span><span>10</span><span>]</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=== 嵌入向量信息 ===</span></div></div><div><div><div>2</div></div><div><span>多模态向量维度: torch.Size([1, 768])</span></div></div><div><div><div>3</div></div><div><span>图像向量维度: torch.Size([1, 768])</span></div></div><div><div><div>4</div></div><div><span>多模态向量示例 (前10个元素): tensor([ 0.0277, -0.0514, -0.0233,  0.0135,  0.0278,  0.0198,  0.0089,  0.0285,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>-0.0357, -0.0242])</span></div></div><div><div><div>6</div></div><div><span>图像向量示例 (前10个元素):   tensor([ 0.0407, -0.0606, -0.0037,  0.0073,  0.0305,  0.0318,  0.0132,  0.0442,</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>-0.0380, -0.0270])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们来对比一下两组输出：</p><section><h2>3.1 结果对比<a href="#31-结果对比"><span>#</span></a></h2>

<table><thead><tr><th>相似度</th><th>文本=“datawhale开源组织的logo”</th><th>文本=“blue whale”</th><th>变化</th></tr></thead><tbody><tr><td><strong>sim₁</strong> 纯图像 vs 纯图像</td><td><strong>0.8318</strong></td><td><strong>0.8318</strong></td><td>→ 不变</td></tr><tr><td><strong>sim₂</strong> 图文结合1 vs 纯图像</td><td><strong>0.8291</strong></td><td><strong>0.9218</strong></td><td>↑ 上升</td></tr><tr><td><strong>sim₃</strong> 图文结合1 vs 纯文本</td><td><strong>0.7627</strong></td><td><strong>0.7572</strong></td><td>↓ 略降</td></tr><tr><td><strong>sim₄</strong> 图文结合1 vs 图文结合2</td><td><strong>0.9058</strong></td><td><strong>0.8719</strong></td><td>↓ 下降</td></tr></tbody></table><hr /></section><section><h2>3.2 逐一分析<a href="#32-逐一分析"><span>#</span></a></h2><section><h3>3.2.1 sim₁ — 不变（0.8318）<a href="#321-sim--不变08318"><span>#</span></a></h3><p>两张图片没有换过，纯图像向量只取决于图片本身，与文本无关，所以结果完全一致 ✅</p><hr /></section><section><h3>3.2.2 sim₂ — 显著上升（0.8291 → 0.9218）<a href="#322-sim--显著上升08291--09218"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>图文结合1 = 图片 datawhale01 + 文本描述</span></div></div><div><div><div>2</div></div><div><span>纯图像   = 图片 datawhale01 的纯图像向量</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个都是基于同一张图片 <code>datawhale01</code> 的向量，只是多模态版本额外融合了文本信息。</p><ul>
<li><strong>「datawhale开源组织的logo」</strong>：这个描述比较<strong>抽象</strong>——它说的是”一个组织的 logo”，而不是图片里的视觉内容。模型需要去理解”logo”是一种抽象标识，与图片里具体画的是什么（一头鲸鱼）存在语义差距。</li>
<li><strong>「blue whale」</strong>：这个描述<strong>直接对应了图片的视觉内容</strong>——Datawhale 的 logo 正是一头蓝色的鲸鱼！文本和图片在语义上高度一致。</li>
</ul><p>当文本描述精确匹配图片内容时，融合向量 <code>multi_emb_1</code> 会<strong>更靠近纯图像向量</strong>，所以 sim₂ 大幅上升。</p><hr /></section><section><h3>3.2.3 sim₃ — 略微下降（0.7627 → 0.7572）<a href="#323-sim--略微下降07627--07572"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>图文结合1 = 图片 datawhale01 + 文本描述</span></div></div><div><div><div>2</div></div><div><span>纯文本   = 文本描述的纯文本向量（不含图片信息）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>两种文本下，融合向量都包含了图片信息，而纯文本只是文本，两者天然有差异。</li>
<li>当文本改成「blue whale」后，融合向量被图片内容**“拉”得更偏向图像侧**，反而与<strong>纯文本向量</strong>的距离稍微拉远了，所以 sim₃ 小幅下降。</li>
</ul><hr /></section><section><h3>3.2.4 sim₄ — 下降（0.9058 → 0.8719）<a href="#324-sim--下降09058--08719"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>图文结合1 = datawhale01 + 文本</span></div></div><div><div><div>2</div></div><div><span>图文结合2 = datawhale02 + 文本</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两张图都是 Datawhale 的 logo，但 <code>datawhale01</code> 和 <code>datawhale02</code> 是<strong>不同版本</strong>的图片（可能颜色、构图、文字不同）。</p><ul>
<li><strong>「datawhale开源组织的logo」</strong>：文本强调了”同一个组织”的共同身份，相当于给两个融合向量加上了<strong>共同的主题锚点</strong>，让它们在高维空间里靠得更近。</li>
<li><strong>「blue whale」</strong>：这是一个通用的视觉描述词，不再强调”同属一个组织”，反而突出了两个融合向量之间<strong>图片本身的差异</strong>（不同形象的鲸鱼 logo），所以相似度降低了。</li>
</ul><hr /></section></section><section><h2>3.3 一句话总结<a href="#33-一句话总结"><span>#</span></a></h2><blockquote><p>文本描述越<strong>贴合图片的实际视觉内容</strong>，融合向量的<strong>图像成分就越突出</strong>，使得图像与融合向量的相似度（sim₂）更高；反之，文本越<strong>强调图片之间的共同语义标签</strong>，不同图片的融合向量之间就越相似（sim₄）。</p></blockquote></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-03/</id>
      <title type="text">CH2_02_character_splitter</title>
      <published>2026-06-18T00:00:00.000Z</published>
      <updated>2026-06-18T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-03/"/>
      <summary type="text">LangChain 中文本分割器的几种核心策略</summary>
      <content type="html"><![CDATA[<p>LangChain 提供了丰富且易于使用的文本分割器（Text Splitters），我们将介绍几种最核心的策略。</p>
<section><h1>1. 固定大小分块<a href="#1-固定大小分块"><span>#</span></a></h1><p>这是最简单直接的分块方法。根据 LangChain 源码，这种方法的工作原理分为两个主要阶段：</p><p>（1）<strong>按段落分割</strong>：<code>CharacterTextSplitter</code> 采用默认分隔符 <code>"\n\n"</code>，使用正则表达式将文本按段落进行分割，通过 <code>_split_text_with_regex</code> 函数处理。</p><p>（2）<strong>智能合并</strong>：调用继承自父类的 <code>_merge_splits</code> 方法，将分割后的段落依次合并。该方法会监控累积长度，当超过 <code>chunk_size</code> 时形成新块，并通过重叠机制（<code>chunk_overlap</code>）保持上下文连续性，同时在必要时发出超长块的警告。</p><p>需要注意，<code>CharacterTextSplitter</code> 实际实现的并非严格的固定大小分块。根据 <code>_merge_splits</code> 源码逻辑，这种方法会：</p><ul>
<li><strong>优先保持段落完整性</strong>：只有当添加新段落会导致总长度超过 <code>chunk_size</code> 时，才会结束当前块</li>
<li><strong>处理超长段落</strong>：如果单个段落超过 <code>chunk_size</code>，系统会发出警告但仍将其作为完整块保留</li>
<li><strong>应用重叠机制</strong>：通过 <code>chunk_overlap</code> 参数在块之间保持内容重叠，确保上下文连续性</li>
</ul><p>所以，LangChain 的实现更准确地应该称为”段落感知的自适应分块”，块大小会根据段落边界动态调整。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> langchain.text_splitter </span><span>import</span><span> CharacterTextSplitter</span></div></div><div><div><div>2</div></div><div><span>from</span><span> langchain_community.document_loaders </span><span>import</span><span> TextLoader</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 文档加载</span></div></div><div><div><div>2</div></div><div><span><span>loader </span><span>=</span><span> </span><span>TextLoader</span><span>(</span></span><span>"../../data/C2/txt/蜂医.txt"</span><span>, </span><span>encoding</span><span>=</span><span>"utf-8"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>docs </span><span>=</span><span> loader.</span><span>load</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 2. 初始化固定大小分块器</span></div></div><div><div><div>2</div></div><div><span><span>text_splitter </span><span>=</span><span> </span><span>CharacterTextSplitter</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>chunk_size</span><span>=</span><span>200</span><span>,    </span><span># 每个块的大小</span></div></div><div><div><div>4</div></div><div><span>    </span><span>chunk_overlap</span><span>=</span><span>10</span><span>   </span><span># 块之间的重叠大小</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们将 <code>chunk_size</code> 设为 200，虽然从下面能看出，实际块的长度并不是严格的 200，而是在它附近波动（块1=72，块2=201，块3=189，块4=133…）。</p><p>这正是因为如我们开头所说，<code>CharacterTextSplitter</code> 的工作方式是：</p><ol>
<li>先用段落分隔符 <code>\n\n</code> 把文本切成段落；</li>
<li>然后 <strong>逐段合并</strong>，每合并一段就检查长度，如果刚好超过 <code>chunk_size</code>，就把前面的内容作为一个 chunk 输出。</li>
</ol><p>所以这实际上就是一种 <strong>段落感知的自适应分块</strong>，优先保证段落的完整性。</p><hr /><p>关于 <code>chunk_overlap</code>，我们将它设为 10，表示相邻两个 chunk 之间会有 10 个字符的重叠。</p><p>这主要是为了 <strong>保持上下文的连续性</strong>，第一个 chunk 的结尾会出现在第二个 chunk 的开头，这样我们就不会在分块的分割处丢失信息。</p><p>需要注意的是，这里的单位指的既不是 word，也不是 token，而是字符数（characters）. 我们在下面的输出中可以看到，<code>len=72</code> 表示 72 个字符（每个汉字、标点各算 1 个字符）。</p><p>如果我们希望按 token 分块的话，应该用基于 tiktoken 的分割器，例如 <code>CharacterTextSplitter.from_tiktoken_encoder(chunk_size=500, chunk_overlap=50)</code>，这样 <code>chunk_size</code> 和 <code>chunk_overlap</code> 的单位就是 tokens 了。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 3. 执行分块</span></div></div><div><div><div>2</div></div><div><span><span>chunks </span><span>=</span><span> text_splitter.</span><span>split_documents</span><span>(docs)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Created a chunk of size 201, which is longer than the specified 200</span></div></div><div><div><div>2</div></div><div><span>Created a chunk of size 277, which is longer than the specified 200</span></div></div><div><div><div>3</div></div><div><span>Created a chunk of size 296, which is longer than the specified 200</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>需要注意的是，上述输出并不代表我们只分出了 3 个 chunks. 上面的输出只是警告，只在 <strong>单个段落已超长</strong> 时触发。</p><p>也就是说：</p><ul>
<li>如果没超过 -&gt; 继续合并下一个段落，不产生警告；</li>
<li>如果超过了 -&gt; 把前面的内容作为一个 chunk 输出，<strong>也不产生警告</strong>；</li>
<li>但如果这一个段落 <strong>本身就超过 200</strong> -&gt; 触发警告。</li>
</ul><p>所以，从下面我们可以看出，我们实际上分出了 14 个 chunk. 只有 3 个警告的原因是这 14 个 chunk 中，大多数 chunk 都是由多个小段落合并而成的（总长度 <span><span>≤200\le 200</span><span><span><span></span><span>≤</span><span></span></span><span><span></span><span>200</span></span></span></span> 或略超 200）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 4. 打印结果</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"文本被切分为 </span><span>{</span><span>len</span><span>(chunks)</span><span>}</span><span> 个块。</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(</span><span>"--- 前5个块内容示例 ---"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>for</span><span> i, chunk </span><span>in</span><span> </span><span>enumerate</span><span>(chunks[:</span><span>5</span><span>]):</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(</span><span>"="</span><span><span> </span><span>*</span><span> </span></span><span>60</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># chunk 是一个 Document 对象，需要访问它的 .page_content 属性来获取文本</span></div></div><div><div><div>7</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>'块 </span><span>{</span><span><span>i</span><span>+</span></span><span>1}</span><span> (长度: </span><span>{</span><span>len</span><span>(chunk.page_content)</span><span>}</span><span>): "</span><span>{</span><span>chunk.page_content</span><span>}</span><span>"'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>文本被切分为 14 个块。</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>--- 前5个块内容示例 ---</span></div></div><div><div><div>4</div></div><div><span>============================================================</span></div></div><div><div><div>5</div></div><div><span>块 1 (长度: 72): "# 蜂医</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>游戏《三角洲行动》中的支援型干员</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>蜂医是2024年琳琅天上发行的《三角洲行动》中的支援型干员之一，在早期版本是唯一一个支援型干员。"</span></div></div><div><div><div>10</div></div><div><span>============================================================</span></div></div><div><div><div>11</div></div><div><span>块 2 (长度: 201): "蜂医在游戏中能够使用战术装备“激素枪”：远程治疗队友或'自我治疗'，还可以使用兵种道具“烟幕无人机”：释放长烟幕，和“蜂巢科技烟雾弹”：产生一团白色烟雾（使用激素枪对烟雾射击换变成绿色烟雾，可起到治疗作用），干员特长为“高效救援”：救援倒地队友时速度更快，在全面战场模式中约1.4秒就能救起队友，且被救起的队友能恢复更多生命值。在烽火地带中，还能够移除队友血量上限减少的负面效果。 \[1-2]****"</span></div></div><div><div><div>12</div></div><div><span>============================================================</span></div></div><div><div><div>13</div></div><div><span>块 3 (长度: 189): "* 中文名</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>罗伊•斯米</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>* 外文名</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>Roy smee \[2]**</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>* 别    名</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>罗伊、蜂医</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>* 性    别</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>蜂医在干员档案中标明他有一个妻子和女儿。</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>## 角色定位"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h1>2. 递归字符分块<a href="#2-递归字符分块"><span>#</span></a></h1><p>在前面的章节中（见 CH1_01_langchain_example），我们已经尝试了使用 <code>RecursiveCharacterTextSplitter</code> 的默认配置来处理文档分块。现在让我们深入了解 <code>RecursiveCharacterTextSplitter</code> 的实现。这种分块器通过分隔符层级递归处理，相对与固定大小分块，改善了超长文本的处理效果。</p><p><strong>算法流程</strong>：</p><p>（1）<strong>寻找有效分隔符</strong>: 从分隔符列表中从前到后遍历，找到第一个在当前文本中<strong>存在</strong>的分隔符。如果都不存在，使用最后一个分隔符（通常是空字符串 <code>""</code>）。</p><p>（2）<strong>切分与分类处理</strong>: 使用选定的分隔符切分文本，然后遍历所有片段：</p><ul>
<li><strong>如果片段不超过块大小</strong>: 暂存到 <code>_good_splits</code> 中，准备合并</li>
<li><strong>如果片段超过块大小</strong>:
<ul>
<li>首先，将暂存的合格片段通过 <code>_merge_splits</code> 合并成块</li>
<li>然后，检查是否还有剩余分隔符：
<ul>
<li><strong>有剩余分隔符</strong>: 递归调用 <code>_split_text</code> 继续分割</li>
<li><strong>无剩余分隔符</strong>: 直接保留为超长块</li>
</ul>
</li>
</ul>
</li>
</ul><p>（3）<strong>最终处理</strong>: 将剩余的暂存片段合并成最后的块</p><p><strong>实现细节</strong>：</p><ul>
<li><strong>批处理机制</strong>: 先收集所有合格片段（<code>_good_splits</code>），遇到超长片段时才触发合并操作。</li>
<li><strong>递归终止条件</strong>: 关键在于 <code>if not new_separators</code> 判断。当分隔符用尽时（<code>new_separators</code> 为空），停止递归，直接保留超长片段。确保算法不会无限递归。</li>
</ul><p><strong>与固定大小分块的关键差异</strong>：</p><ol>
<li>本质区别
<ul>
<li>固定大小分块遇到超长段落时只能发出警告，然后原样保留这个超长块。换句话说，它放弃了对这个超长段的进一步切分。</li>
<li>遇到超大片段时，会换用更细粒度的分隔符继续切。比如：段落切不动 → 用句号切 → 用逗号切 → 用空格切 → 逐字符切。它会把问题一直”递归下去”，直到块大小满足要求或分隔符用尽。</li>
</ul>
</li>
<li>分隔符策略
<ul>
<li>固定大小分块：只用一个分隔符（默认 <code>\n\n</code>，即段落标记）；</li>
<li>递归分块：使用 <strong>一组</strong> 按优先级排列的分隔符。</li>
</ul>
</li>
<li>语义完整性
<ul>
<li>固定大小分块：尽量保持段落完整，块的边界落在段落之间，语义相对较好；</li>
<li>递归分块：更灵活，块大小更均匀，但如果不得不切到句号甚至逗号级别，可能会在句子中间断开，对中文等无明显词边界的语言需要额外配置分隔符（如 <code>。、，</code>）。</li>
</ul>
</li>
</ol><div><div><div><div><span></span></div><div><span></span></div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span><p>是</p></span></div><div><span></span></div><div><span><p>否</p></span></div><div><span><p>原始文本</p></span></div><div><span><p>用最高优先级分隔符切分</p></span></div><div><span><p>片段 ≤ chunk_size?</p></span></div><div><span><p>✅ 合格，暂存待合并</p></span></div><div><span><p>还有其他分隔符?</p></span></div><div><span><p>🔄 递归：换下一级分隔符继续切</p></span></div><div><span><p>⚠️ 放弃，保留为超长块</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span><p>是</p></span></div><div><span></span></div><div><span><p>否</p></span></div><div><span><p>原始文本</p></span></div><div><span><p>用最高优先级分隔符切分</p></span></div><div><span><p>片段 ≤ chunk_size?</p></span></div><div><span><p>✅ 合格，暂存待合并</p></span></div><div><span><p>还有其他分隔符?</p></span></div><div><span><p>🔄 递归：换下一级分隔符继续切</p></span></div><div><span><p>⚠️ 放弃，保留为超长块</p></span></div>
</div></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> langchain.text_splitter </span><span>import</span><span> RecursiveCharacterTextSplitter</span></div></div><div><div><div>2</div></div><div><span>from</span><span> langchain_community.document_loaders </span><span>import</span><span> TextLoader</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>loader </span><span>=</span><span> </span><span>TextLoader</span><span>(</span></span><span>"../../data/C2/txt/蜂医.txt"</span><span>, </span><span>encoding</span><span>=</span><span>"utf-8"</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>docs </span><span>=</span><span> loader.</span><span>load</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>text_splitter </span><span>=</span><span> </span><span>RecursiveCharacterTextSplitter</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span># 针对中英文混合文本，定义一个更全面的分隔符列表</span></div></div><div><div><div>3</div></div><div><span>    </span><span>separators</span><span><span>=</span><span>[</span></span><span>"</span><span>\n\n</span><span>"</span><span>, </span><span>"</span><span>\n</span><span>"</span><span>, </span><span>"。"</span><span>, </span><span>"，"</span><span>, </span><span>" "</span><span>, </span><span>""</span><span>], </span><span># 按顺序尝试分割</span></div></div><div><div><div>4</div></div><div><span>    </span><span>chunk_size</span><span>=</span><span>200</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>chunk_overlap</span><span>=</span><span>10</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>chunks </span><span>=</span><span> text_splitter.</span><span>split_documents</span><span>(docs)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果不预先定义分隔符列表的话，<code>RecursiveCharacterTextSplitter</code> 使用的默认分隔符列表为 <code>["\n\n", "\n", " ", ""]</code>（4 级优先级）。实际上，如果所有段落都 <span><span>≤\le</span><span><span><span></span><span>≤</span></span></span></span> <code>chunk_size</code> 的话，递归分块与固定大小分块的行为 <strong>完全一致</strong>，因为我们可以从上面的默认分隔符列表看到，递归分块也是先用 <code>\n\n</code> 切出段落。而如果每个段落都不超长的话，后续的分隔符根本没有机会上场。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>文本段落都 ≤ chunk_size</p></span></div><div><span><p>两者行为完全相同 ✅</p></span></div><div><span><p>存在超长段落</p></span></div><div><span><p>CharacterTextSplitter: ⚠️ 警告 + 原样保留</p></span></div><div><span><p>RecursiveCharacterTextSplitter: 🔄 用 <br /><br /> 再切 → 用空格切 → 逐字符切</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>文本段落都 ≤ chunk_size</p></span></div><div><span><p>两者行为完全相同 ✅</p></span></div><div><span><p>存在超长段落</p></span></div><div><span><p>CharacterTextSplitter: ⚠️ 警告 + 原样保留</p></span></div><div><span><p>RecursiveCharacterTextSplitter: 🔄 用 <br /><br /> 再切 → 用空格切 → 逐字符切</p></span></div>
</div></div></div><p>因此从这个角度看，递归分块相当于固定分块 + 兜底递归机制，多了一道保险。</p><hr /><p>在定义 <code>separators</code> 分隔符列表时，LangChain 实际上有丰富的多语言支持，对于无词边界语言（中文、日文、泰文等），我们可以添加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>separators</span><span>=</span><span>[</span></div></div><div><div><div>2</div></div><div><span>      </span><span>"</span><span>\n\n</span><span>"</span><span>, </span><span>"</span><span>\n</span><span>"</span><span>, </span><span>" "</span><span>,</span></div></div><div><div><div>3</div></div><div><span>      </span><span>"."</span><span>, </span><span>","</span><span>, </span><span>"</span><span>\u200b</span><span>"</span><span>,      </span><span># 零宽空格(泰文、日文)</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"</span><span>\uff0c</span><span>"</span><span>, </span><span>"</span><span>\u3001</span><span>"</span><span>,      </span><span># 全角逗号、表意逗号</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"</span><span>\uff0e</span><span>"</span><span>, </span><span>"</span><span>\u3002</span><span>"</span><span>,      </span><span># 全角句号、表意句号</span></div></div><div><div><div>6</div></div><div><span>      </span><span>""</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p>而对于编程语言，<code>RecursiveCharacterTextSplitter</code> 能够针对特定的编程语言（如Python, Java等）使用预设的、更符合代码结构的分隔符。它们通常包含语言的顶级语法结构（如类、函数定义）和次级结构（如控制流语句），以实现更符合代码逻辑的分割：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 针对代码文档的优化分隔符</span></div></div><div><div><div>2</div></div><div><span><span>splitter </span><span>=</span><span> RecursiveCharacterTextSplitter.</span><span>from_language</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>language</span><span><span>=</span><span>Language.</span><span>PYTHON</span><span>,  </span></span><span># 支持Python、Java、C++等</span></div></div><div><div><div>4</div></div><div><span>    </span><span>chunk_size</span><span>=</span><span>500</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>chunk_overlap</span><span>=</span><span>50</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>f</span><span>"文本被切分为 </span><span>{</span><span>len</span><span>(chunks)</span><span>}</span><span> 个块。</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"--- 前5个块内容示例 ---"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>for</span><span> i, chunk </span><span>in</span><span> </span><span>enumerate</span><span>(chunks[:</span><span>5</span><span>]):</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>"="</span><span><span> </span><span>*</span><span> </span></span><span>60</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>'块 </span><span>{</span><span><span>i</span><span>+</span></span><span>1}</span><span> (长度: </span><span>{</span><span>len</span><span>(chunk.page_content)</span><span>}</span><span>): "</span><span>{</span><span>chunk.page_content</span><span>}</span><span>"'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h1>3. 语义分块<a href="#3-语义分块"><span>#</span></a></h1><p>语义分块（Semantic Chunking）是一种更智能的方法，这种方法不依赖于固定的字符数或预设的分隔符，而是尝试根据文本的语义内涵来切分。其核心是：<strong>在语义主题发生显著变化的地方进行切分</strong>。这使得每个分块都具有高度的内部语义一致性。LangChain 提供了 <code>langchain_experimental.text_splitter.SemanticChunker</code> 来实现这一功能。</p><section><h2>3.1 实现原理<a href="#31-实现原理"><span>#</span></a></h2><p><code>SemanticChunker</code> 的工作流程可以概括为以下几个步骤：</p><p>（1）<strong>句子分割 (Sentence Splitting)</strong>：首先，使用标准的句子分割规则（例如，基于句号、问号、感叹号）将输入文本拆分成一个句子列表。</p><p>（2）<strong>上下文感知嵌入 (Context-Aware Embedding)</strong>：这是 <code>SemanticChunker</code> 的一个关键设计。该分块器不是对每个句子独立进行嵌入，而是通过 <code>buffer_size</code> 参数（默认为1）来捕捉上下文信息。对于列表中的每一个句子，这种方法会将其与前后各 <code>buffer_size</code> 个句子组合起来，然后对这个临时的、更长的组合文本进行嵌入。这样，每个句子最终得到的嵌入向量就融入了其上下文的语义。</p><p>（3）<strong>计算语义距离 (Distance Calculation)</strong>：计算每对<strong>相邻</strong>句子的嵌入向量之间的余弦距离。这个距离值量化了两个句子之间的语义差异——距离越大，表示语义关联越弱，跳跃越明显。</p><p>（4）<strong>识别断点 (Breakpoint Identification)</strong>：<code>SemanticChunker</code> 会分析所有计算出的距离值，并根据一个统计方法（默认为 <code>percentile</code>）来确定一个动态阈值。例如，它可能会将所有距离中第 95 百分位的值作为切分阈值。所有距离大于此阈值的点，都被识别为语义上的“断点”。</p><p>（5）<strong>合并成块 (Merging into Chunks)</strong>：最后，根据识别出的所有断点位置，将原始的句子序列进行切分，并将每个切分后的部分内的所有句子合并起来，形成一个最终的、语义连贯的文本块。</p><p><strong>断点识别方法 (<code>breakpoint_threshold_type</code>)</strong></p><p>如何定义“显著的语义跳跃”是语义分块的关键。<code>SemanticChunker</code> 提供了几种基于统计的方法来识别断点：</p><ul>
<li>
<p><code>percentile</code> (百分位法 - <strong>默认方法</strong>):</p>
<ul>
<li><strong>逻辑</strong>: 计算所有相邻句子的语义差异值，并将这些差异值进行排序。当一个差异值超过某个百分位阈值时，就认为该差异值是一个断点。</li>
<li><strong>参数</strong>: <code>breakpoint_threshold_amount</code> (默认为 <code>95</code>)，表示使用第 95 个百分位作为阈值。这意味着，只有最显著的 5% 的语义差异点会被选为切分点。</li>
</ul>
</li>
<li>
<p><code>standard_deviation</code> (标准差法):</p>
<ul>
<li><strong>逻辑</strong>: 计算所有差异值的平均值和标准差。当一个差异值超过“平均值 + N * 标准差”时，被视为异常高的跳跃，即断点。</li>
<li><strong>参数</strong>: <code>breakpoint_threshold_amount</code> (默认为 <code>3</code>)，表示使用 3 倍标准差作为阈值。</li>
</ul>
</li>
<li>
<p><code>interquartile</code> (四分位距法):</p>
<ul>
<li><strong>逻辑</strong>: 使用统计学中的四分位距（IQR）来识别异常值。当一个差异值超过 <code>Q3 + N * IQR</code> 时，被视为断点。</li>
<li><strong>参数</strong>: <code>breakpoint_threshold_amount</code> (默认为 <code>1.5</code>)，表示使用 1.5 倍的IQR。</li>
</ul>
</li>
<li>
<p><code>gradient</code> (梯度法):</p>
<ul>
<li><strong>逻辑</strong>: 这是一种更复杂的方法。它首先计算差异值的变化率（梯度），然后对梯度应用百分位法。对于那些句子间语义联系紧密、差异值普遍较低的文本（如法律、医疗文档）特别有效，因为这种方法能更好地捕捉到语义变化的“拐点”。</li>
<li><strong>参数</strong>: <code>breakpoint_threshold_amount</code> (默认为 <code>95</code>)。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> langchain_experimental.text_splitter </span><span>import</span><span> SemanticChunker</span></div></div><div><div><div>2</div></div><div><span>from</span><span> langchain_community.embeddings </span><span>import</span><span> HuggingFaceEmbeddings</span></div></div><div><div><div>3</div></div><div><span>from</span><span> langchain_community.document_loaders </span><span>import</span><span> TextLoader</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>embeddings </span><span>=</span><span> </span><span>HuggingFaceEmbeddings</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>model_name</span><span>=</span><span>"BAAI/bge-small-zh-v1.5"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>model_kwargs</span><span><span>=</span><span>{</span></span><span>'device'</span><span>: </span><span>'cpu'</span><span>},</span></div></div><div><div><div>4</div></div><div><span>    </span><span>encode_kwargs</span><span><span>=</span><span>{</span></span><span>'normalize_embeddings'</span><span>: </span><span>True</span><span>}</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们实例化一个 embedding 模型，其中的 <code>normalize_embeddings=True</code> 指的是：告诉 HuggingFace 的 embedding 模型，<strong>将输出的向量做 L2 归一化</strong>，使得每个向量的模长变为 1.</p><p>简单回顾一下 L2 归一化，对于一个原始向量 <span><span>v=[v1,v2,...,vn]\mathbf{v}=[v_1,v_2,...,v_n]</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，归一化后就变成：</p><span><span><span>vnorm=v∣∣v∣∣2,∣∣vnorm∣∣2=1\mathbf{v}_{\text{norm}}=\frac{\mathbf{v}}{||\mathbf{v}||_2},||\mathbf{v}_{\text{norm}}||_2=1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span><span>norm</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∣∣</span><span>v</span><span>∣</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>∣∣</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span><span>norm</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p>这样做的原因是，归一化后 <strong>向量点积=余弦相似度</strong>：</p><span><span><span>anorm⋅bnorm=cos⁡(θ)\mathbf{a}_{\text{norm}}\cdot \mathbf{b}_{\text{norm}}=\cos(\theta)</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>norm</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span><span>norm</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>cos</span><span>(</span><span>θ</span><span>)</span></span></span></span></span><p>对于 RAG 检索，这有几个好处：</p><ul>
<li>检索更快：余弦相似度需要除法（模长相乘），而归一化后直接用点积算，省了除法运算；</li>
<li>兼容 Milvus / FAISS 等向量库：很多向量库的默认索引（如 Milvus 的 IP 度量）假设输入已是归一化向量；</li>
<li>分数有界：归一化后的点积范围是 <span><span>[−1,1][-1,1]</span><span><span><span></span><span>[</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>]</span></span></span></span> ，方便设定相似度阈值。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 初始化 SemanticChunker</span></div></div><div><div><div>2</div></div><div><span><span>text_splitter </span><span>=</span><span> </span><span>SemanticChunker</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>embeddings,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>breakpoint_threshold_type</span><span>=</span><span>"percentile"</span><span> </span><span># 也可以是 "standard_deviation", "interquartile", "gradient"</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>loader </span><span>=</span><span> </span><span>TextLoader</span><span>(</span></span><span>"../../data/C2/txt/蜂医.txt"</span><span>, </span><span>encoding</span><span>=</span><span>"utf-8"</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>documents </span><span>=</span><span> loader.</span><span>load</span><span>()</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>docs </span><span>=</span><span> text_splitter.</span><span>split_documents</span><span>(documents)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(</span><span>f</span><span>"文本被切分为 </span><span>{</span><span>len</span><span>(docs)</span><span>}</span><span> 个块。</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"--- 前2个块内容示例 ---"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>for</span><span> i, chunk </span><span>in</span><span> </span><span>enumerate</span><span>(docs[:</span><span>2</span><span>]):</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>"="</span><span><span> </span><span>*</span><span> </span></span><span>60</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>'块 </span><span>{</span><span><span>i</span><span>+</span></span><span>1}</span><span> (长度: </span><span>{</span><span>len</span><span>(chunk.page_content)</span><span>}</span><span>):</span><span>\n</span><span>"</span><span>{</span><span>chunk.page_content</span><span>}</span><span>"'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># percentile 的切分结果</span></div></div><div><div><div>2</div></div><div><span>文本被切分为 2 个块。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>--- 前2个块内容示例 ---</span></div></div><div><div><div>5</div></div><div><span>============================================================</span></div></div><div><div><div>6</div></div><div><span>块 1 (长度: 566):</span></div></div><div><div><div>7</div></div><div><span>"# 蜂医</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>游戏《三角洲行动》中的支援型干员</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>蜂医是2024年琳琅天上发行的《三角洲行动》中的支援型干员之一，在早期版本是唯一一个支援型干员。</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>...</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>1. 1[角色设定](#1)</span></div></div><div><div><div>16</div></div><div><span>2."</span></div></div><div><div><div>17</div></div><div><span>============================================================</span></div></div><div><div><div>18</div></div><div><span>块 2 (长度: 1776):</span></div></div><div><div><div>19</div></div><div><span>"2[角色定位](#2)</span></div></div><div><div><div>20</div></div><div><span>3. 3[技能](#3)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>1) ▪[战术装备 - 激素枪](#3-1)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>...</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>* 4</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>[](#sup-4)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>[《三角洲行动》全新黑夜之子赛季4月17日正式上线](/reference/65245483/533aYdO6cr3_z3kATPSPmKqhNHmQNIivurSFV-ZzzqIPmGapB4rjFJkjrtQw87hkBETK4sgwNIZMxr6nWxIb8a5OJbh9XO1mwCo)．腾讯游戏 \[引用日期2025-06-13]**</span></div></div><div><div><div>31</div></div><div><span>"</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span># standard_deviation 的切分结果</span></div></div><div><div><div>34</div></div><div><span>文本被切分为 1 个块。</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>--- 前2个块内容示例 ---</span></div></div><div><div><div>37</div></div><div><span>============================================================</span></div></div><div><div><div>38</div></div><div><span>块 1 (长度: 2343):</span></div></div><div><div><div>39</div></div><div><span>"# 蜂医</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>游戏《三角洲行动》中的支援型干员</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>蜂医是2024年琳琅天上发行的《三角洲行动》中的支援型干员之一，在早期版本是唯一一个支援型干员。</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>...</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>* 4</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>[](#sup-4)</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>[《三角洲行动》全新黑夜之子赛季4月17日正式上线](/reference/65245483/533aYdO6cr3_z3kATPSPmKqhNHmQNIivurSFV-ZzzqIPmGapB4rjFJkjrtQw87hkBETK4sgwNIZMxr6nWxIb8a5OJbh9XO1mwCo)．腾讯游戏 \[引用日期2025-06-13]**</span></div></div><div><div><div>52</div></div><div><span>"</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span># interquartile 切分结果与上同</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span># gradient 切分结果</span></div></div><div><div><div>57</div></div><div><span>文本被切分为 2 个块。</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>--- 前2个块内容示例 ---</span></div></div><div><div><div>60</div></div><div><span>============================================================</span></div></div><div><div><div>61</div></div><div><span>块 1 (长度: 551):</span></div></div><div><div><div>62</div></div><div><span>"# 蜂医</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>游戏《三角洲行动》中的支援型干员</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>蜂医是2024年琳琅天上发行的《三角洲行动》中的支援型干员之一，在早期版本是唯一一个支援型干员。</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>...</span></div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span>- 体    重</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span><span>  </span></span><span>75 kg \[3]**</span></div></div><div><div><div>73</div></div><div>
</div></div><div><div><div>74</div></div><div><span>## 目录</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>1."</span></div></div><div><div><div>77</div></div><div><span>============================================================</span></div></div><div><div><div>78</div></div><div><span>块 2 (长度: 1791):</span></div></div><div><div><div>79</div></div><div><span>"1[角色设定](#1)</span></div></div><div><div><div>80</div></div><div><span>2. 2[角色定位](#2)</span></div></div><div><div><div>81</div></div><div>
</div></div><div><div><div>82</div></div><div><span>...</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>* 4</span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>[](#sup-4)</span></div></div><div><div><div>87</div></div><div>
</div></div><div><div><div>88</div></div><div><span><span>  </span></span><span>[《三角洲行动》全新黑夜之子赛季4月17日正式上线](/reference/65245483/533aYdO6cr3_z3kATPSPmKqhNHmQNIivurSFV-ZzzqIPmGapB4rjFJkjrtQw87hkBETK4sgwNIZMxr6nWxIb8a5OJbh9XO1mwCo)．腾讯游戏 \[引用日期2025-06-13]**</span></div></div><div><div><div>89</div></div><div><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>需要注意的是，LangChain 中的 <code>SemanticChunker</code> 没有任何的 <code>chunk_size</code> 上限，也就是说，分块完全由语义距离决定，如果相邻句子间的语义距离都较小（低于阈值），<strong>所有句子会合并成一个块，不管它有多长</strong>。</p><p>在这里我们可以看到，如果选用 <code>standard_deviation</code> 或 <code>interquartile</code> 策略，整个文档就只会被切成 1 块。</p><p>所以，如果遇到长文档语义连贯的情况，可以考虑在语义分块的外面再加一层长度兜底，防止单块切得太长。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-02/</id>
      <title type="text">CH2_01_unstructured_example</title>
      <published>2026-06-16T00:00:00.000Z</published>
      <updated>2026-06-16T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-02/"/>
      <summary type="text">如何直接使用 Unstructured 库的示例</summary>
      <content type="html"><![CDATA[<section><h1>1. 创建简单示例<a href="#1-创建简单示例"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> unstructured.partition.auto </span><span>import</span><span> partition</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># PDF文件路径</span></div></div><div><div><div>2</div></div><div><span><span>pdf_path </span><span>=</span><span> </span></span><span>"../../data/C2/pdf/rag.pdf"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 使用Unstructured加载并解析PDF文档</span></div></div><div><div><div>2</div></div><div><span><span>elements </span><span>=</span><span> </span><span>partition</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>filename</span><span><span>=</span><span>pdf_path,</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>content_type</span><span>=</span><span>"application/pdf"</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 打印解析结果</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"解析完成: </span><span>{</span><span>len</span><span>(elements)</span><span>}</span><span> 个元素, </span><span>{</span><span>sum</span><span>(</span><span>len</span><span>(</span><span>str</span><span>(e)) </span><span>for</span><span> e </span><span>in</span><span> elements)</span><span>}</span><span> 字符"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>解析完成: 279 个元素, 7500 字符</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>partition 函数参数解析：</strong></p><ul>
<li><code>filename</code>: 文档文件路径，支持本地文件路径；</li>
<li><code>content_type</code>: 可选参数，指定MIME类型（如”application/pdf”），可绕过自动文件类型检测；</li>
<li><code>file</code>: 可选参数，文件对象，与 filename 二选一使用；</li>
<li><code>url</code>: 可选参数，远程文档 URL，支持直接处理网络文档；</li>
<li><code>include_page_breaks</code>: 布尔值，是否在输出中包含页面分隔符；</li>
<li><code>strategy</code>: 处理策略，可选 “auto”、“fast”、“hi_res” 等；
<ul>
<li><code>auto</code> ：优先尝试快速提取文本，如果文本提取效果差就自动降级为 OCR.</li>
<li><code>fast</code> ：直接用 PDF 解析器提取 <strong>内置文本层</strong>，速度快，但不解析图片中的文字；</li>
<li><code>hi_res</code> ：高精度模式，先做 OCR，然后再结合布局分析，精度最高；</li>
<li><code>ocr_only</code> ：完全忽略 PDF 内置文本层，只靠 OCR 识别所有内容。</li>
</ul>
</li>
<li><code>encoding</code>: 文本编码格式，默认自动检测。</li>
</ul><p><code>partition</code> 函数使用自动文件类型检测，内部会根据文件类型路由到对应的专用函数（如 PDF 文件会调用<code>partition_pdf</code>）。如果需要更专业的 PDF 处理，可以直接使用 <code>from unstructured.partition.pdf import partition_pdf</code>，它提供更多 PDF 特有的参数选项，如 OCR 语言设置、图像提取、表格结构推理等高级功能，同时性能更优。</p><blockquote><p>在实际应用中，针对 pdf 的处理，目前更多选用的是 PaddleOCR、MinerU 等模型或工具。</p></blockquote><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 统计元素类型</span></div></div><div><div><div>2</div></div><div><span>from</span><span> collections </span><span>import</span><span> Counter</span></div></div><div><div><div>3</div></div><div><span><span>types </span><span>=</span><span> </span><span>Counter</span><span>(e.category </span></span><span>for</span><span> e </span><span>in</span><span> elements)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>f</span><span>"元素类型: </span><span>{</span><span>dict</span><span>(types)</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>元素类型: {'Header': 22, 'Title': 195, 'UncategorizedText': 41, 'NarrativeText': 3, 'Footer': 15, 'ListItem': 3}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 显示所有元素</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"</span><span>\n</span><span>所有元素:"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>for</span><span> i, element </span><span>in</span><span> </span><span>enumerate</span><span>(elements, </span><span>1</span><span>):</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"Element </span><span>{</span><span>i</span><span>}</span><span> (</span><span>{</span><span>element.category</span><span>}</span><span>):"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(element)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>print</span><span>(</span><span>"="</span><span><span> </span><span>*</span><span> </span></span><span>60</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>所有元素:</span></div></div><div><div><div>2</div></div><div><span>Element 1 (Header):</span></div></div><div><div><div>3</div></div><div><span>网页</span></div></div><div><div><div>4</div></div><div><span>============================================================</span></div></div><div><div><div>5</div></div><div><span>Element 2 (Header):</span></div></div><div><div><div>6</div></div><div><span>新闻</span></div></div><div><div><div>7</div></div><div><span>============================================================</span></div></div><div><div><div>8</div></div><div><span>Element 3 (Header):</span></div></div><div><div><div>9</div></div><div><span>贴吧</span></div></div><div><div><div>10</div></div><div><span>============================================================</span></div></div><div><div><div>11</div></div><div><span>Element 4 (Header):</span></div></div><div><div><div>12</div></div><div><span>知道</span></div></div><div><div><div>13</div></div><div><span>============================================================</span></div></div><div><div><div>14</div></div><div><span>Element 5 (Header):</span></div></div><div><div><div>15</div></div><div><span>网盘</span></div></div><div><div><div>16</div></div><div><span>============================================================</span></div></div><div><div><div>17</div></div><div><span>Element 6 (Header):</span></div></div><div><div><div>18</div></div><div><span>图片</span></div></div><div><div><div>19</div></div><div><span>============================================================</span></div></div><div><div><div>20</div></div><div><span>Element 7 (Header):</span></div></div><div><div><div>21</div></div><div><span>视频</span></div></div><div><div><div>22</div></div><div><span>============================================================</span></div></div><div><div><div>23</div></div><div><span>Element 8 (Header):</span></div></div><div><div><div>24</div></div><div><span>地图</span></div></div><div><div><div>25</div></div><div><span>...</span></div></div><div><div><div>26</div></div><div><span>============================================================</span></div></div><div><div><div>27</div></div><div><span>Element 279 (Footer):</span></div></div><div><div><div>28</div></div><div><span>HTML to PDF</span></div></div><div><div><div>29</div></div><div><span>============================================================</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h1>2. 用 <code>partition_pdf</code> 替换当前 <code>partition</code> 函数<a href="#2-用-partition_pdf-替换当前-partition-函数"><span>#</span></a></h1><p>我们使用 <code>partition_pdf</code> 替换当前 <code>partition</code> 函数并分别尝试用 <code>hi_res</code> 和 <code>ocr_only</code> 进行解析，观察输出结果有何变化。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> unstructured.partition.pdf </span><span>import</span><span> partition_pdf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 运行前确保安装 Poppler 和 Tesseract OCR</span></div></div><div><div><div>2</div></div><div><span><span>elements </span><span>=</span><span> </span><span>partition_pdf</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>filename</span><span><span>=</span><span>pdf_path,</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>content_type</span><span>=</span><span>"application/pdf"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>strategy</span><span><span> </span><span>=</span><span> </span></span><span>"ocr_only"</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 打印解析结果</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>f</span><span>"解析完成: </span><span>{</span><span>len</span><span>(elements)</span><span>}</span><span> 个元素, </span><span>{</span><span>sum</span><span>(</span><span>len</span><span>(</span><span>str</span><span>(e)) </span><span>for</span><span> e </span><span>in</span><span> elements)</span><span>}</span><span> 字符"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hi_res 结果</span></div></div><div><div><div>2</div></div><div><span>解析完成: 221 个元素, 8265 字符</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># ocr_only 结果</span></div></div><div><div><div>5</div></div><div><span>解析完成: 137 个元素, 8282 字符</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关于元素数量，一般而言是越少越好：</p>

<table><thead><tr><th>策略</th><th>元素数</th><th>原因</th></tr></thead><tbody><tr><td><code>fast</code></td><td><strong>279（最多）</strong></td><td>直接读 PDF 文本层，PDF 里每个独立文本对象（哪怕只是”网页”两个字）都算一个元素</td></tr><tr><td><code>hi_res</code></td><td><strong>221</strong></td><td>布局模型会把相邻小文本<strong>合并</strong>到同一区域，所以元素更少，但结构更合理</td></tr><tr><td><code>ocr_only</code></td><td><strong>137（最少）</strong></td><td>每页是一大块文字 + 标题，碎片化程度最低</td></tr></tbody></table><p><code>fast</code> 的 279 个元素中，有 195 个 <code>Title</code>，很多只是导航栏的碎片（“网页”、“新闻”、“贴吧”……）。<code>hi_res</code> 的布局模型将这些碎片合并成了更大的块。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 统计元素类型</span></div></div><div><div><div>2</div></div><div><span>from</span><span> collections </span><span>import</span><span> Counter</span></div></div><div><div><div>3</div></div><div><span><span>types </span><span>=</span><span> </span><span>Counter</span><span>(e.category </span></span><span>for</span><span> e </span><span>in</span><span> elements)</span></div></div><div><div><div>4</div></div><div><span>print</span><span>(</span><span>f</span><span>"元素类型: </span><span>{</span><span>dict</span><span>(types)</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hi_res 结果</span></div></div><div><div><div>2</div></div><div><span>元素类型: {'Image': 22, 'UncategorizedText': 85, 'Header': 4, 'NarrativeText': 68, 'Table': 4, 'FigureCaption': 4, 'Title': 30, 'ListItem': 4}</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># ocr_only 结果</span></div></div><div><div><div>5</div></div><div><span>元素类型: {'UncategorizedText': 54, 'Title': 55, 'NarrativeText': 27, 'ListItem': 1}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，在输出的元素类型上，两种策略有很大不同：</p>

<table><thead><tr><th>类型</th><th><code>hi_res</code></th><th><code>ocr_only</code></th><th>原因</th></tr></thead><tbody><tr><td><code>Image</code></td><td>✅ 22</td><td>❌ 无</td><td><code>hi_res</code> 有布局检测模型，能识别图片区域；<code>ocr_only</code> 只做 OCR，不分析布局</td></tr><tr><td><code>Table</code></td><td>✅ 4</td><td>❌ 无</td><td>同上，表格区域由布局模型识别</td></tr><tr><td><code>FigureCaption</code></td><td>✅ 4</td><td>❌ 无</td><td>图片/表格的标题，也只有布局模型能识别</td></tr><tr><td><code>Header</code></td><td>✅ 4</td><td>❌ 无</td><td>页面顶部导航栏等</td></tr><tr><td><code>NarrativeText</code></td><td>68</td><td>27</td><td><code>hi_res</code> 中布局模型区分了正文和杂项，<code>ocr_only</code> 很多被归入 <code>UncategorizedText</code></td></tr><tr><td><code>UncategorizedText</code></td><td>85</td><td>54</td><td><code>ocr_only</code> 没有布局信息，大量文本无法归类</td></tr><tr><td><code>Title</code></td><td>30</td><td>55</td><td><code>ocr_only</code> 中 OCR 识别的短文本片段容易被判为 Title</td></tr></tbody></table><p>这主要是由于两种策略不同的流程导致的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hi_res 流程：</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>PDF → Poppler 转图片 → 布局检测模型(YOLOX)识区域 → Tesseract OCR 各区域</span></div></div><div><div><div>3</div></div><div><span><span>                                                            </span></span><span>↓</span></div></div><div><div><div>4</div></div><div><span><span>                                                   </span></span><span>Image / Table / Header</span></div></div><div><div><div>5</div></div><div><span><span>                                                   </span></span><span>NarrativeText / Title ...</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>ocr_only 流程：</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>PDF → Poppler 转图片 → Tesseract OCR 整页 → 全是文字，无布局标签</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看到，<code>hi_res</code> 多了一个 <strong>布局检测步骤</strong>，所以可以区分出 image, table, figure caption 等结构化元素。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 显示所有元素</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(</span><span>"</span><span>\n</span><span>所有元素:"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>for</span><span> i, element </span><span>in</span><span> </span><span>enumerate</span><span>(elements, </span><span>1</span><span>):</span></div></div><div><div><div>4</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"Element </span><span>{</span><span>i</span><span>}</span><span> (</span><span>{</span><span>element.category</span><span>}</span><span>):"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(element)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>print</span><span>(</span><span>"="</span><span><span> </span><span>*</span><span> </span></span><span>60</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hi_res 结果</span></div></div><div><div><div>2</div></div><div><span>所有元素:</span></div></div><div><div><div>3</div></div><div><span>Element 1 (Image):</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>============================================================</span></div></div><div><div><div>6</div></div><div><span>Element 2 (UncategorizedText):</span></div></div><div><div><div>7</div></div><div><span>Bh fe</span></div></div><div><div><div>8</div></div><div><span>============================================================</span></div></div><div><div><div>9</div></div><div><span>Element 3 (UncategorizedText):</span></div></div><div><div><div>10</div></div><div><span>Se «6 Be BR 8H Me OE 6B CR OBS</span></div></div><div><div><div>11</div></div><div><span>============================================================</span></div></div><div><div><div>12</div></div><div><span>Element 4 (Header):</span></div></div><div><div><div>13</div></div><div><span>百度首页 登录 注册</span></div></div><div><div><div>14</div></div><div><span>============================================================</span></div></div><div><div><div>15</div></div><div><span>Element 5 (UncategorizedText):</span></div></div><div><div><div>16</div></div><div><span>0 0 Bai @ Bil | eme22n</span></div></div><div><div><div>17</div></div><div><span>============================================================</span></div></div><div><div><div>18</div></div><div><span>Element 6 (UncategorizedText):</span></div></div><div><div><div>19</div></div><div><span>x</span></div></div><div><div><div>20</div></div><div><span>============================================================</span></div></div><div><div><div>21</div></div><div><span>Element 7 (UncategorizedText):</span></div></div><div><div><div>22</div></div><div><span>进⼊词条</span></div></div><div><div><div>23</div></div><div><span>============================================================</span></div></div><div><div><div>24</div></div><div><span>Element 8 (UncategorizedText):</span></div></div><div><div><div>25</div></div><div><span>全站搜索</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span>============================================================</span></div></div><div><div><div>28</div></div><div><span>Element 221 (UncategorizedText):</span></div></div><div><div><div>29</div></div><div><span>HTML to PDF</span></div></div><div><div><div>30</div></div><div><span>============================================================</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span># ocr_only 结果</span></div></div><div><div><div>33</div></div><div><span>所有元素:</span></div></div><div><div><div>34</div></div><div><span>Element 1 (UncategorizedText):</span></div></div><div><div><div>35</div></div><div><span>Bh fe Se «6 Be BR 8H Me OE 6B CR OBS ESR SR ith</span></div></div><div><div><div>36</div></div><div><span>============================================================</span></div></div><div><div><div>37</div></div><div><span>Element 2 (Title):</span></div></div><div><div><div>38</div></div><div><span>0 0 Bai @ Bil | eme22n x SME | mm</span></div></div><div><div><div>39</div></div><div><span>============================================================</span></div></div><div><div><div>40</div></div><div><span>Element 3 (NarrativeText):</span></div></div><div><div><div>41</div></div><div><span>MBA RAATEABEBANEAAR, VRE A BIDHAE RR O, CEL IERAA: BREN RRMA, ONCE CIRS, LYSE! iFIB&gt;&gt; Bn pias HEAR ALIAS a MAB BAAR RAE &gt; 1 REA i oa E AK ) HR Otte Lew</span></div></div><div><div><div>42</div></div><div><span>============================================================</span></div></div><div><div><div>43</div></div><div><span>Element 4 (UncategorizedText):</span></div></div><div><div><div>44</div></div><div><span>ABB AWARAZ—</span></div></div><div><div><div>45</div></div><div><span>============================================================</span></div></div><div><div><div>46</div></div><div><span>Element 5 (Title):</span></div></div><div><div><div>47</div></div><div><span>MoingSh</span></div></div><div><div><div>48</div></div><div><span>============================================================</span></div></div><div><div><div>49</div></div><div><span>Element 6 (NarrativeText):</span></div></div><div><div><div>50</div></div><div><span>fomieas tat OACe &gt;)</span></div></div><div><div><div>51</div></div><div><span>============================================================</span></div></div><div><div><div>52</div></div><div><span>Element 7 (NarrativeText):</span></div></div><div><div><div>53</div></div><div><span>it =</span></div></div><div><div><div>54</div></div><div><span>============================================================</span></div></div><div><div><div>55</div></div><div><span>Element 8 (Title):</span></div></div><div><div><div>56</div></div><div><span>[= = HEhH - SAR RAGFlow RAF RAMA</span></div></div><div><div><div>57</div></div><div><span>...</span></div></div><div><div><div>58</div></div><div><span>============================================================</span></div></div><div><div><div>59</div></div><div><span>Element 137 (UncategorizedText):</span></div></div><div><div><div>60</div></div><div><span>Explore our developer-friendly HTML to PDF API Printed using PDFCrowd H to PDF</span></div></div><div><div><div>61</div></div><div><span>============================================================</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>从具体识别到的元素来看，我们可以发现 <code>ocr_only</code> 对中文的效果很差，比如 <code>Bh</code>, <code>fe</code> 等完全就是乱码，甚至英文部分的 <code>MBA</code>, <code>RAATEABEBANEAAR</code> 也严重变形。</p><p>这主要是 tesseract 默认使用英文语言包导致的，所以对于我们的示例（中文 PDF），tesseract 几乎无法正确识别。</p><p>对此，我们可以添加 <code>language</code> 参数（记得下载 tesseract 的中文语言包）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>elements </span><span>=</span><span> </span><span>partition_pdf</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>filename</span><span><span>=</span><span>pdf_path,</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>strategy</span><span>=</span><span>"ocr_only"</span><span>, </span><span># ocr_only, hi_res</span></div></div><div><div><div>4</div></div><div><span>    </span><span>languages</span><span><span>=</span><span>[</span></span><span>"chi_sim"</span><span>]   </span><span># 简体中文</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>增加语言包后的输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hi_res 的结果</span></div></div><div><div><div>2</div></div><div><span>解析完成: 241 个元素, 8224 字符</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># ocr_only 的结果</span></div></div><div><div><div>5</div></div><div><span>解析完成: 143 个元素, 12247 字符</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>首先我们来关注 <code>hi_res</code> 的变化。中文包让 OCR 能在布局模型划分的区域内识别出更多独立文本块，所以元素数略微增加。之前因乱码被合并或丢弃的文本现在被正确提取。而字符数反而有所减少，这是因为 OCR 正确识别中文后，同一个面积的区域内，中文字符比英文乱码字符更少（一个汉字 vs 一串”Xe «6 Be BR…”），所以总数反而略降。</p><p>可以看到，在加装了中文语言包后，<code>ocr_only</code> 解析出的字符几乎暴涨了 50%，从 8282 -&gt; 12247. 这是因为英文 OCR 时，中文段落被识别为一堆乱码字符，很多”字”根本没被识别出来。中文包生效后，每个汉字都被正确识别，所以总字符数大幅增加。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hi_res 的结果</span></div></div><div><div><div>2</div></div><div><span>元素类型: {'Image': 21, 'UncategorizedText': 106, 'Header': 4, 'NarrativeText': 68, 'Table': 4, 'FigureCaption': 4, 'Title': 30, 'ListItem': 4}</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># ocr_only 的结果</span></div></div><div><div><div>5</div></div><div><span>元素类型: {'Title': 52, 'NarrativeText': 27, 'UncategorizedText': 54, 'ListItem': 10}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>hi_res</code> 的 <code>UncategorizedText</code> 有所增加，这些新增的 UncategorizedText 主要来自导航栏碎片——之前因为乱码被丢弃或归为其他类型，现在被正确识别并保留。</p><p>在加装中文语言包后，<code>ocr_only</code> 也成功识别出了更多的结构化信息，例如列表项（<code>ListItem</code> 1 -&gt; 10），之前因为乱码而根本无法判定。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># hi_res 的结果</span></div></div><div><div><div>2</div></div><div><span>所有元素:</span></div></div><div><div><div>3</div></div><div><span>Element 1 (Image):</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>============================================================</span></div></div><div><div><div>6</div></div><div><span>Element 2 (UncategorizedText):</span></div></div><div><div><div>7</div></div><div><span>国 页</span></div></div><div><div><div>8</div></div><div><span>============================================================</span></div></div><div><div><div>9</div></div><div><span>Element 3 (UncategorizedText):</span></div></div><div><div><div>10</div></div><div><span>新 闻 _</span></div></div><div><div><div>11</div></div><div><span>============================================================</span></div></div><div><div><div>12</div></div><div><span>Element 4 (UncategorizedText):</span></div></div><div><div><div>13</div></div><div><span>吧</span></div></div><div><div><div>14</div></div><div><span>============================================================</span></div></div><div><div><div>15</div></div><div><span>Element 5 (UncategorizedText):</span></div></div><div><div><div>16</div></div><div><span>贴吧</span></div></div><div><div><div>17</div></div><div><span>============================================================</span></div></div><div><div><div>18</div></div><div><span>Element 6 (UncategorizedText):</span></div></div><div><div><div>19</div></div><div><span>知 道</span></div></div><div><div><div>20</div></div><div><span>============================================================</span></div></div><div><div><div>21</div></div><div><span>Element 7 (UncategorizedText):</span></div></div><div><div><div>22</div></div><div><span>网盘</span></div></div><div><div><div>23</div></div><div><span>============================================================</span></div></div><div><div><div>24</div></div><div><span>Element 8 (UncategorizedText):</span></div></div><div><div><div>25</div></div><div><span>图片</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span>============================================================</span></div></div><div><div><div>28</div></div><div><span>Element 241 (UncategorizedText):</span></div></div><div><div><div>29</div></div><div><span>HTML to PDF</span></div></div><div><div><div>30</div></div><div><span>============================================================</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span># ocr_only 的结果</span></div></div><div><div><div>33</div></div><div><span>所有元素:</span></div></div><div><div><div>34</div></div><div><span>Element 1 (Title):</span></div></div><div><div><div>35</div></div><div><span>国 页 新 闻 _ 贴 吧 知 道</span></div></div><div><div><div>36</div></div><div><span>============================================================</span></div></div><div><div><div>37</div></div><div><span>Element 2 (Title):</span></div></div><div><div><div>38</div></div><div><span>百 科 百 度 首 页 “ 登 录 “ 注 册</span></div></div><div><div><div>39</div></div><div><span>============================================================</span></div></div><div><div><div>40</div></div><div><span>Element 3 (Title):</span></div></div><div><div><div>41</div></div><div><span>.... Baidb 百 租 | 8458 办 x</span></div></div><div><div><div>42</div></div><div><span>============================================================</span></div></div><div><div><div>43</div></div><div><span>Element 4 (Title):</span></div></div><div><div><div>44</div></div><div><span>全 站 搜 索 ‖ 帝 助</span></div></div><div><div><div>45</div></div><div><span>============================================================</span></div></div><div><div><div>46</div></div><div><span>Element 5 (NarrativeText):</span></div></div><div><div><div>47</div></div><div><span>近 期 有 不 法 分 子 冒 兆 百 度 百 科 客 方 人 员 , 以 删 除 词 条 为 申 姚 防 并 敌 许 关 企 业 , 在 此 严 正 声 明 : 百 度 百 科 是 免 资 缉 辑 平 台 , 绝 不 存 在 收 费 代 编 服 务 , 请 勿 上 当 受 腾 ! 详 情 &gt;&gt;</span></div></div><div><div><div>48</div></div><div><span>============================================================</span></div></div><div><div><div>49</div></div><div><span>Element 6 (UncategorizedText):</span></div></div><div><div><div>50</div></div><div><span>首 页 秒 懂 百 科 特 色 百 科 知 识 专 题 加 入 百 科 百 科 团 队 权 威 合 作</span></div></div><div><div><div>51</div></div><div><span>============================================================</span></div></div><div><div><div>52</div></div><div><span>Element 7 (Title):</span></div></div><div><div><div>53</div></div><div><span>《 索 增 强 生 成 。m</span></div></div><div><div><div>54</div></div><div><span>============================================================</span></div></div><div><div><div>55</div></div><div><span>Element 8 (Title):</span></div></div><div><div><div>56</div></div><div><span>大 模 型 前 治 技 术 之 一</span></div></div><div><div><div>57</div></div><div><span>...</span></div></div><div><div><div>58</div></div><div><span>============================================================</span></div></div><div><div><div>59</div></div><div><span>Element 143 (NarrativeText):</span></div></div><div><div><div>60</div></div><div><span>Printed using PDFCrowd H to PDF</span></div></div><div><div><div>61</div></div><div><span>============================================================</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>hi_res</code> 在实际解析出的元素上变化不大，因为 <code>hi_res</code> 的元素类型靠的是布局模型（YOLOX），语言包只影响 OCR 文字质量，不影响区域检测，所以类型分布变化不大。</p><p>可以很明显地看出，加装了中文语言包后，<code>ocr_only</code> 实际解析出的内容从天书变成了可读的中文。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/all-in-rag-01/</id>
      <title type="text">CH1_02_llamaindex_example</title>
      <published>2026-06-15T00:00:00.000Z</published>
      <updated>2026-06-15T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/all-in-rag-01/"/>
      <summary type="text">RAG 的 一个简单 Llamaindex 示例</summary>
      <content type="html"><![CDATA[<p>相比 LangChain 更像一个通用的 LLM 编排工具，LlamaIndex 更专注于 “数据索引与检索” 这个 RAG 核心场景，开箱即用。</p>
<section><h1>1. 库导入<a href="#1-库导入"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> os</span></div></div><div><div><div>2</div></div><div><span># os.environ['HF_ENDPOINT']='https://hf-mirror.com'</span></div></div><div><div><div>3</div></div><div><span>from</span><span> dotenv </span><span>import</span><span> load_dotenv</span></div></div><div><div><div>4</div></div><div><span>from</span><span> llama_index.core </span><span>import</span><span> VectorStoreIndex, SimpleDirectoryReader, Settings</span></div></div><div><div><div>5</div></div><div><span>from</span><span> llama_index.embeddings.huggingface </span><span>import</span><span> HuggingFaceEmbedding</span></div></div><div><div><div>6</div></div><div><span>from</span><span> llama_index.llms.deepseek </span><span>import</span><span> DeepSeek</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>load_dotenv</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>1.1 核心库解析<a href="#11-核心库解析"><span>#</span></a></h2>

<table><thead><tr><th>导入语句</th><th>作用</th></tr></thead><tbody><tr><td><code>from dotenv import load_dotenv</code></td><td>读取 <code>.env</code> 文件中的环境变量（如 API Key）</td></tr><tr><td><code>from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings</code></td><td>三大核心组件，见下方详解</td></tr><tr><td><code>from llama_index.embeddings.huggingface import HuggingFaceEmbedding</code></td><td>使用 HuggingFace 上的<strong>嵌入模型</strong>，将文本转为向量</td></tr><tr><td><code>from llama_index.llms.deepseek import DeepSeek</code></td><td>LlamaIndex 对 DeepSeek 大语言模型的封装，用于回答问题</td></tr></tbody></table><hr /></section><section><h2>1.2 🧩 核心函数详解<a href="#12--核心函数详解"><span>#</span></a></h2><p><code>SimpleDirectoryReader</code> — 数据加载器</p><ul>
<li>从指定文件（或文件夹）中读取文档内容</li>
<li>支持 Markdown、PDF、Word、TXT 等多种格式</li>
<li>返回一个 <code>Document</code> 对象列表（每个文件/页面是一个 Document）</li>
</ul><p><code>Settings</code> — 全局配置</p><ul>
<li>一个<strong>全局单例配置对象</strong>，设置后整个项目自动生效</li>
<li><code>Settings.llm</code>：指定用哪个大模型来<strong>回答问题</strong></li>
<li><code>Settings.embed_model</code>：指定用哪个模型将文本转为<strong>向量（embedding）</strong></li>
</ul><p><code>HuggingFaceEmbedding</code> — 嵌入模型</p><ul>
<li>调用 HuggingFace 上的文本嵌入模型（这里是 BAAI 的 <code>bge-small-zh-v1.5</code>）</li>
<li>作用：把一段文字转换成一个<strong>固定长度的向量（数字数组）</strong>，用于相似度搜索</li>
<li><code>bge-small-zh-v1.5</code> 是一个<strong>轻量级的中文嵌入模型</strong>，速度快且效果不错</li>
</ul><p><code>DeepSeek</code> — LLM 封装</p><ul>
<li>将 DeepSeek 的 API 封装成 LlamaIndex 能调用的 LLM 接口</li>
<li><code>model</code> 参数指定模型名，<code>api_key</code> 从环境变量读取</li>
</ul><p><code>VectorStoreIndex</code> — 向量索引（核心！）</p><ul>
<li><strong>将 Document 列表构建成一个可检索的向量索引</strong>，内部自动完成：
<ol>
<li><strong>文本分块</strong>（把长文档切成小段）</li>
<li><strong>向量化</strong>（用 <code>embed_model</code> 给每个块生成向量）</li>
<li><strong>存入内存向量存储</strong>（默认的内存索引）</li>
</ol>
</li>
</ul></section></section>
<section><h1>2. 模型传入<a href="#2-模型传入"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>Settings.llm </span><span>=</span><span> </span><span>DeepSeek</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>model</span><span>=</span><span>"deepseek-v4-flash"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>api_key</span><span><span>=</span><span>os.</span><span>getenv</span><span>(</span></span><span>"DEEPSEEK_API_KEY"</span><span>),</span></div></div><div><div><div>4</div></div><div><span>)</span></div></div><div><div><div>5</div></div><div><span><span>Settings.embed_model </span><span>=</span><span> </span><span>HuggingFaceEmbedding</span><span>(</span></span><span>"BAAI/bge-small-zh-v1.5"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>指定回答问题的大模型与向量嵌入模型。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>docs </span><span>=</span><span> </span><span>SimpleDirectoryReader</span><span>(</span></span><span>input_files</span><span><span>=</span><span>[</span></span><span>"../../data/C1/markdown/easy-rl-chapter1.md"</span><span><span>]).</span><span>load_data</span><span>()</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>index </span><span>=</span><span> VectorStoreIndex.</span><span>from_documents</span><span>(docs)</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>query_engine </span><span>=</span><span> index.</span><span>as_query_engine</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们用 <code>.as_query_engine()</code> 查询引擎将 <code>index</code> 包装为一个问答引擎，调用的时候会自动执行检索 + 生成的全过程。</p><p>它的内部流程大致是：用户问题 → 向量检索 Top-K 相关段落 → 拼到 Prompt 中 → 发给 LLM 回答</p></section>
<section><h1>3. 输出<a href="#3-输出"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span><span>(query_engine.</span><span>get_prompts</span><span>())</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>print</span><span><span>(query_engine.</span><span>query</span><span>(</span></span><span>"文中举了哪些例子?"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'response_synthesizer:text_qa_template': SelectorPromptTemplate(metadata={'prompt_type': &lt;PromptType.QUESTION_ANSWER: 'text_qa'&gt;}, template_vars=['context_str', 'query_str'], kwargs={}, output_parser=None, template_var_mappings={}, function_mappings={}, default_template=PromptTemplate(metadata={'prompt_type': &lt;PromptType.QUESTION_ANSWER: 'text_qa'&gt;}, template_vars=['context_str', 'query_str'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, template='Context information is below.\n---------------------\n{context_str}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query_str}\nAnswer: '), conditionals=[(&lt;function is_chat_model at 0x000001E63D7F40E0&gt;, ChatPromptTemplate(metadata={'prompt_type': &lt;PromptType.CUSTOM: 'custom'&gt;}, template_vars=['context_str', 'query_str'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, message_templates=[ChatMessage(role=&lt;MessageRole.SYSTEM: 'system'&gt;, additional_kwargs={}, blocks=[TextBlock(block_type='text', text="You are an expert Q&amp;A system that is trusted around the world.\nAlways answer the query using the provided context information, and not prior knowledge.\nSome rules to follow:\n1. Never directly reference the given context in your answer.\n2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.")]), ChatMessage(role=&lt;MessageRole.USER: 'user'&gt;, additional_kwargs={}, blocks=[TextBlock(block_type='text', text='Context information is below.\n---------------------\n{context_str}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query_str}\nAnswer: ')])]))]), 'response_synthesizer:refine_template': SelectorPromptTemplate(metadata={'prompt_type': &lt;PromptType.REFINE: 'refine'&gt;}, template_vars=['query_str', 'existing_answer', 'context_msg'], kwargs={}, output_parser=None, template_var_mappings={}, function_mappings={}, default_template=PromptTemplate(metadata={'prompt_type': &lt;PromptType.REFINE: 'refine'&gt;}, template_vars=['query_str', 'existing_answer', 'context_msg'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, template="The original query is as follows: {query_str}\nWe have provided an existing answer: {existing_answer}\nWe have the opportunity to refine the existing answer (only if needed) with some more context below.\n------------\n{context_msg}\n------------\nGiven the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.\nRefined Answer: "), conditionals=[(&lt;function is_chat_model at 0x000001E63D7F40E0&gt;, ChatPromptTemplate(metadata={'prompt_type': &lt;PromptType.CUSTOM: 'custom'&gt;}, template_vars=['context_msg', 'query_str', 'existing_answer'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, message_templates=[ChatMessage(role=&lt;MessageRole.USER: 'user'&gt;, additional_kwargs={}, blocks=[TextBlock(block_type='text', text="You are an expert Q&amp;A system that strictly operates in two modes when refining existing answers:\n1. **Rewrite** an original answer using the new context.\n2. **Repeat** the original answer if the new context isn't useful.\nNever reference the original answer or context directly in your answer.\nWhen in doubt, just repeat the original answer.\nNew Context: {context_msg}\nQuery: {query_str}\nOriginal Answer: {existing_answer}\nNew Answer: ")])]))])}</span></div></div><div><div><div>2</div></div><div><span>文中举了选择餐馆、做广告、挖油和玩游戏的例子。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上面的一整段输出中其实就包含两个 prompt 模板：</p><ol>
<li><code>response_synthesizer:text_qa_template</code> ：<strong>主要问答模板</strong>，你可以在开头找到它；</li>
<li><code>response_synthesizer:refine_template</code> ：<strong>精炼模板</strong>，在这一大段输出中间。</li>
</ol><p>我们接着来一个个看里面的参数：</p><ul>
<li><code>text_qa_template</code> ：<strong>问答主模板</strong>，这是 LLM 首次回答时用的 prompt，在第一个长长的 <code>context_str</code> 前后。
<ul>
<li><code>context_str</code> ：检索到的 <strong>相关文本片段</strong>，拼到这里；</li>
<li><code>query_str</code> ：用户的 <strong>原始问题</strong>；</li>
<li><code>Answer</code> ：留给 LLM 输出答案的位置。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Context information is below.</span></div></div><div><div><div>2</div></div><div><span>---------------------</span></div></div><div><div><div>3</div></div><div><span>{context_str}</span></div></div><div><div><div>4</div></div><div><span>---------------------</span></div></div><div><div><div>5</div></div><div><span>Given the context information and not prior knowledge, answer the query.</span></div></div><div><div><div>6</div></div><div><span>Query: {query_str}</span></div></div><div><div><div>7</div></div><div><span>Answer:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的 “Given the …” 提示词是 query_engine 内置的，因为我们在创建查询引擎 <code>query_engine = index.as_query_engine()</code> 的时候没有传入任何参数，所以它内部自动使用了 LlamaIndex 的默认 prompt 模板。</p><p>如果想要创建自己的模板或 prompt，应该类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> llama_index.core </span><span>import</span><span> PromptTemplate</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>custom_qa_prompt </span><span>=</span><span> </span><span>PromptTemplate</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>"请根据以下内容回答问题。</span><span>\n</span><span>"</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"内容：</span><span>{context_str}</span><span>\n</span><span>"</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"问题：</span><span>{query_str}</span><span>\n</span><span>"</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"答案："</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>query_engine </span><span>=</span><span> index.</span><span>as_query_engine</span><span>(</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>text_qa_template</span><span><span>=</span><span>custom_qa_prompt</span></span></div></div><div><div><div>12</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>is_chat_model</code> ：<strong>Chat 模型条件分支</strong>。因为 Deepseek 是 Chat 模型，所以实际用的是下面这个版本。
<ul>
<li><code>role=SYSTEM</code> ：系统角色指令，约束回答风格；</li>
<li><code>role=USER</code> ：包含上下文和用户问题的消息。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>System: You are an expert Q&amp;A system...</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>1. Never directly reference the given context in your answer.</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>2. Avoid statements like 'Based on the context, ...'</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>User: Context information is below.</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>---------------------</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>{context_str}</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>---------------------</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>Given the context information and not prior knowledge, answer the query.</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>Query: {query_str}</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>Answer:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>refine_template</code> ：<strong>精炼模板</strong>。当文档被切分成多个块，依次检索时，除了第一块用上面的主模板，后续块用这个模板精炼答案。
<ul>
<li><code>query_str</code> ：原始问题；</li>
<li><code>existing_answer</code> ：上一轮已有的答案（需要被精炼）；</li>
<li><code>context_msg</code> ：新的补充上下文（当前检索到的另一块文本）；</li>
<li><code>Refined Answer:</code> ：LLM 输出优化后答案的位置。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Chat 分支版本，带 Sytem/User 角色</span></div></div><div><div><div>2</div></div><div><span>System: ...1. **Rewrite** an original answer using the new context.</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>2. **Repeat** the original answer if the new context isn't useful.</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>Never reference the original answer or context directly...</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>User: New Context: {context_msg}</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>Query: {query_str}</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>Original Answer: {existing_answer}</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>New Answer:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>综上，完整的流程如下图：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>用户问："文中举了哪些例子？"</span></div></div><div><div><div>2</div></div><div><span><span>           </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>           </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>向量检索 Top-K 个相关块</span></div></div><div><div><div>5</div></div><div><span><span>           </span></span><span>│</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>┌─────┴─────┐</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>│  第 1 块  │ ──→ text_qa_template ──→  LLM  →  初步答案 A1</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>└─────┬─────┘</span></div></div><div><div><div>9</div></div><div><span><span>           </span></span><span>│</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>┌─────┴─────┐</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>│  第 2 块  │ ──→ refine_template (A1 + 新块)  →  精炼答案 A2</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>└─────┬─────┘</span></div></div><div><div><div>13</div></div><div><span><span>           </span></span><span>│</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>┌─────┴─────┐</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>│  第 3 块  │ ──→ refine_template (A2 + 新块)  →  最终答案 A3</span></div></div><div><div><div>16</div></div><div><span><span>     </span></span><span>└───────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>其他元数据补充：</p><ul>
<li><code>prompt_type</code> ：模板类型标识，这里是 <code>QUESTION_ANSWER</code>（问答）和 <code>REFINE</code>（精炼）；</li>
<li><code>template_vars</code> ：模板中用到的变量名列表</li>
<li><code>conditionals</code> ：条件分支 —— 根据模型类型（是否是 Chat 模型）选择不同的模板格式</li>
<li><code>default_template</code> ：文本补全模型的默认模板（<code>text-davinci-003</code> 风格）</li>
</ul><section><h2>3.1 什么是 Chat 模型？<a href="#31-什么是-chat-模型"><span>#</span></a></h2><p>在 LlamaIndex 的语境里，<code>is_chat_model</code> 的判断对应的是 LLM 的两种 API 调用方式：</p><section><h3>1️⃣ Chat 模型（如 DeepSeek、GPT-4o、GPT-3.5-turbo、Claude 等）<a href="#1️⃣-chat-模型如-deepseekgpt-4ogpt-35-turboclaude-等"><span>#</span></a></h3><ul>
<li><strong>API 格式</strong>：接受 <code>System</code> + <code>User</code> + <code>Assistant</code> 角色的消息列表（Messages）</li>
<li><strong>特点</strong>：支持多轮对话、角色分离、指令遵循能力强</li>
<li>LlamaIndex 对 Chat 模型使用 <code>ChatPromptTemplate</code>，如我们看到的带 <code>role=SYSTEM</code> 和 <code>role=USER</code> 的模板</li>
</ul></section><section><h3>2️⃣ 文本补全模型（Text Completion / 原始 LLM）<a href="#2️⃣-文本补全模型text-completion--原始-llm"><span>#</span></a></h3><ul>
<li><strong>API 格式</strong>：接受一段<strong>连续文本字符串</strong>，模型接着往后生成</li>
<li><strong>代表</strong>：OpenAI 早期的 <code>text-davinci-003</code>、<code>text-curie-001</code>、<code>gpt-3.5-turbo-instruct</code> 等</li>
<li><strong>特点</strong>：没有 System/User 角色之分，全靠 Prompt 本身的措辞来引导</li>
</ul><p>LlamaIndex 对这类模型使用 <code>default_template</code>，就是一个纯文本模板：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Context information is below.</span></div></div><div><div><div>2</div></div><div><span>---------------------</span></div></div><div><div><div>3</div></div><div><span>{context_str}</span></div></div><div><div><div>4</div></div><div><span>---------------------</span></div></div><div><div><div>5</div></div><div><span>Given the context information and not prior knowledge, answer the query.</span></div></div><div><div><div>6</div></div><div><span>Query: {query_str}</span></div></div><div><div><div>7</div></div><div><span>Answer:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>3.1.3 区别对比<a href="#313-区别对比"><span>#</span></a></h3>

<table><thead><tr><th>维度</th><th>Chat 模型</th><th>文本补全模型</th></tr></thead><tbody><tr><td><strong>输入格式</strong></td><td>消息列表（<code>[System, User, Assistant]</code>）</td><td>纯文本字符串</td></tr><tr><td><strong>角色区分</strong></td><td>✅ 有 System / User 角色</td><td>❌ 无角色，全部混合在文本中</td></tr><tr><td><strong>指令位置</strong></td><td>System 消息放系统指令</td><td>指令写在 Prompt 开头/结尾</td></tr><tr><td><strong>代表模型</strong></td><td>DeepSeek, GPT-4o, Claude, Gemini</td><td><code>text-davinci-003</code>, <code>gpt-3.5-turbo-instruct</code></td></tr><tr><td><strong>LlamaIndex 模板</strong></td><td><code>ChatPromptTemplate</code></td><td><code>PromptTemplate</code>（即 <code>default_template</code>）</td></tr></tbody></table><hr /></section><section><h3>3.1.4 实际效果差异<a href="#314-实际效果差异"><span>#</span></a></h3><p>对于同一个意图，模型实际收到的内容分别是：</p><p><strong>文本补全模型（纯文本）→</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Context information is below.</span></div></div><div><div><div>2</div></div><div><span>---------------------</span></div></div><div><div><div>3</div></div><div><span>文中提到选择餐馆...</span></div></div><div><div><div>4</div></div><div><span>---------------------</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div><div><div><div>6</div></div><div><span>Answer:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>Chat 模型（消息列表）→</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>System: You are an expert Q&amp;A system...</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>1. Never directly reference the given context...</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>User: Context information is below.</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>---------------------</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>文中提到选择餐馆...</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>---------------------</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>...</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>Answer:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Chat 模型多了 <strong>System message</strong> 这个专门放指令的位置，指令不会被用户问题”淹没”，所以 LlamaIndex 在 Chat 模型的模板里加了更多细粒度的约束（如”不要提到 ‘根据上下文‘“等），让回答更自然。</p><p>现在几乎<strong>所有主流 API 都是 Chat 模型</strong>了，OpenAI 也在 2024 年彻底弃用了 <code>text-davinci-003</code> 等补全模型。所以我们大概率只会用到 Chat 模型分支，<code>default_template</code> 更多是历史兼容用途。</p></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-07/</id>
      <title type="text">07_model_outputs</title>
      <published>2026-06-12T00:00:00.000Z</published>
      <updated>2026-06-19T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-07/"/>
      <summary type="text">动手写 BERT 系列笔记第七节</summary>
      <content type="html"><![CDATA[<p>本节我们详细地探讨 BERT 模型的 outputs.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div><div><div><div>3</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertModel, BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>4</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_name, </span></span><span>output_hidden_states</span><span>=</span><span>True</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>1. input<a href="#1-input"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 表示拼接</span></div></div><div><div><div>2</div></div><div><span><span>text </span><span>=</span><span> </span></span><span>'After stealing money from the bank vault, the bank robber was seen '</span><span>\</span></div></div><div><div><div>3</div></div><div><span>    </span><span>'fishing on the Mississippi river bank.'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>token_input </span><span>=</span><span> </span><span>tokenizer</span><span>(text, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(token_input)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[  101,  2044, 11065,  2769,  2013,  1996,  2924, 11632,  1010,  1996,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>2924, 27307,  2001,  2464,  5645,  2006,  1996,  5900,  2314,  2924,</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>1012,   102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>token_input[</span><span>'input_ids'</span><span>], token_input[</span><span>'input_ids'</span><span>].shape</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(tensor([[  101,  2044, 11065,  2769,  2013,  1996,  2924, 11632,  1010,  1996,</span></div></div><div><div><div>2</div></div><div><span><span>           </span></span><span>2924, 27307,  2001,  2464,  5645,  2006,  1996,  5900,  2314,  2924,</span></div></div><div><div><div>3</div></div><div><span><span>           </span></span><span>1012,   102]]),</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>torch.Size([1, 22]))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可见 <code>batch_size</code> = 1（我们只有一个句子），序列长度 <code>seq_length</code> = 22（没有 <code>truncate</code> 和 <code>padding</code>）.</p></section>
<section><h1>2. model forward<a href="#2-model-forward"><span>#</span></a></h1><ul>
<li>forward
<ul>
<li>embedding -&gt; encoder -&gt; pooler</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model.</span><span>eval</span><span>()</span></span></div></div><div><div><div>2</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>outputs </span><span>=</span><span> </span><span>model</span><span>(**token_input)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BaseModelOutputWithPoolingAndCrossAttentions(last_hidden_state=tensor([[[-0.4964, -0.1831, -0.5231,  ..., -0.1902,  0.3738,  0.3964],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.1323, -0.2762, -0.3495,  ..., -0.4567,  0.3786, -0.1096],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.3626, -0.4002,  0.0676,  ..., -0.3207, -0.2709, -0.3004],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.2961, -0.2856, -0.0382,  ..., -0.6056, -0.5163,  0.2005],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.4878, -0.0909, -0.2358,  ..., -0.0017, -0.5945, -0.2431],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.2517, -0.3519, -0.4688,  ...,  0.2500,  0.0336, -0.2627]]]), pooler_output=tensor([[-0.6031, -0.3342, -0.7174,  0.3347,  0.5145, -0.1722,  0.4502,  0.2768,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>-0.3769, -0.9998, -0.3657,  0.7535,  0.9817, -0.0192,  0.7959, -0.3459,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>-0.1338, -0.3026,  0.1097,  0.5836,  0.5736,  0.9999,  0.1798,  0.1845,</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>0.2250,  0.9109, -0.5653,  0.8616,  0.8994,  0.7423, -0.2525,  0.0394,</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>-0.9894, -0.1331, -0.7763, -0.9826,  0.2223, -0.6115,  0.1941,  0.0177,</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>-0.7634,  0.2312,  0.9999, -0.7000,  0.4623, -0.2202, -1.0000,  0.1908,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>-0.8150,  0.6483,  0.5878,  0.8198,  0.1014,  0.3185,  0.3963, -0.3216,</span></div></div><div><div><div>14</div></div><div><span><span>         </span></span><span>-0.1701,  0.0588, -0.1544, -0.4987, -0.5284,  0.1228, -0.4823, -0.7788,</span></div></div><div><div><div>15</div></div><div><span><span>          </span></span><span>0.6954,  0.0891, -0.0855, -0.1500,  0.0390, -0.0760,  0.6154,  0.2662,</span></div></div><div><div><div>16</div></div><div><span><span>         </span></span><span>-0.0129, -0.7253,  0.1352,  0.2921, -0.5613,  1.0000,  0.1536, -0.9681,</span></div></div><div><div><div>17</div></div><div><span><span>          </span></span><span>0.7166,  0.2600,  0.4519,  0.5470, -0.2798, -1.0000,  0.3419, -0.2645,</span></div></div><div><div><div>18</div></div><div><span><span>         </span></span><span>-0.9863,  0.1263,  0.5249, -0.2000,  0.5980,  0.4752, -0.2355, -0.4808,</span></div></div><div><div><div>19</div></div><div><span><span>         </span></span><span>-0.3786, -0.7284, -0.0909,  0.0124, -0.0689, -0.2531, -0.1324, -0.2361,</span></div></div><div><div><div>20</div></div><div><span><span>          </span></span><span>0.1732, -0.3216, -0.0188,  0.2302, -0.3221,  0.4996,  0.4346, -0.1935,</span></div></div><div><div><div>21</div></div><div><span><span>          </span></span><span>0.2968, -0.9292,  0.5326, -0.3695, -0.9876, -0.4770, -0.9902,  0.6349,</span></div></div><div><div><div>22</div></div><div><span><span>         </span></span><span>-0.1863, -0.2612,  0.9123, -0.1930,  0.3110,  0.0803, -0.7598, -1.0000,</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>0.0292, -0.0628, -0.1086, -0.2135, -0.9671, -0.9521,  0.3334,  0.8675,</span></div></div><div><div><div>24</div></div><div><span><span>          </span></span><span>0.2254,  0.9995, -0.2908,  0.9420,  0.0336, -0.4542,  0.4123, -0.4455,</span></div></div><div><div><div>25</div></div><div><span><span>          </span></span><span>0.4558, -0.4442, -0.0685,  0.3043,  0.0575,  0.1843, -0.6577, -0.3121,</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>         </span></span><span>[-0.3626, -0.4002,  0.0676,  ..., -0.3207, -0.2709, -0.3004],</span></div></div><div><div><div>28</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>29</div></div><div><span><span>         </span></span><span>[ 0.2961, -0.2856, -0.0382,  ..., -0.6056, -0.5163,  0.2005],</span></div></div><div><div><div>30</div></div><div><span><span>         </span></span><span>[ 0.4878, -0.0909, -0.2358,  ..., -0.0017, -0.5945, -0.2431],</span></div></div><div><div><div>31</div></div><div><span><span>         </span></span><span>[-0.2517, -0.3519, -0.4688,  ...,  0.2500,  0.0336, -0.2627]]])), past_key_values=None, attentions=None, cross_attentions=None)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里就是我们把 <code>token_input</code> 丢给 BERT 模型进行前向传播。</p><p>其中 <code>**token_input</code> 是把字典展开为关键字参数，相当于：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 不要执行这段代码</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># token_input 是一个 dict，包含：</span></div></div><div><div><div>4</div></div><div><span># {</span></div></div><div><div><div>5</div></div><div><span>#   'input_ids': tensor([[101, 2044, ..., 102]]),     # shape: (1, 22)</span></div></div><div><div><div>6</div></div><div><span>#   'token_type_ids': tensor([[0, 0, ..., 0]]),        # shape: (1, 22)</span></div></div><div><div><div>7</div></div><div><span>#   'attention_mask': tensor([[1, 1, ..., 1]])         # shape: (1, 22)</span></div></div><div><div><div>8</div></div><div><span># }</span></div></div><div><div><div>9</div></div><div><span>#</span></div></div><div><div><div>10</div></div><div><span># 所以 model(**token_input) 等价于：</span></div></div><div><div><div>11</div></div><div><span><span>outputs </span><span>=</span><span> </span><span>model</span><span>(</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>input_ids</span><span><span>=</span><span>tensor_input_ids,</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>token_type_ids</span><span><span>=</span><span>tensor_token_type_ids,</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>attention_mask</span><span><span>=</span><span>tensor_attention_mask</span></span></div></div><div><div><div>15</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h1>3. output<a href="#3-output"><span>#</span></a></h1><ul>
<li><code>len(outputs) == 3</code></li>
<li><code>outputs[0]</code>
<ul>
<li><code>last_hidden_state</code>, shape: <code>batch_size</code> * <code>seq_len</code> * <code>hidden_size</code>(1 * 22 * 768)</li>
</ul>
</li>
<li><code>outputs[1]</code>
<ul>
<li><code>pooler_output</code>, shape: <code>batch_size</code> * <code>hidden_size</code>(1 * 768)</li>
</ul>
</li>
<li><code>outputs[2]</code>(<code>model.config.output_hidden_states == True</code>)
<ul>
<li>type: tuple</li>
<li>one for the output of the embeddings(1), if the model has an embedding layer(12), + one for the output of each layer
<ul>
<li>(1 + 12) * (<code>batch_size</code> * <code>seq_len</code> * <code>hidden_size</code>) = 1 * 22 * 768</li>
</ul>
</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len</span><span>(outputs) </span><span># 3</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>type</span><span>(outputs[</span><span>2</span><span>]), </span><span>len</span><span>(outputs[</span><span>2</span><span>]) </span><span># (tuple, 13)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>0</span><span>]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>outputs[</span><span>2</span><span><span>][</span><span>-</span></span><span>1</span><span>]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>outputs[</span><span>0</span><span><span>] </span><span>==</span><span> outputs[</span></span><span>2</span><span><span>][</span><span>-</span></span><span>1</span><span>] </span><span># True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[-0.4964, -0.1831, -0.5231,  ..., -0.1902,  0.3738,  0.3964],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.1323, -0.2762, -0.3495,  ..., -0.4567,  0.3786, -0.1096],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.3626, -0.4002,  0.0676,  ..., -0.3207, -0.2709, -0.3004],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.2961, -0.2856, -0.0382,  ..., -0.6056, -0.5163,  0.2005],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.4878, -0.0909, -0.2358,  ..., -0.0017, -0.5945, -0.2431],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.2517, -0.3519, -0.4688,  ...,  0.2500,  0.0336, -0.2627]]])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>tensor([[[-0.4964, -0.1831, -0.5231,  ..., -0.1902,  0.3738,  0.3964],</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>[-0.1323, -0.2762, -0.3495,  ..., -0.4567,  0.3786, -0.1096],</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>[-0.3626, -0.4002,  0.0676,  ..., -0.3207, -0.2709, -0.3004],</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>[ 0.2961, -0.2856, -0.0382,  ..., -0.6056, -0.5163,  0.2005],</span></div></div><div><div><div>14</div></div><div><span><span>         </span></span><span>[ 0.4878, -0.0909, -0.2358,  ..., -0.0017, -0.5945, -0.2431],</span></div></div><div><div><div>15</div></div><div><span><span>         </span></span><span>[-0.2517, -0.3519, -0.4688,  ...,  0.2500,  0.0336, -0.2627]]])</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>tensor([[[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>18</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>19</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>20</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>21</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>22</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>23</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True]]])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可以看到二者的输出是相等的。因为 <code>outputs[0]</code> 就是最后一层隐藏层的状态，而 <code>outputs[2][-1]</code> 就是所有隐藏层状态的最后一个，二者当然相等。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>1</span><span>]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>model.</span><span>pooler</span><span>(outputs[</span></span><span>2</span><span><span>][</span><span>-</span></span><span>1</span><span>])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>outputs[</span><span>1</span><span><span>] </span><span>==</span><span> model.</span><span>pooler</span><span>(outputs[</span></span><span>2</span><span><span>][</span><span>-</span></span><span>1</span><span>]) </span><span># True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[-0.6031, -0.3342, -0.7174,  0.3347,  0.5145, -0.1722,  0.4502,  0.2768,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>-0.3769, -0.9998, -0.3657,  0.7535,  0.9817, -0.0192,  0.7959, -0.3459,</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>-0.1338, -0.3026,  0.1097,  0.5836,  0.5736,  0.9999,  0.1798,  0.1845,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>0.2250,  0.9109, -0.5653,  0.8616,  0.8994,  0.7423, -0.2525,  0.0394,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>-0.9894, -0.1331, -0.7763, -0.9826,  0.2223, -0.6115,  0.1941,  0.0177,</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>-0.7634,  0.2312,  0.9999, -0.7000,  0.4623, -0.2202, -1.0000,  0.1908,</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>-0.8150,  0.6483,  0.5878,  0.8198,  0.1014,  0.3185,  0.3963, -0.3216,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>-0.1701,  0.0588, -0.1544, -0.4987, -0.5284,  0.1228, -0.4823, -0.7788,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>0.6954,  0.0891, -0.0855, -0.1500,  0.0390, -0.0760,  0.6154,  0.2662,</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>-0.0129, -0.7253,  0.1352,  0.2921, -0.5613,  1.0000,  0.1536, -0.9681,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>0.7166,  0.2600,  0.4519,  0.5470, -0.2798, -1.0000,  0.3419, -0.2645,</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>-0.9863,  0.1263,  0.5249, -0.2000,  0.5980,  0.4752, -0.2355, -0.4808,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>-0.3786, -0.7284, -0.0909,  0.0124, -0.0689, -0.2531, -0.1324, -0.2361,</span></div></div><div><div><div>14</div></div><div><span><span>          </span></span><span>0.1732, -0.3216, -0.0188,  0.2302, -0.3221,  0.4996,  0.4346, -0.1935,</span></div></div><div><div><div>15</div></div><div><span><span>          </span></span><span>0.2968, -0.9292,  0.5326, -0.3695, -0.9876, -0.4770, -0.9902,  0.6349,</span></div></div><div><div><div>16</div></div><div><span><span>         </span></span><span>-0.1863, -0.2612,  0.9123, -0.1930,  0.3110,  0.0803, -0.7598, -1.0000,</span></div></div><div><div><div>17</div></div><div><span><span>          </span></span><span>0.0292, -0.0628, -0.1086, -0.2135, -0.9671, -0.9521,  0.3334,  0.8675,</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>0.2254,  0.9995, -0.2908,  0.9420,  0.0336, -0.4542,  0.4123, -0.4455,</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>0.4558, -0.4442, -0.0685,  0.3043,  0.0575,  0.1843, -0.6577, -0.3121,</span></div></div><div><div><div>20</div></div><div><span><span>         </span></span><span>-0.1069, -0.7729, -0.2328,  0.9032, -0.4681, -0.5580,  0.3359, -0.1644,</span></div></div><div><div><div>21</div></div><div><span><span>         </span></span><span>-0.1572,  0.6441,  0.2810,  0.2651,  0.1532,  0.4485, -0.5299,  0.2616,</span></div></div><div><div><div>22</div></div><div><span><span>         </span></span><span>-0.7412, -0.0347,  0.2560, -0.2659, -0.6092, -0.9868, -0.2346,  0.4682,</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>0.9771,  0.5646,  0.2360,  0.4567, -0.2170,  0.1420, -0.9554,  0.9832,</span></div></div><div><div><div>24</div></div><div><span><span>         </span></span><span>-0.0935,  0.2621, -0.7901,  0.5758, -0.7642, -0.5362,  0.6374, -0.3891,</span></div></div><div><div><div>25</div></div><div><span><span>         </span></span><span>-0.6368, -0.0045, -0.2658, -0.1721, -0.7466,  0.4832, -0.3128, -0.2640,</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>         </span></span><span>-0.0989, -0.3853, -0.3173, -0.5348,  0.5307, -0.6883, -0.3786, -0.4665,</span></div></div><div><div><div>28</div></div><div><span><span>          </span></span><span>0.7207,  0.3239,  0.9999, -0.5526, -0.4825, -0.3439, -0.3580,  0.1069,</span></div></div><div><div><div>29</div></div><div><span><span>         </span></span><span>-0.2620, -1.0000,  0.2673, -0.3037,  0.4706, -0.6132,  0.8511, -0.4910,</span></div></div><div><div><div>30</div></div><div><span><span>         </span></span><span>-0.7231, -0.1092,  0.5412,  0.5265, -0.4635, -0.2461,  0.4946, -0.3469,</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>0.9149,  0.5902, -0.1862,  0.7004,  0.5465, -0.2839, -0.5723,  0.6648]])</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>tensor([[-0.6031, -0.3342, -0.7174,  0.3347,  0.5145, -0.1722,  0.4502,  0.2768,</span></div></div><div><div><div>34</div></div><div><span><span>         </span></span><span>-0.3769, -0.9998, -0.3657,  0.7535,  0.9817, -0.0192,  0.7959, -0.3459,</span></div></div><div><div><div>35</div></div><div><span><span>         </span></span><span>-0.1338, -0.3026,  0.1097,  0.5836,  0.5736,  0.9999,  0.1798,  0.1845,</span></div></div><div><div><div>36</div></div><div><span><span>          </span></span><span>0.2250,  0.9109, -0.5653,  0.8616,  0.8994,  0.7423, -0.2525,  0.0394,</span></div></div><div><div><div>37</div></div><div><span><span>         </span></span><span>-0.9894, -0.1331, -0.7763, -0.9826,  0.2223, -0.6115,  0.1941,  0.0177,</span></div></div><div><div><div>38</div></div><div><span><span>         </span></span><span>-0.7634,  0.2312,  0.9999, -0.7000,  0.4623, -0.2202, -1.0000,  0.1908,</span></div></div><div><div><div>39</div></div><div><span><span>         </span></span><span>-0.8150,  0.6483,  0.5878,  0.8198,  0.1014,  0.3185,  0.3963, -0.3216,</span></div></div><div><div><div>40</div></div><div><span><span>         </span></span><span>-0.1701,  0.0588, -0.1544, -0.4987, -0.5284,  0.1228, -0.4823, -0.7788,</span></div></div><div><div><div>41</div></div><div><span><span>          </span></span><span>0.6954,  0.0891, -0.0855, -0.1500,  0.0390, -0.0760,  0.6154,  0.2662,</span></div></div><div><div><div>42</div></div><div><span><span>         </span></span><span>-0.0129, -0.7253,  0.1352,  0.2921, -0.5613,  1.0000,  0.1536, -0.9681,</span></div></div><div><div><div>43</div></div><div><span><span>          </span></span><span>0.7166,  0.2600,  0.4519,  0.5470, -0.2798, -1.0000,  0.3419, -0.2645,</span></div></div><div><div><div>44</div></div><div><span><span>         </span></span><span>-0.9863,  0.1263,  0.5249, -0.2000,  0.5980,  0.4752, -0.2355, -0.4808,</span></div></div><div><div><div>45</div></div><div><span><span>         </span></span><span>-0.3786, -0.7284, -0.0909,  0.0124, -0.0689, -0.2531, -0.1324, -0.2361,</span></div></div><div><div><div>46</div></div><div><span><span>          </span></span><span>0.1732, -0.3216, -0.0188,  0.2302, -0.3221,  0.4996,  0.4346, -0.1935,</span></div></div><div><div><div>47</div></div><div><span><span>          </span></span><span>0.2968, -0.9292,  0.5326, -0.3695, -0.9876, -0.4770, -0.9902,  0.6349,</span></div></div><div><div><div>48</div></div><div><span><span>         </span></span><span>-0.1863, -0.2612,  0.9123, -0.1930,  0.3110,  0.0803, -0.7598, -1.0000,</span></div></div><div><div><div>49</div></div><div><span><span>          </span></span><span>0.0292, -0.0628, -0.1086, -0.2135, -0.9671, -0.9521,  0.3334,  0.8675,</span></div></div><div><div><div>50</div></div><div><span><span>          </span></span><span>0.2254,  0.9995, -0.2908,  0.9420,  0.0336, -0.4542,  0.4123, -0.4455,</span></div></div><div><div><div>51</div></div><div><span><span>          </span></span><span>0.4558, -0.4442, -0.0685,  0.3043,  0.0575,  0.1843, -0.6577, -0.3121,</span></div></div><div><div><div>52</div></div><div><span><span>         </span></span><span>-0.1069, -0.7729, -0.2328,  0.9032, -0.4681, -0.5580,  0.3359, -0.1644,</span></div></div><div><div><div>53</div></div><div><span><span>         </span></span><span>-0.1572,  0.6441,  0.2810,  0.2651,  0.1532,  0.4485, -0.5299,  0.2616,</span></div></div><div><div><div>54</div></div><div><span><span>         </span></span><span>-0.7412, -0.0347,  0.2560, -0.2659, -0.6092, -0.9868, -0.2346,  0.4682,</span></div></div><div><div><div>55</div></div><div><span><span>          </span></span><span>0.9771,  0.5646,  0.2360,  0.4567, -0.2170,  0.1420, -0.9554,  0.9832,</span></div></div><div><div><div>56</div></div><div><span><span>         </span></span><span>-0.0935,  0.2621, -0.7901,  0.5758, -0.7642, -0.5362,  0.6374, -0.3891,</span></div></div><div><div><div>57</div></div><div><span><span>         </span></span><span>-0.6368, -0.0045, -0.2658, -0.1721, -0.7466,  0.4832, -0.3128, -0.2640,</span></div></div><div><div><div>58</div></div><div><span>...</span></div></div><div><div><div>59</div></div><div><span><span>         </span></span><span>-0.0989, -0.3853, -0.3173, -0.5348,  0.5307, -0.6883, -0.3786, -0.4665,</span></div></div><div><div><div>60</div></div><div><span><span>          </span></span><span>0.7207,  0.3239,  0.9999, -0.5526, -0.4825, -0.3439, -0.3580,  0.1069,</span></div></div><div><div><div>61</div></div><div><span><span>         </span></span><span>-0.2620, -1.0000,  0.2673, -0.3037,  0.4706, -0.6132,  0.8511, -0.4910,</span></div></div><div><div><div>62</div></div><div><span><span>         </span></span><span>-0.7231, -0.1092,  0.5412,  0.5265, -0.4635, -0.2461,  0.4946, -0.3469,</span></div></div><div><div><div>63</div></div><div><span><span>          </span></span><span>0.9149,  0.5902, -0.1862,  0.7004,  0.5465, -0.2839, -0.5723,  0.6648]])</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>tensor([[True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>66</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>67</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>68</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>69</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>70</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>71</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>72</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>73</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>74</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>75</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>76</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>77</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>78</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>79</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>80</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>81</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>82</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>83</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>84</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>85</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>86</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>87</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>88</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>89</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>90</div></div><div><span>...</span></div></div><div><div><div>91</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>92</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>93</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>94</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True,</span></div></div><div><div><div>95</div></div><div><span><span>         </span></span><span>True, True, True, True, True, True, True, True, True, True, True, True]])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可以看到，<code>outputs[1]</code> 实际上就是 <code>outputs[0]</code> 中 <code>[CLS]</code> 的那个 1 * 768 维的向量，再经过一个 <strong>全连接层 + tanh 激活函数</strong>得到的东西。</p><p>但对于 <code>model.pooler(outputs[2][-1])</code>，<code>outputs[2][-1]</code> 似乎拿出的是最后一个隐藏层的状态，而 outputs[1] 是最后一个隐藏层状态的第一个 token <code>[CLS]</code> 经过激活函数后的输出。那么，<code>model.pooler(outputs[2][-1])</code> 不应该是对最后一个隐藏层的所有 tokens 都做激活函数处理吗？为什么他会与只取第一个 token 的 <code>outputs[1]</code> 相等？</p><p>的确，<strong>如果 pooler 真的对全部 tokens 都做激活，那么输出形状应该是 (1, 22, 768)，不可能和 (1, 768) 的 <code>outputs[1]</code> 相等</strong>。</p><p>关键的秘密在于：<strong><code>model.pooler</code> 内部其实只取了第一个 token（<code>[CLS]</code>）</strong>，而不是对所有 tokens 做处理。</p><p>我们来看看 HuggingFace 的源码：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>BertPooler</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>config</span></span><span>):</span></div></div><div><div><div>3</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.dense </span><span>=</span><span> nn.</span><span>Linear</span><span>(config.hidden_size, config.hidden_size)</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.activation </span><span>=</span><span> nn.</span><span>Tanh</span><span>()</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>hidden_states</span></span><span>):</span></div></div><div><div><div>8</div></div><div><span>        </span><span># ⭐ 关键在这里：只取第一个 token ([CLS])！</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>first_token_tensor </span><span>=</span><span> hidden_states[:, </span><span>0</span><span>]</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>pooled_output </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>dense</span><span>(first_token_tensor)</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>pooled_output </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>activation</span><span>(pooled_output)</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> pooled_output</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看到 <code>hidden_states[:, 0]</code> 了吗？Pooler 在 forward 的第一步就做了<strong>切片</strong>——从 <code>(1, 22, 768)</code> 中只取出 <code>[CLS]</code> 对应的 <code>(1, 768)</code>，<strong>剩下的 21 个 token 直接丢弃了</strong>。</p><p>所以整个过程是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[2][-1]  shape: (1, 22, 768)</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>      </span></span><span>│  pooler.forward() 内部:</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>│    hidden_states[:, 0]   ← 只取 [CLS]</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>│    ↓</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>│    shape: (1, 768)</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>│    ↓</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>│    nn.Linear(768→768)</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>│    ↓</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>│    nn.Tanh()</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>│    ↓</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>▼</span></div></div><div><div><div>13</div></div><div><span>pooler_output  shape: (1, 768)  ✅ 与 outputs[1] 一致</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而 <code>outputs[1]</code> 本质上就是 BERT 模型内部调用了完全相同的这个 <code>model.pooler(outputs[0])</code> 计算出来的，所以二者必然相等。</p><p><strong>总结</strong>：Pooler 不是”对所有 tokens 做激活”，而是”<strong>只取 [CLS] 的向量，然后做全连接+tanh</strong>”。它的设计目的就是为<strong>句子级别的分类任务</strong>压缩出一个固定长度的句子表示向量。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs[</span><span>2</span><span>][</span><span>0</span><span>]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>model.</span><span>embeddings</span><span>(token_input[</span></span><span>'input_ids'</span><span>], token_input[</span><span>'token_type_ids'</span><span>])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>outputs[</span><span>2</span><span>][</span><span>0</span><span><span>] </span><span>==</span><span> model.</span><span>embeddings</span><span>(token_input[</span></span><span>'input_ids'</span><span>], token_input[</span><span>'token_type_ids'</span><span>]) </span><span># True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1686, -0.2858, -0.3261,  ..., -0.0276,  0.0383,  0.1640],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[ 0.2329,  0.1390,  0.2979,  ..., -0.0655,  0.8885,  0.5109],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[ 0.2257, -0.7165, -0.7255,  ...,  0.4844,  0.6030, -0.0957],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[-0.0374, -0.6155, -1.4419,  ...,  0.0793, -0.0811, -0.3802],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[-0.0228,  0.4207, -0.3288,  ...,  0.4464,  0.5178,  0.5501],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.2350,  0.1566, -0.0462,  ..., -0.4206,  0.3074, -0.2288]]])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>tensor([[[ 0.1686, -0.2858, -0.3261,  ..., -0.0276,  0.0383,  0.1640],</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>[ 0.2329,  0.1390,  0.2979,  ..., -0.0655,  0.8885,  0.5109],</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>[ 0.2257, -0.7165, -0.7255,  ...,  0.4844,  0.6030, -0.0957],</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>[-0.0374, -0.6155, -1.4419,  ...,  0.0793, -0.0811, -0.3802],</span></div></div><div><div><div>14</div></div><div><span><span>         </span></span><span>[-0.0228,  0.4207, -0.3288,  ...,  0.4464,  0.5178,  0.5501],</span></div></div><div><div><div>15</div></div><div><span><span>         </span></span><span>[-0.2350,  0.1566, -0.0462,  ..., -0.4206,  0.3074, -0.2288]]],</span></div></div><div><div><div>16</div></div><div><span><span>       </span></span><span>grad_fn=&lt;NativeLayerNormBackward0&gt;)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>tensor([[[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>19</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>20</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>21</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>22</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>23</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True],</span></div></div><div><div><div>24</div></div><div><span><span>         </span></span><span>[True, True, True,  ..., True, True, True]]])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可见，<code>outputs[2][0]</code> 实际上是所有隐藏层状态中的第一个，也就是 embedding 层，它的后面才是 12 个 transformer 层。</p><p>自然，embedding 层等于 <code>model.embeddings(token_input['input_ids'], token_input['token_type_ids'])</code>. 注意这里的 <code>model.embeddings()</code> 不接受 <code>attention_mask</code> 参数，所以我们只传入 <code>input_ids</code> 和 <code>token_type_ids</code>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(outputs[</span><span>2</span><span>])):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>print</span><span>(i, outputs[</span><span>2</span><span>][i].shape)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>0 torch.Size([1, 22, 768])</span></div></div><div><div><div>2</div></div><div><span>1 torch.Size([1, 22, 768])</span></div></div><div><div><div>3</div></div><div><span>2 torch.Size([1, 22, 768])</span></div></div><div><div><div>4</div></div><div><span>3 torch.Size([1, 22, 768])</span></div></div><div><div><div>5</div></div><div><span>4 torch.Size([1, 22, 768])</span></div></div><div><div><div>6</div></div><div><span>5 torch.Size([1, 22, 768])</span></div></div><div><div><div>7</div></div><div><span>6 torch.Size([1, 22, 768])</span></div></div><div><div><div>8</div></div><div><span>7 torch.Size([1, 22, 768])</span></div></div><div><div><div>9</div></div><div><span>8 torch.Size([1, 22, 768])</span></div></div><div><div><div>10</div></div><div><span>9 torch.Size([1, 22, 768])</span></div></div><div><div><div>11</div></div><div><span>10 torch.Size([1, 22, 768])</span></div></div><div><div><div>12</div></div><div><span>11 torch.Size([1, 22, 768])</span></div></div><div><div><div>13</div></div><div><span>12 torch.Size([1, 22, 768])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就能很清晰地看到所有隐藏层的形状了。</p><p>最后，详细解释如下。</p><section><h2>3.1 为什么 <code>len(outputs) == 3</code><a href="#31-为什么-lenoutputs--3"><span>#</span></a></h2><p><code>outputs</code> 是 HuggFace 的 <code>BaseModelOutputWithPoolingAndCrossAttentions</code> 对象，它的设计类似一个<strong>命名元组</strong>，支持两种访问方式：</p>

<table><thead><tr><th>索引</th><th>属性名</th><th>内容</th><th>shape</th></tr></thead><tbody><tr><td><code>outputs[0]</code></td><td><code>outputs.last_hidden_state</code></td><td>最后一层编码器输出</td><td>1 × 22 × 768</td></tr><tr><td><code>outputs[1]</code></td><td><code>outputs.pooler_output</code></td><td>池化后的 [CLS] 向量</td><td>1 × 768</td></tr><tr><td><code>outputs[2]</code></td><td><code>outputs.hidden_states</code></td><td><strong>所有层的 hidden states（元组）</strong></td><td>13个 1×22×768</td></tr></tbody></table><p>这就像你有一个<strong>文件夹</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>outputs/                  ← len(outputs) = 3（顶层文件夹数）</span></div></div><div><div><div>2</div></div><div><span>├── last_hidden_state/    ← outputs[0]，里面有 1×22×768 个数字</span></div></div><div><div><div>3</div></div><div><span>├── pooler_output/        ← outputs[1]，里面有 1×768 个数字</span></div></div><div><div><div>4</div></div><div><span>└── hidden_states/        ← outputs[2]，里面还有 13 个子文件夹</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>├── layer_0 (embedding)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>├── layer_1</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>├── ...</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>└── layer_12</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong><code>len()</code> 只统计顶层有几个”文件夹”，而不是递归数里面所有的”文件”。</strong> 所以返回 3 是对的 ✅</p></section><section><h2>3.2 三个输出分别是什么？<a href="#32-三个输出分别是什么"><span>#</span></a></h2><section><h3>① <code>last_hidden_state</code>（最后一层隐藏状态）<a href="#-last_hidden_state最后一层隐藏状态"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>shape: (batch_size=1, seq_len=22, hidden_size=768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>含义：</strong> 句子中<strong>每个 token</strong> 经过 BERT 全部 12 层 transformer 编码后的最终向量表示。</p><ul>
<li>每一行对应一个 token（<code>[CLS]</code> → <code>after</code> → <code>stealing</code> → … → <code>bank</code> → <code>[SEP]</code>）</li>
<li>每个 token 被编码为一个 <strong>768 维的稠密向量</strong></li>
<li>这 22 个向量<strong>各自包含了上下文信息</strong>——比如句子中两个 <code>bank</code> 虽然 token 相同，但它们在第 10 行和第 20 行的向量是<strong>不一样</strong>的，因为上下文不同</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>例如：token "bank" 出现了 3 次</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>position 6:  "the bank vault"   → 银行（金融机构）</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>position 10: "the bank robber"  → 银行（金融机构）</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>position 19: "river bank"       → 河岸（地理位置）</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>→ 这三个 bank 的 768 维向量是不同的！因为 BERT 编码了上下文</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p><strong>用途：</strong> 这是最通用的特征，可以用来做<strong>序列标注</strong>（命名实体识别、词性标注）或直接取某个位置的向量做下游任务。</p></blockquote><hr /></section><section><h3>② <code>pooler_output</code>（池化输出）<a href="#-pooler_output池化输出"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>shape: (batch_size=1, hidden_size=768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>含义：</strong> 专门为<strong>句子级别的分类</strong>任务设计的输出。</p><p>它的计算方式：</p><ol>
<li>先取出 <code>last_hidden_state</code> 中第一个 token（<code>[CLS]</code>）的向量</li>
<li>再经过一个 <strong>全连接层 + tanh 激活函数</strong></li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>last_hidden_state[0, 0, :]  →  [CLS] 的 768 维向量</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>全连接层 (768 → 768)</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>tanh 激活</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>pooler_output  (768 维)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为什么要有 <code>pooler_output</code>？</p><p>BERT 在预训练时，<code>[CLS]</code> token 被设计用来<strong>聚合整个句子的信息</strong>（做下一句预测任务）。所以 <code>pooler_output</code> 是对整个句子的<strong>压缩表示</strong>。</p><blockquote><p><strong>用途：</strong> 文本分类、情感分析、句子对匹配等<strong>句子级别</strong>的任务。</p></blockquote><hr /></section><section><h3>③ <code>hidden_states</code>（所有隐藏层状态）<a href="#-hidden_states所有隐藏层状态"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>type: tuple of 13 tensors</span></div></div><div><div><div>2</div></div><div><span>每个 tensor shape: (batch_size=1, seq_len=22, hidden_size=768)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>含义：</strong> BERT <strong>每一层</strong>的输出都被保存下来了，包括第 0 层（embedding 层）到第 12 层（最后一层 transformer）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hidden_states[0]  → embedding 层的输出（还没经过 transformer）</span></div></div><div><div><div>2</div></div><div><span>hidden_states[1]  → 第 1 层 transformer 的输出</span></div></div><div><div><div>3</div></div><div><span>hidden_states[2]  → 第 2 层 transformer 的输出</span></div></div><div><div><div>4</div></div><div><span>...</span></div></div><div><div><div>5</div></div><div><span>hidden_states[12] → 第 12 层 transformer 的输出 = last_hidden_state 🎯</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为什么需要看每一层？</p><p>不同层的编码器学到了<strong>不同层次的语言特征</strong>：</p>

<table><thead><tr><th>层次</th><th>学到了什么</th><th>类比</th></tr></thead><tbody><tr><td>底层（1-4）</td><td>词法、语法特征</td><td>像学语言时先认单词、学词性</td></tr><tr><td>中层（5-8）</td><td>句法、语义特征</td><td>理解短语结构、主谓关系</td></tr><tr><td>高层（9-12）</td><td>上下文语义、抽象概念</td><td>理解整体意思、指代关系</td></tr></tbody></table><blockquote><p><strong>用途：</strong> 某些任务中<strong>拼接多层</strong>的特征比只用最后一层效果更好（比如命名实体识别中拼最后 4 层）。这也是为什么需要设置 <code>output_hidden_states=True</code>。</p></blockquote><hr /></section><section><h3>一张图总结<a href="#一张图总结"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入句子: "After stealing money from the bank vault..."</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>Tokenization</span></div></div><div><div><div>4</div></div><div><span><span>              </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>┌──────────────────┐</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>│  Embedding 层     │  ← hidden_states[0]</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>└────────┬─────────┘</span></div></div><div><div><div>8</div></div><div><span><span>              </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>┌──────────────────┐</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>│  Transformer 1    │  ← hidden_states[1]</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>└────────┬─────────┘</span></div></div><div><div><div>12</div></div><div><span><span>              </span></span><span>↓</span></div></div><div><div><div>13</div></div><div><span><span>           </span></span><span>...</span></div></div><div><div><div>14</div></div><div><span><span>              </span></span><span>↓</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>┌──────────────────┐</span></div></div><div><div><div>16</div></div><div><span><span>     </span></span><span>│  Transformer 12   │  ← hidden_states[12] = last_hidden_state</span></div></div><div><div><div>17</div></div><div><span><span>     </span></span><span>└────────┬─────────┘</span></div></div><div><div><div>18</div></div><div><span><span>              </span></span><span>↓</span></div></div><div><div><div>19</div></div><div><span><span>       </span></span><span>取 [CLS] 位置</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>+ 全连接层 + tanh</span></div></div><div><div><div>21</div></div><div><span><span>              </span></span><span>↓</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>pooler_output</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-06/</id>
      <title type="text">06_subword_wordpiece_tokenizer</title>
      <published>2026-06-11T00:00:00.000Z</published>
      <updated>2026-06-18T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-06/"/>
      <summary type="text">动手写 BERT 系列笔记第六节</summary>
      <content type="html"><![CDATA[<p>我们来看下面几个语料：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>s1 </span><span>=</span><span> </span></span><span>'albums sold 500,000 copies'</span><span> </span><span># tokenizer 如何处理数字</span></div></div><div><div><div>2</div></div><div><span><span>s2 </span><span>=</span><span> </span></span><span>'techically perfect, melodically correct'</span><span> </span><span># tokenizer 如何处理类似 melodically 这样的单词</span></div></div><div><div><div>3</div></div><div><span><span>s3 </span><span>=</span><span> </span></span><span>'featuring a previously unheard track'</span><span> </span><span># tokenizer 如何处理 unheard 这样的单词</span></div></div><div><div><div>4</div></div><div><span><span>s4 </span><span>=</span><span> </span></span><span>'best-selling music artist'</span><span> </span><span># tokenizer 如何处理连字符</span></div></div><div><div><div>5</div></div><div><span><span>s5 </span><span>=</span><span> </span></span><span>'s1 d1 o1 and o2'</span><span> </span><span># tokenizer 如何处理缩写和数字</span></div></div><div><div><div>6</div></div><div><span><span>s6 </span><span>=</span><span> </span></span><span>'asbofwheowbeif'</span><span> </span><span># tokenizer 如何处理未知词汇</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<section><h1>0. 实例化 tokenizer<a href="#0-实例化-tokenizer"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h1>1. <code>vocab</code><a href="#1-vocab"><span>#</span></a></h1><ul>
<li><code>tokenizer.vocab</code> vs. <code>tokenizer.convert_ids_to_tokens</code></li>
<li><code>len(tokenizer.vocab) == 30522</code></li>
</ul><p>下面我们先看看 tokenizer 的词表：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tokenizer.vocab</span></div></div><div><div><div>2</div></div><div><span>tokenizer.convert_ids_to_tokens</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'butt': 10007,</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>'film': 2143,</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>'##kia': 21128,</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>'bart': 12075,</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>'wrath': 14532,</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>'humidity': 18213,</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>'streetcar': 21420,</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>'unable': 4039,</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>'taiwanese': 16539,</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>'[unused908]': 913,</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>'lyman': 27587,</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>'slain': 19668,</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>'[unused182]': 187,</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>'regimental': 17604,</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>'prolonged': 15330,</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>'libraries': 8860,</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>'explicitly': 12045,</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>'38th': 22051,</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>'extinction': 14446,</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>'\\': 1032,</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>'mammals': 11993,</span></div></div><div><div><div>22</div></div><div><span><span> </span></span><span>'constraint': 27142,</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>'masters': 5972,</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>'madeira': 27309,</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>'##pee': 28084,</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>'eyebrow': 9522,</span></div></div><div><div><div>28</div></div><div><span><span> </span></span><span>'rushes': 18545,</span></div></div><div><div><div>29</div></div><div><span><span> </span></span><span>'kung': 18577,</span></div></div><div><div><div>30</div></div><div><span><span> </span></span><span>'isla': 25340,</span></div></div><div><div><div>31</div></div><div><span><span> </span></span><span>...}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>&lt;bound method TokenizersBackend.convert_ids_to_tokens of BertTokenizer(name_or_path='bert-base-uncased', vocab_size=30522, model_max_length=512, padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}, added_tokens_decoder={</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>0: AddedToken("[PAD]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>100: AddedToken("[UNK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>101: AddedToken("[CLS]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>102: AddedToken("[SEP]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>103: AddedToken("[MASK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>39</div></div><div><span>})&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>实际上，<code>tokenizer.vocab</code> 与 <code>tokenizer.convert_ids_to_tokens</code> 是相对应的：</p><ul>
<li><code>tokenizer.vocab</code> 展示了 word 与 id 的对应关系；</li>
<li><code>tokenizer.convert_ids_to_tokens</code> 则展示了 id 与 word 的对应关系。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len</span><span>(tokenizer.vocab) </span><span># 30522</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>tokenizer.vocab[</span><span>'[UNK]'</span><span>] </span><span># 100</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>词表大小 30522</li>
<li><code>[UNK]</code> 对应的 id 为 100</li>
</ul></section>
<section><h1>2. 样本子词测试<a href="#2-样本子词测试"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span><span>(</span><span>tokenizer</span><span>(s1))</span></span></div></div><div><div><div>2</div></div><div><span>print</span><span><span>(</span><span>tokenizer</span><span>(s2))</span></span></div></div><div><div><div>3</div></div><div><span>print</span><span><span>(</span><span>tokenizer</span><span>(s3))</span></span></div></div><div><div><div>4</div></div><div><span>print</span><span><span>(</span><span>tokenizer</span><span>(s4))</span></span></div></div><div><div><div>5</div></div><div><span>print</span><span><span>(</span><span>tokenizer</span><span>(s5))</span></span></div></div><div><div><div>6</div></div><div><span>print</span><span><span>(</span><span>tokenizer</span><span>(s6))</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': [101, 4042, 2853, 3156, 1010, 2199, 4809, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>{'input_ids': [101, 6627, 15004, 3819, 1010, 17187, 3973, 6149, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>{'input_ids': [101, 3794, 1037, 3130, 4895, 26362, 2650, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>{'input_ids': [101, 2190, 1011, 4855, 2189, 3063, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>{'input_ids': [101, 1055, 2487, 1040, 2487, 1051, 2487, 1998, 1051, 2475, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>{'input_ids': [101, 2004, 5092, 2546, 2860, 5369, 5004, 19205, 2546, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span><span>(tokenizer.</span><span>tokenize</span><span>(s1))</span></span></div></div><div><div><div>2</div></div><div><span>print</span><span><span>(tokenizer.</span><span>tokenize</span><span>(s2))</span></span></div></div><div><div><div>3</div></div><div><span>print</span><span><span>(tokenizer.</span><span>tokenize</span><span>(s3))</span></span></div></div><div><div><div>4</div></div><div><span>print</span><span><span>(tokenizer.</span><span>tokenize</span><span>(s4))</span></span></div></div><div><div><div>5</div></div><div><span>print</span><span><span>(tokenizer.</span><span>tokenize</span><span>(s5))</span></span></div></div><div><div><div>6</div></div><div><span>print</span><span><span>(tokenizer.</span><span>tokenize</span><span>(s6))</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['albums', 'sold', '500', ',', '000', 'copies']</span></div></div><div><div><div>2</div></div><div><span>['tech', '##ically', 'perfect', ',', 'melodic', '##ally', 'correct']</span></div></div><div><div><div>3</div></div><div><span>['featuring', 'a', 'previously', 'un', '##heard', 'track']</span></div></div><div><div><div>4</div></div><div><span>['best', '-', 'selling', 'music', 'artist']</span></div></div><div><div><div>5</div></div><div><span>['s', '##1', 'd', '##1', 'o', '##1', 'and', 'o', '##2']</span></div></div><div><div><div>6</div></div><div><span>['as', '##bo', '##f', '##w', '##he', '##ow', '##bei', '##f']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>2.1 <code>##</code> 符号是什么意思<a href="#21--符号是什么意思"><span>#</span></a></h2><p><code>##</code> 是 <strong>WordPiece 分词算法</strong>（BERT 使用的分词方法）中的一个重要标记。它<strong>表示这个子词（subword）是某个单词的”后续部分”，不是新词的开头。</strong></p><p>换句话说，在还原回原句时，<code>##</code> 前面的部分和 <code>##</code> 后面的部分是要<strong>拼在一起</strong>的。</p><hr /><section><h3>2.1.1 具体例子分析<a href="#211-具体例子分析"><span>#</span></a></h3><section><h4>例 1：<code>s2 = "techically perfect, melodically correct"</code><a href="#例-1s2--techically-perfect-melodically-correct"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['tech', '##ically', 'perfect', ',', 'melodic', '##ally', 'correct']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>"tech" + "##ically"</code> = <strong>technically</strong>（原词被拆成两部分）</li>
<li><code>"melodic" + "##ally"</code> = <strong>melodically</strong>（同理）</li>
</ul><p>词表中没有完整的 <code>"technically"</code> 这个词，但有 <code>"tech"</code> 和 <code>"##ically"</code>，所以 WordPiece 把它们拼起来表示原词。</p><hr /></section><section><h4>例 2：<code>s3 = "featuring a previously unheard track"</code><a href="#例-2s3--featuring-a-previously-unheard-track"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['featuring', 'a', 'previously', 'un', '##heard', 'track']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>"un" + "##heard"</code> = <strong>unheard</strong>（原词被拆为前缀 + 剩余部分）</li>
</ul><hr /></section><section><h4>例 3：<code>s5 = "s1 d1 o1 and o2"</code><a href="#例-3s5--s1-d1-o1-and-o2"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['s', '##1', 'd', '##1', 'o', '##1', 'and', 'o', '##2']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>"s" + "##1"</code> = <strong>s1</strong>，<code>"d" + "##1"</code> = <strong>d1</strong>，<code>"o" + "##1"</code> = <strong>o1</strong>，<code>"o" + "##2"</code> = <strong>o2</strong></li>
<li>词表中没有 <code>"s1"</code> 这个完整词，但有 <code>"s"</code> 和 <code>"##1"</code>，所以拼起来</li>
</ul><hr /></section><section><h4>例 4：<code>s6 = "asbofwheowbeif"</code>（生造词）<a href="#例-4s6--asbofwheowbeif生造词"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['as', '##bo', '##f', '##w', '##he', '##ow', '##bei', '##f']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>这是一个不存在的词，词表中完全没有</li>
<li>WordPiece 会尽可能用<strong>已知子词</strong>来拼凑：<code>"as" + "##bo" + "##f" + "##w" + "##he" + "##ow" + "##bei" + "##f"</code></li>
<li>注意：这里<strong>没有出现</strong> <code>[UNK]</code>（未知标记），说明 WordPiece 成功用子词拼了出来</li>
</ul><hr /></section></section><section><h3>2.1.2 词表中的 <code>##</code> 长什么样？<a href="#212-词表中的--长什么样"><span>#</span></a></h3><p>从前面打印的词表可以看到，<code>##</code> 开头的词条确实存在：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tokenizer.vocab</span></div></div><div><div><div>2</div></div><div><span># ... '##kia': 21128, '##pee': 28084, ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>"##kia"</code> 就是专门用来接在某个词根后面的子词。比如 <code>"olympia"</code> → <code>"olym" + "##pi" + "##a"</code>，但 <code>"##kia"</code> 可能出现在类似 <code>"magnolia"</code> → <code>"magnol" + "##ia"</code> 的场景中（具体取决于词表）。</p><hr /></section><section><h3>2.1.3 总结一下<a href="#213-总结一下"><span>#</span></a></h3>

<table><thead><tr><th>符号</th><th>含义</th><th>示例</th></tr></thead><tbody><tr><td><strong>完整词</strong></td><td>词表中直接有这个单词</td><td><code>"albums"</code>, <code>"perfect"</code>, <code>"featuring"</code></td></tr><tr><td><strong><code>##xxx</code></strong></td><td>是前一个子词的<strong>后缀</strong>，需要拼在一起</td><td><code>"tech" + "##ically"</code> → <code>"technically"</code></td></tr><tr><td>没有 <code>##</code></td><td>表示<strong>新词的开头</strong>（或完整词）</td><td><code>"un"</code>, <code>"melodic"</code> 都是独立子词的开头</td></tr></tbody></table><p>所以通过 <code>##</code> 标记，BERT 的 tokenizer 可以<strong>用有限的词表（30522 个词条）覆盖无限的词汇</strong>——即使没见过的词，也能拆成已知子词来理解。这正是 WordPiece 算法的核心思想 🎯<code>"##kia"</code> 就是专门用来接在某个词根后面的子词。比如 <code>"olympia"</code> → <code>"olym" + "##pi" + "##a"</code>，但 <code>"##kia"</code> 可能出现在类似 <code>"magnolia"</code> → <code>"magnol" + "##ia"</code> 的场景中（具体取决于词表）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>cnt_sharp </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>2</div></div><div><span>for</span><span> token, </span><span>id</span><span> </span><span>in</span><span><span> tokenizer.vocab.</span><span>items</span><span>():</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span><span> token.</span><span>startswith</span><span>(</span></span><span>'##'</span><span>):</span></div></div><div><div><div>4</div></div><div><span>        </span><span>print</span><span>(token, </span><span>id</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>cnt_sharp </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>6</div></div><div><span>print</span><span>(cnt_sharp) </span><span># 5828</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>##ode 10244</span></div></div><div><div><div>2</div></div><div><span>##■ 30144</span></div></div><div><div><div>3</div></div><div><span>##gai 23805</span></div></div><div><div><div>4</div></div><div><span>##ツ 30238</span></div></div><div><div><div>5</div></div><div><span>##ɹ 29691</span></div></div><div><div><div>6</div></div><div><span>##dice 24598</span></div></div><div><div><div>7</div></div><div><span>##gical 26715</span></div></div><div><div><div>8</div></div><div><span>##ake 13808</span></div></div><div><div><div>9</div></div><div><span>##edo 26010</span></div></div><div><div><div>10</div></div><div><span>##ר 29811</span></div></div><div><div><div>11</div></div><div><span>##gating 16961</span></div></div><div><div><div>12</div></div><div><span>##lese 24527</span></div></div><div><div><div>13</div></div><div><span>##aj 13006</span></div></div><div><div><div>14</div></div><div><span>##re 2890</span></div></div><div><div><div>15</div></div><div><span>##rians 23543</span></div></div><div><div><div>16</div></div><div><span>##宗 30347</span></div></div><div><div><div>17</div></div><div><span>##ates 8520</span></div></div><div><div><div>18</div></div><div><span>##rys 24769</span></div></div><div><div><div>19</div></div><div><span>##helm 24546</span></div></div><div><div><div>20</div></div><div><span>##dict 29201</span></div></div><div><div><div>21</div></div><div><span>##rained 27361</span></div></div><div><div><div>22</div></div><div><span>##lena 20844</span></div></div><div><div><div>23</div></div><div><span>##tale 22059</span></div></div><div><div><div>24</div></div><div><span>##water 5880</span></div></div><div><div><div>25</div></div><div><span>##ivist 21997</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span>##hee 21030</span></div></div><div><div><div>28</div></div><div><span>##ق 29834</span></div></div><div><div><div>29</div></div><div><span>##rds 17811</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>5828</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可见，词表中共有 5828 个 <code>##</code> 开头的词。</p></section></section></section>
<section><h1>3. Summary<a href="#3-summary"><span>#</span></a></h1><ul>
<li>tokenizer 不会轻易将一个词处理为 <code>[UNK]</code> (100)</li>
<li>基于词表，tokenizer, encode, decode 为一体
<ul>
<li>tokenize: word -&gt; tokens(s), 将 word 尽可能地映射为 vocab 中的 keys</li>
<li>encode: token -&gt; id</li>
<li>decode: id -&gt; token -&gt; word
<ul>
<li>encode 完后并不是终点，decode 还要能很好地将 id 还原，尽可能与输入的 word 对齐</li>
</ul>
</li>
</ul>
</li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-05/</id>
      <title type="text">05_BERT+embedding_source_code</title>
      <published>2026-06-10T00:00:00.000Z</published>
      <updated>2026-06-17T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-05/"/>
      <summary type="text">动手写 BERT 系列笔记第五节</summary>
      <content type="html"><![CDATA[<p>本节主要关注 BERT 模型中 Embedding 部分的细节与源码。</p>
<p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260606164624475.png" alt="" /></p>
<section><h1>1. 总览<a href="#1-总览"><span>#</span></a></h1><ul>
<li>BERT imput embedding: 一种查表操作（lookup table）
<ul>
<li>查表
<ul>
<li>token embeddings: <span><span>30522×76830522\times 768</span><span><span><span></span><span>30522</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></li>
<li>segment embeddings: <span><span>0/1×8=7680/1 \times 8=768</span><span><span><span></span><span>0/1</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span><span></span><span>=</span><span></span></span><span><span></span><span>768</span></span></span></span></li>
<li>position embeddings: <span><span>512×768512\times 768</span><span><span><span></span><span>512</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></li>
</ul>
</li>
<li>后处理
<ul>
<li>layer norm</li>
<li>dropout</li>
</ul>
</li>
</ul>
</li>
</ul><section><h2>1.1 详解<a href="#11-详解"><span>#</span></a></h2><p>“查表” 这个词很形象：<strong>Embedding 本质上就是一张表（矩阵），每一行代表一个”东西”的向量表示</strong>。输入一个编号，就去表里找出对应的那一行向量。</p><p>BERT 的输入 Embedding 由 <strong>三张表</strong>（三个子嵌入）相加而成：</p><hr /><section><h3>1.1.1 Token Embeddings — <code>30522 × 768</code><a href="#111-token-embeddings--30522--768"><span>#</span></a></h3><ul>
<li><strong>30522</strong>：BERT 的词表大小，也就是 BERT 认识的”单词/子词”总数。</li>
<li><strong>768</strong>：每个 token 被映射成一个 <strong>768 维的向量</strong>。</li>
<li><strong>含义</strong>：这本质上是一个大矩阵，有 30522 行，每行是一个 768 维的向量。输入一个 token（比如 “猫”），就在这张表里找到对应的那一行，取出一个 768 维的向量。</li>
<li><strong>直觉理解</strong>：把每个单词”翻译”成计算机能理解的一串数字（向量）。</li>
</ul><hr /></section><section><h3>1.1.2 Segment Embeddings — <code>0/1 × 8 = 768</code><a href="#112-segment-embeddings--01--8--768"><span>#</span></a></h3><ul>
<li>BERT 在处理<strong>句子对</strong>任务（如问答、推理）时，需要区分两个句子。</li>
<li><strong>0/1</strong>：第一个句子标记为 0，第二个句子标记为 1。</li>
<li><strong>8</strong> 是隐藏维度（实际上就是 768），写法 <code>0/1 × 8 = 768</code> 意思是：
<ul>
<li>句子 A 的 segment embedding = 一个 768 维的全 0 向量</li>
<li>句子 B 的 segment embedding = 一个 768 维的全 1 向量</li>
</ul>
</li>
<li><strong>直觉理解</strong>：告诉 BERT “这个词属于第一句还是第二句”。</li>
</ul><hr /></section><section><h3>1.1.3 Position Embeddings — <code>512 × 768</code><a href="#113-position-embeddings--512--768"><span>#</span></a></h3><ul>
<li><strong>512</strong>：BERT 最大能处理的序列长度（512 个 token）。</li>
<li><strong>768</strong>：每个位置也是一个 768 维向量。</li>
<li>位置 0 有一个向量，位置 1 有另一个向量……位置 511 也有一个向量。</li>
<li><strong>直觉理解</strong>：因为 Transformer 没有”顺序感”，位置嵌入负责告诉模型”这个词在句子的第几个位置”。</li>
</ul><hr /></section><section><h3>1.1.4 核心公式<a href="#114-核心公式"><span>#</span></a></h3><p><span><span>最终输入=Token Embedding+Segment Embedding+Position Embedding\text{最终输入} = \text{Token Embedding} + \text{Segment Embedding} + \text{Position Embedding}</span><span><span><span></span><span><span>最终输入</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Token Embedding</span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>Segment Embedding</span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>Position Embedding</span></span></span></span></span></p><p>三个 768 维向量<strong>逐元素相加</strong>，结果还是一个 768 维向量，作为 BERT 第一层的输入。</p><hr /></section><section><h3>1.1.5 后处理（Post-processing）<a href="#115-后处理post-processing"><span>#</span></a></h3><p>相加之后，还要经过两步：</p><ol>
<li><strong>LayerNorm（层归一化）</strong> — 把数据拉到一个稳定的范围，加速训练、防止梯度爆炸/消失。</li>
<li><strong>Dropout</strong> — 训练时随机”丢弃”一部分神经元（设为 0），防止过拟合。</li>
</ol><hr /></section></section><section><h2>1.2 一张图总结<a href="#12-一张图总结"><span>#</span></a></h2>

<table><thead><tr><th>嵌入类型</th><th>矩阵维度</th><th>作用</th></tr></thead><tbody><tr><td>Token Embedding</td><td><span><span>30522×76830522 \times 768</span><span><span><span></span><span>30522</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></td><td>把词变成向量</td></tr><tr><td>Segment Embedding</td><td><span><span>2×7682 \times 768</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></td><td>区分句子 A/B</td></tr><tr><td>Position Embedding</td><td><span><span>512×768512 \times 768</span><span><span><span></span><span>512</span><span></span><span>×</span><span></span></span><span><span></span><span>768</span></span></span></span></td><td>告诉位置信息</td></tr></tbody></table><p>最后三个向量相加 → LayerNorm → Dropout，就得到了 BERT 的最终输入。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer, BertModel</span></div></div><div><div><div>2</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>3</div></div><div><span>from</span><span> torch </span><span>import</span><span> nn</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>3</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>test_sentence </span><span>=</span><span> </span></span><span>"This is a test sentence"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>inputs </span><span>=</span><span> </span><span>tokenizer</span><span>(test_sentence, </span></span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inputs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[ 101, 2023, 2003, 1037, 3231, 6251,  102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上述代码把句子 <strong>“This is a test sentence”</strong> 分词成了 7 个 token：</p>

<table><thead><tr><th>token ID</th><th>含义</th></tr></thead><tbody><tr><td>101</td><td><code>[CLS]</code> — 句子开头标记</td></tr><tr><td>2023</td><td>“this”</td></tr><tr><td>2003</td><td>“is”</td></tr><tr><td>1037</td><td>“a”</td></tr><tr><td>3231</td><td>“test”</td></tr><tr><td>6251</td><td>“sentence”</td></tr><tr><td>102</td><td><code>[SEP]</code> — 句子结束/分隔标记</td></tr></tbody></table><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>input_ids </span><span>=</span><span> inputs[</span></span><span>'input_ids'</span><span>]</span></div></div><div><div><div>2</div></div><div><span><span>token_type_ids </span><span>=</span><span> inputs[</span></span><span>'token_type_ids'</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model.</span><span>embeddings</span><span>(</span></span><span>input_ids</span><span><span>=</span><span>input_ids, </span></span><span>token_type_ids</span><span><span>=</span><span>token_type_ids)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1686, -0.2858, -0.3261,  ..., -0.0276,  0.0383,  0.1640],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.6485,  0.6739, -0.0932,  ...,  0.4475,  0.6696,  0.1820],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.6270, -0.0633, -0.3143,  ...,  0.3427,  0.4636,  0.4594],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.6010, -0.6970, -0.2001,  ...,  0.2960,  0.2060, -1.7181],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.8323,  0.2878,  0.0021,  ...,  0.2628, -1.1310, -1.2708],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.1481, -0.2948, -0.1690,  ..., -0.5009,  0.2544, -0.0700]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;NativeLayerNormBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这调用了 BERT 的 <strong>Embedding 层</strong>（我们上一轮刚讲过的那三层：Token + Segment + Position 嵌入相加 → LayerNorm → Dropout），<strong>返回的就是这 7 个 token 的最终嵌入向量</strong>。</p><p>这个张量的形状是 <strong><code>[1, 7, 768]</code></strong>：</p>

<table><thead><tr><th>维度</th><th>大小</th><th>含义</th></tr></thead><tbody><tr><td>dim 0</td><td><strong>1</strong></td><td>batch size（一次只输入了一个句子）</td></tr><tr><td>dim 1</td><td><strong>7</strong></td><td>序列长度（7 个 token）</td></tr><tr><td>dim 2</td><td><strong>768</strong></td><td>隐藏维度（每个 token 的嵌入向量维度）</td></tr></tbody></table><section><h3>1.3.1 每一行代表什么<a href="#131-每一行代表什么"><span>#</span></a></h3><p>输出的 <strong>7 行</strong>，每一行对应一个 token 的 <strong>768 维嵌入向量</strong>：</p>

<table><thead><tr><th>行</th><th>对应 token</th><th>768 维向量（部分显示）</th></tr></thead><tbody><tr><td>第 1 行</td><td><code>[CLS]</code></td><td><code>[ 0.1686, -0.2858, -0.3261, ..., -0.0276, 0.0383, 0.1640]</code></td></tr><tr><td>第 2 行</td><td>“this”</td><td><code>[-0.6485, 0.6739, -0.0932, ..., 0.4475, 0.6696, 0.1820]</code></td></tr><tr><td>第 3 行</td><td>“is”</td><td><code>[-0.6270, -0.0633, -0.3143, ..., 0.3427, 0.4636, 0.4594]</code></td></tr><tr><td>第 4-5 行</td><td>“a”、“test”</td><td>…（中间省略）</td></tr><tr><td>第 6 行</td><td>“sentence”</td><td><code>[ 0.8323, 0.2878, 0.0021, ..., 0.2628, -1.1310, -1.2708]</code></td></tr><tr><td>第 7 行</td><td><code>[SEP]</code></td><td><code>[-0.1481, -0.2948, -0.1690, ..., -0.5009, 0.2544, -0.0700]</code></td></tr></tbody></table></section><section><h3>1.3.2 这些数字到底是什么？<a href="#132-这些数字到底是什么"><span>#</span></a></h3><p>每个 token 的 768 个数字共同构成了它的 <strong>语义向量（embedding vector）</strong>：</p><ul>
<li><strong>每个数字本身没有直观含义</strong>，我们不能说”0.1686 代表什么”。</li>
<li>但所有这些数字的<strong>组合</strong>（模式、方向、距离），就编码了这个 token 的<strong>语义信息</strong>。</li>
<li>比如：语义相近的词，它们的 768 维向量在<strong>高维空间中的距离更近</strong>（余弦相似度更高）。</li>
</ul></section><section><h3>1.3.3 <code>grad_fn=&lt;NativeLayerNormBackward0&gt;</code> 是什么意思？<a href="#133-grad_fnnativelayernormbackward0-是什么意思"><span>#</span></a></h3><p>这表示这个张量是<strong>计算图的一部分</strong>，可以自动求梯度（<code>requires_grad=True</code>）。<code>NativeLayerNormBackward0</code> 告诉我们这个张量是由 <strong>LayerNorm</strong> 操作产生的，所以它的反向传播函数（用于计算梯度）来自 LayerNorm。简单说就是：<strong>这个张量可以参与梯度下降训练</strong>。</p><hr /></section><section><h3>1.3.4 用一张图总结<a href="#134-用一张图总结"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>句子: "This is a test sentence"</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>│ 分词</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>▼</span></div></div><div><div><div>4</div></div><div><span>tokens: [CLS]  this   is    a    test  sentence  [SEP]</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>│      │     │     │     │      │        │</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>▼      ▼     ▼     ▼     ▼      ▼        ▼</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>┌──────────────────────────────────────────────┐</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>│         BERT Embedding Layer                 │</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>│  (Token Emb + Segment Emb + Position Emb     │</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>│        → LayerNorm → Dropout)                │</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>└──────────────────────────────────────────────┘</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>│      │     │     │     │      │        │</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>▼      ▼     ▼     ▼     ▼      ▼        ▼</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>768维   768维  768维  768维  768维   768维   768维   ← 这就是我们看到的输出</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>向量    向量   向量   向量   向量    向量    向量</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>所以，我们看到的这个 <code>[1, 7, 768]</code> 的张量，本质上就是 <strong>7 个 token 各自的高维语义表示</strong>，后续会送入 BERT 的 Transformer 层进一步处理。</p><p>我们也可以看到位置编码：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>pos_ids </span><span>=</span><span> torch.</span><span>arange</span><span>(input_ids.shape[</span></span><span>1</span><span>])</span></div></div><div><div><div>2</div></div><div><span>pos_ids</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([0, 1, 2, 3, 4, 5, 6])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>代码解析：</p><ol>
<li><strong><code>input_ids.shape</code></strong> — 前面我们知道 <code>input_ids</code> 的形状是 <code>[1, 7]</code>（1 个 batch，7 个 token）</li>
<li><strong><code>input_ids.shape[1]</code></strong> — 取第 1 个维度（索引从 0 开始），值为 <strong>7</strong>（序列长度）</li>
<li><strong><code>torch.arange(7)</code></strong> — PyTorch 的 <code>arange</code> = “array range”，生成从 0 开始到 6 的整数序列</li>
</ol><p>所以输出就是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([0, 1, 2, 3, 4, 5, 6])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>这 7 个数字就是每个 token 在句子中的位置索引（Position IDs）。</strong></p>

<table><thead><tr><th>位置 ID</th><th>对应 token</th><th>位置含义</th></tr></thead><tbody><tr><td><strong>0</strong></td><td><code>[CLS]</code></td><td>句子的<strong>第 1 个</strong>位置</td></tr><tr><td><strong>1</strong></td><td>“this”</td><td>第 2 个位置</td></tr><tr><td><strong>2</strong></td><td>“is”</td><td>第 3 个位置</td></tr><tr><td><strong>3</strong></td><td>“a”</td><td>第 4 个位置</td></tr><tr><td><strong>4</strong></td><td>“test”</td><td>第 5 个位置</td></tr><tr><td><strong>5</strong></td><td>“sentence”</td><td>第 6 个位置</td></tr><tr><td><strong>6</strong></td><td><code>[SEP]</code></td><td>第 7 个位置</td></tr></tbody></table><p>回顾我们第一轮讲的内容：BERT 的 Embedding 由 <strong>三部分相加</strong> 组成：</p><p><span><span>最终嵌入=Token Embedding+Segment Embedding+Position Embedding\text{最终嵌入} = \text{Token Embedding} + \text{Segment Embedding} + \text{Position Embedding}</span><span><span><span></span><span><span>最终嵌入</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Token Embedding</span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>Segment Embedding</span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>Position Embedding</span></span></span></span></span></p><p>而 <strong>Position Embedding</strong> 就是通过这个 <code>pos_ids</code> <strong>去查另一张表</strong>得到的：</p><span><span><span>Position Embedding 表=512⏟最大长度×768⏟向量维度\text{Position Embedding 表} = \underbrace{512}_{\text{最大长度}} \times \underbrace{768}_{\text{向量维度}}</span><span><span><span></span><span><span>Position Embedding </span><span>表</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>最大长度</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>512</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>向量维度</span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span>768</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>

<table><thead><tr><th>pos_id</th><th>查表得到</th></tr></thead><tbody><tr><td>0</td><td>Position Embedding 表的<strong>第 0 行</strong>（768 维向量）</td></tr><tr><td>1</td><td>第 1 行</td></tr><tr><td>2</td><td>第 2 行</td></tr><tr><td>…</td><td>…</td></tr><tr><td>6</td><td>第 6 行</td></tr></tbody></table><p>这 7 个位置向量分别加到对应 token 的 Token Embedding 上，模型就知道每个词在句子中的先后顺序了。</p><p>总之，<strong><code>pos_ids</code> 就是 BERT 用来查 Position Embedding 表的”索引”</strong>，0 表示第一个 token，1 表示第二个，依此类推。因为 Transformer 本身没有顺序感，所以需要用位置编号来告诉模型词的排列顺序。</p></section></section></section>
<section><h1>2. Embedding 中各个 embeddings<a href="#2-embedding-中各个-embeddings"><span>#</span></a></h1><section><h2>2.1 token embedding<a href="#21-token-embedding"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>token_embeddings </span><span>=</span><span> model.embeddings.</span><span>word_embeddings</span><span>(input_ids)</span></span></div></div><div><div><div>2</div></div><div><span>token_embeddings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 1.3630e-02, -2.6490e-02, -2.3503e-02,  ...,  8.6805e-03,</span></div></div><div><div><div>2</div></div><div><span><span>           </span></span><span>7.1340e-03,  1.5147e-02],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-5.7095e-02,  1.5283e-02, -4.6868e-03,  ..., -3.2484e-03,</span></div></div><div><div><div>4</div></div><div><span><span>           </span></span><span>9.7317e-05,  9.4175e-03],</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[-3.6044e-02, -2.4606e-02, -2.5735e-02,  ...,  3.3691e-03,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>-1.8300e-03,  2.6855e-02],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>[ 2.3670e-02, -6.1351e-02, -2.9575e-02,  ..., -1.0239e-02,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>-7.2316e-03, -1.1745e-01],</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>[ 3.2079e-02,  6.3135e-03, -6.4352e-03,  ..., -1.1689e-03,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>-1.0810e-01, -8.9524e-02],</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>[-1.4521e-02, -9.9615e-03,  6.0263e-03,  ..., -2.5035e-02,</span></div></div><div><div><div>13</div></div><div><span><span>           </span></span><span>4.6379e-03, -1.5378e-03]]], grad_fn=&lt;EmbeddingBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>2.1.1 两段输出对比<a href="#211-两段输出对比"><span>#</span></a></h3><p>我们来对比一下 <code>model.embeddings(...)</code> 的输出和上面这段 <code>model.embeddings.word_embeddings(...)</code> 的输出。</p><p><strong>1. 代码上的区别</strong></p>

<table><thead><tr><th></th><th>输出 1（之前）</th><th>输出 2（现在）</th></tr></thead><tbody><tr><td><strong>调用的代码</strong></td><td><code>model.embeddings(...)</code></td><td><code>model.embeddings.word_embeddings(...)</code></td></tr><tr><td><strong>含义</strong></td><td><strong>完整 Embedding 层</strong></td><td><strong>仅 Token Embedding 查表</strong></td></tr></tbody></table><hr /><p><strong>2. 输出上的区别</strong></p>

<table><thead><tr><th>对比项</th><th>① 完整 Embedding</th><th>② 仅 Token Embedding</th></tr></thead><tbody><tr><td><strong>数值格式</strong></td><td>标准小数 <code>0.1686</code></td><td>科学记数法 <code>1.3630e-02</code></td></tr><tr><td><strong>数值大小</strong></td><td>约 <strong>-1.7 ~ 0.8</strong>（较大）</td><td>约 <strong>-0.12 ~ 0.03</strong>（很小）</td></tr><tr><td><strong><code>grad_fn</code></strong></td><td><code>&lt;NativeLayerNormBackward0&gt;</code></td><td><code>&lt;EmbeddingBackward0&gt;</code></td></tr></tbody></table><hr /><p><strong>3. 为什么会这样？</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                     </span></span><span>model.embeddings(...)</span></div></div><div><div><div>2</div></div><div><span><span>                     </span></span><span>┌──────────────────────────┐</span></div></div><div><div><div>3</div></div><div><span>input_ids ──────►  Token Embedding       ──┐    │</span></div></div><div><div><div>4</div></div><div><span><span>                   </span></span><span>(word_embeddings)       │    │</span></div></div><div><div><div>5</div></div><div><span><span>                   </span></span><span>👆 这是我们现在的输出    │ 相加│</span></div></div><div><div><div>6</div></div><div><span><span>                                          </span></span><span>├──►   │</span></div></div><div><div><div>7</div></div><div><span>token_type_ids ──► Segment Embedding    ──┘      │</span></div></div><div><div><div>8</div></div><div><span><span>                                                 </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span>pos_ids ─────────► Position Embedding   ──┐      │</span></div></div><div><div><div>10</div></div><div><span><span>                   </span></span><span>↑ 前面我们算的 pos_ids │ 相加 │</span></div></div><div><div><div>11</div></div><div><span><span>                                          </span></span><span>├──►   │</span></div></div><div><div><div>12</div></div><div><span><span>                                          </span></span><span>│      │</span></div></div><div><div><div>13</div></div><div><span><span>                                         </span></span><span>LayerNorm  ◄── 让数值范围变大到 -2 ~ 2</span></div></div><div><div><div>14</div></div><div><span><span>                                            </span></span><span>│</span></div></div><div><div><div>15</div></div><div><span><span>                                         </span></span><span>Dropout</span></div></div><div><div><div>16</div></div><div><span><span>                                            </span></span><span>│</span></div></div><div><div><div>17</div></div><div><span><span>                                            </span></span><span>▼</span></div></div><div><div><div>18</div></div><div><span><span>                                    </span></span><span>👆 这是之前的输出</span></div></div><div><div><div>19</div></div><div><span><span>                    </span></span><span>└──────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>2.1.2 关键点<a href="#212-关键点"><span>#</span></a></h3><p><strong>① Token Embedding 的输出（我们现在看到的）：</strong></p><ul>
<li>只是简单的<strong>查表</strong>，从 <code>[30522 × 768]</code> 的矩阵中取出对应行</li>
<li>这些初始嵌入的数值范围很小（均值 0 附近），所以显示为科学记数法 <code>1.36e-02 = 0.0136</code></li>
<li><code>grad_fn=&lt;EmbeddingBackward0&gt;</code> — 表示这个张量来自<strong>查表操作</strong></li>
</ul><p><strong>② 完整 Embedding 的输出（之前看到的）：</strong></p><ul>
<li>= Token Embedding + Segment Embedding + Position Embedding，然后通过 <strong>LayerNorm</strong></li>
<li>LayerNorm 会对数据做<strong>标准化</strong>：减去均值、除以标准差，再乘以缩放参数</li>
<li>标准化后数值范围被拉大到 <strong>-2 ~ 2</strong> 左右，所以显示为普通小数</li>
<li><code>grad_fn=&lt;NativeLayerNormBackward0&gt;</code> — 表示这个张量来自<strong>LayerNorm 操作</strong></li>
</ul><hr /></section><section><h3>2.1.3 一句话总结<a href="#213-一句话总结"><span>#</span></a></h3><p>我们现在看到的（②）是 <strong>“原材料”</strong>——每个 token 从词表中查出来的原始向量，数值小且分散；之前看到的（①）是 <strong>“加工后”</strong> 的成品——三个嵌入相加后再经过 LayerNorm 归一化，数值范围更大更规整，这才是真正喂给 BERT Transformer 层的输入。</p></section></section><section><h2>2.2 segment type embedding<a href="#22-segment-type-embedding"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>segment_embeddings </span><span>=</span><span> model.embeddings.</span><span>token_type_embeddings</span><span>(token_type_ids)</span></span></div></div><div><div><div>2</div></div><div><span>segment_embeddings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.0004,  0.0110,  0.0037,  ..., -0.0066, -0.0034, -0.0086],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[ 0.0004,  0.0110,  0.0037,  ..., -0.0066, -0.0034, -0.0086],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[ 0.0004,  0.0110,  0.0037,  ..., -0.0066, -0.0034, -0.0086],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.0004,  0.0110,  0.0037,  ..., -0.0066, -0.0034, -0.0086],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.0004,  0.0110,  0.0037,  ..., -0.0066, -0.0034, -0.0086],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[ 0.0004,  0.0110,  0.0037,  ..., -0.0066, -0.0034, -0.0086]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;EmbeddingBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关于上面的输出，我们可以观察到，全部 7 行都一模一样。</p><p>这是因为在之前 <code>tokenizer</code> 的输出中：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0]])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>所有 token 的 segment 标记都是 0</strong>，因为我们的句子 “This is a test sentence” 只有一个句子（没有句子 B）.</p><section><h3>2.2.1 查表过程<a href="#221-查表过程"><span>#</span></a></h3><p>Segment Embedding 表的大小是 <strong><code>2 × 768</code></strong>（只有两行）：</p>

<table><thead><tr><th>segment 编号</th><th>含义</th><th>嵌入向量</th></tr></thead><tbody><tr><td><strong>0</strong></td><td>句子 A（第一个句子）</td><td><code>[0.0004, 0.0110, 0.0037, ..., -0.0066, -0.0034, -0.0086]</code></td></tr><tr><td><strong>1</strong></td><td>句子 B（第二个句子）</td><td>另一个不同的 768 维向量</td></tr></tbody></table><p>因为所有 <code>token_type_ids</code> 都是 0，所以每个 token 查到的都是<strong>第 0 行</strong>，7 个结果自然完全一样。</p><hr /></section><section><h3>2.2.2 示意图<a href="#222-示意图"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>token_type_ids: [0, 0, 0, 0, 0, 0, 0]</span></div></div><div><div><div>2</div></div><div><span><span>                 </span></span><span>│  │  │  │  │  │  │</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>┌───────────┘  │  │  │  │  │  └───────────┐</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>│    ┌─────────┘  │  │  │  └─────────┐    │</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>│    │    ┌───────┘  │  └───────┐    │    │</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>▼    ▼    ▼          ▼          ▼    ▼    ▼</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>┌────────────────────────────────────────────┐</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>│   Segment Embedding Table (2 × 768)        │</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>│   ┌─────────────────────────────┐          │</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>│   │ 行 0: 句子A的向量 ← 全部查到这一行    │</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>│   │ 行 1: 句子B的向量                      │</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>│   └─────────────────────────────┘          │</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>└────────────────────────────────────────────┘</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>│    │    │          │          │    │    │</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>▼    ▼    ▼          ▼          ▼    ▼    ▼</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>全部是相同的 768 维向量</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h3>2.2.3 那什么时候会用上 segment 1？<a href="#223-那什么时候会用上-segment-1"><span>#</span></a></h3><p>当处理<strong>句子对</strong>任务时，比如 QA（问答）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer</span><span>(</span></span><span>"What is your name?"</span><span>, </span><span>"My name is BERT."</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这时 <code>token_type_ids</code> 就会是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>↑ 第一句        ↑ 第二句</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前 7 个 token（第一句）查到<strong>行 0</strong>，后 5 个 token（第二句）查到<strong>行 1</strong>，两句的 segment embedding 就会不同，BERT 就能区分”哪个词属于哪个句子”了。</p></section></section><section><h2>2.3 position embedding<a href="#23-position-embedding"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>pos_embeddings </span><span>=</span><span> model.embeddings.</span><span>position_embeddings</span><span>(pos_ids)</span></span></div></div><div><div><div>2</div></div><div><span>pos_embeddings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[ 1.7505e-02, -2.5631e-02, -3.6642e-02,  ...,  3.3437e-05,</span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>6.8312e-04,  1.5441e-02],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[ 7.7580e-03,  2.2613e-03, -1.9444e-02,  ...,  2.8910e-02,</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>2.9753e-02, -5.3247e-03],</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[-1.1287e-02, -1.9644e-03, -1.1573e-02,  ...,  1.4908e-02,</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>1.8741e-02, -7.3140e-03],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>...,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[-5.6087e-03, -1.0445e-02, -7.2288e-03,  ...,  2.0837e-02,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>3.5402e-03,  4.7708e-03],</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>[-3.0871e-03, -1.8956e-02, -1.8930e-02,  ...,  7.4045e-03,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>2.0183e-02,  3.4077e-03],</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>[ 6.4257e-03, -1.7664e-02, -2.2067e-02,  ...,  6.7531e-04,</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>1.1108e-02,  3.7521e-03]], grad_fn=&lt;EmbeddingBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>2.3.1 七行各不相同<a href="#231-七行各不相同"><span>#</span></a></h3>

<table><thead><tr><th>位置 ID</th><th>768 维向量（部分）</th><th>是否与其它行相同？</th></tr></thead><tbody><tr><td><strong>0</strong></td><td><code>[ 1.7505e-02, -2.5631e-02, -3.6642e-02, ...]</code></td><td>❌ <strong>唯一</strong></td></tr><tr><td><strong>1</strong></td><td><code>[ 7.7580e-03,  2.2613e-03, -1.9444e-02, ...]</code></td><td>❌ <strong>唯一</strong></td></tr><tr><td><strong>2</strong></td><td><code>[-1.1287e-02, -1.9644e-03, -1.1573e-02, ...]</code></td><td>❌ <strong>唯一</strong></td></tr><tr><td><strong>3</strong></td><td>…</td><td>❌ <strong>唯一</strong></td></tr><tr><td><strong>4</strong></td><td>…</td><td>❌ <strong>唯一</strong></td></tr><tr><td><strong>5</strong></td><td><code>[-3.0871e-03, -1.8956e-02, -1.8930e-02, ...]</code></td><td>❌ <strong>唯一</strong></td></tr><tr><td><strong>6</strong></td><td><code>[ 6.4257e-03, -1.7664e-02, -2.2067e-02, ...]</code></td><td>❌ <strong>唯一</strong></td></tr></tbody></table><p><strong>每个位置的向量都是独一无二的</strong>，这样才能让模型区分”第 0 个位置”和”第 1 个位置”。</p><hr /></section><section><h3>2.3.2 查表过程<a href="#232-查表过程"><span>#</span></a></h3><p>Position Embedding 表的大小是 <strong><code>512 × 768</code></strong>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pos_ids: [0, 1, 2, 3, 4, 5, 6]</span></div></div><div><div><div>2</div></div><div><span><span>           </span></span><span>│  │  │  │  │  │  │</span></div></div><div><div><div>3</div></div><div><span><span>           </span></span><span>▼  ▼  ▼  ▼  ▼  ▼  ▼</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>┌────────────────────────────────┐</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>│ Position Embedding Table       │</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>│   (512 × 768)                  │</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>│                                │</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>│ 行 0:  ──── 查 → 第1行输出     │</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>│ 行 1:  ──── 查 → 第2行输出     │</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>│ 行 2:  ──── 查 → 第3行输出     │</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>│  ...                           │</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>│ 行 6:  ──── 查 → 第7行输出     │</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>│  ...                           │</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>│ 行 511:                        │</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>└────────────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h3>2.3.3 与前面几个输出的对比总结<a href="#233-与前面几个输出的对比总结"><span>#</span></a></h3><p>现在我们已经看完了 BERT Embedding 的三张表：</p>

<table><thead><tr><th>嵌入类型</th><th>代码</th><th>输出特点</th></tr></thead><tbody><tr><td><strong>Token Embedding</strong></td><td><code>model.embeddings.word_embeddings(input_ids)</code></td><td>7 行各不相同（不同词 → 不同向量）</td></tr><tr><td><strong>Segment Embedding</strong></td><td><code>model.embeddings.token_type_embeddings(token_type_ids)</code></td><td>7 行全部相同（都是句子 A）</td></tr><tr><td><strong>Position Embedding</strong></td><td><code>model.embeddings.position_embeddings(pos_ids)</code></td><td>7 行各不相同（不同位置 → 不同向量）</td></tr></tbody></table><p>最终，这三者<strong>逐元素相加</strong>，再经过 LayerNorm 和 Dropout，就得到了我们第一次看到的完整 Embedding 输出 🎯</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>最终输出 = Token向量 + Segment向量 + Position向量 → LayerNorm → Dropout</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h2>2.4 input embedding<a href="#24-input-embedding"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>print</span><span>(token_embeddings.shape)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(segment_embeddings.shape)</span></div></div><div><div><div>3</div></div><div><span>print</span><span>(pos_embeddings.shape)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>input_embeddings </span><span>=</span><span> token_embeddings </span><span>+</span><span> segment_embeddings </span><span>+</span><span> pos_embeddings</span></span></div></div><div><div><div>2</div></div><div><span>input_embeddings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>torch.Size([1, 7, 768])</span></div></div><div><div><div>2</div></div><div><span>torch.Size([1, 7, 768])</span></div></div><div><div><div>3</div></div><div><span>torch.Size([7, 768])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>tensor([[[ 0.0316, -0.0411, -0.0564,  ...,  0.0021,  0.0044,  0.0219],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[-0.0489,  0.0285, -0.0204,  ...,  0.0190,  0.0265, -0.0045],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.0469, -0.0156, -0.0336,  ...,  0.0117,  0.0135,  0.0109],</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>[ 0.0185, -0.0608, -0.0331,  ...,  0.0040, -0.0071, -0.1213],</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>[ 0.0294, -0.0017, -0.0217,  ..., -0.0004, -0.0913, -0.0948],</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>[-0.0077, -0.0166, -0.0123,  ..., -0.0310,  0.0124, -0.0064]]],</span></div></div><div><div><div>12</div></div><div><span><span>       </span></span><span>grad_fn=&lt;AddBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们可以对比一下之前的 <code>model.embedding()</code> ，来看一下两者在流程中的位置：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>          </span></span><span>token_embeddings   segment_embeddings   pos_embeddings</span></div></div><div><div><div>2</div></div><div><span><span>               </span></span><span>│                   │                   │</span></div></div><div><div><div>3</div></div><div><span><span>               </span></span><span>└───────┬───────────┴───────┬───────────┘</span></div></div><div><div><div>4</div></div><div><span><span>                       </span></span><span>│                   │</span></div></div><div><div><div>5</div></div><div><span><span>                       </span></span><span>▼                   ▼</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>input_embeddings = 三者相加         ← 我们现在看到的</span></div></div><div><div><div>7</div></div><div><span><span>             </span></span><span>(grad_fn=&lt;AddBackward0&gt;)</span></div></div><div><div><div>8</div></div><div><span><span>                       </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span><span>                       </span></span><span>▼</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>LayerNorm  ◄── 这一步会把数值分布重新调整！</span></div></div><div><div><div>11</div></div><div><span><span>                       </span></span><span>│</span></div></div><div><div><div>12</div></div><div><span><span>                      </span></span><span>Dropout</span></div></div><div><div><div>13</div></div><div><span><span>                       </span></span><span>│</span></div></div><div><div><div>14</div></div><div><span><span>                       </span></span><span>▼</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>model.embeddings(...) 的返回值    ← 之前看到的</span></div></div><div><div><div>16</div></div><div><span><span>             </span></span><span>(grad_fn=&lt;NativeLayerNormBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>2.4.1 数值上的直接对比<a href="#241-数值上的直接对比"><span>#</span></a></h3><p>以第 1 个 token（<code>[CLS]</code>）的前几个维度为例：</p>

<table><thead><tr><th>维度</th><th>三者相加（input_embeddings）</th><th>model.embeddings(…)</th></tr></thead><tbody><tr><td>dim 0</td><td><strong>0.0316</strong></td><td><strong>0.1686</strong></td></tr><tr><td>dim 1</td><td><strong>-0.0411</strong></td><td><strong>-0.2858</strong></td></tr><tr><td>dim 2</td><td><strong>-0.0564</strong></td><td><strong>-0.3261</strong></td></tr></tbody></table><p>数值完全不同，就是因为 <strong>LayerNorm</strong> 在中间起了作用：</p><p><span><span>LayerNorm(x)=γ⋅x−μσ+β\text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{\sigma} + \beta</span><span><span><span></span><span><span>LayerNorm</span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>γ</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>x</span><span>−</span><span>μ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>β</span></span></span></span></p><p>它会：</p><ol>
<li><strong>减去均值 <span><span>μ\mu</span><span><span><span></span><span>μ</span></span></span></span></strong> — 对每个 token 的 768 个维度求均值然后减去</li>
<li><strong>除以标准差 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span></strong> — 把数值缩放到单位方差附近</li>
<li><strong>乘以可学习参数 <span><span>γ\gamma</span><span><span><span></span><span>γ</span></span></span></span> 再加 <span><span>β\beta</span><span><span><span></span><span>β</span></span></span></span></strong> — 缩放和平移</li>
</ol><p>所以即使输入是 <code>0.0316</code>，经过 LayerNorm 后可能会变成 <code>0.1686</code>（数值被拉大了）。</p></section><section><h3>2.4.2 另外还有一个细节<a href="#242-另外还有一个细节"><span>#</span></a></h3><p>注意打印出来的形状：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>torch.Size([1, 7, 768])    ← token_embeddings</span></div></div><div><div><div>2</div></div><div><span>torch.Size([1, 7, 768])    ← segment_embeddings</span></div></div><div><div><div>3</div></div><div><span>torch.Size([7, 768])       ← pos_embeddings（少了一个维度！）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pos_embeddings</code> 的形状是 <code>[7, 768]</code> 而不是 <code>[1, 7, 768]</code>，<strong>相加时 PyTorch 会自动广播（broadcast）</strong>，给它补上一个 batch 维度，这也是通过代码手动相加与 <code>model.embeddings()</code> 内部处理的一个小差异。</p></section><section><h3>2.4.3 总结<a href="#243-总结"><span>#</span></a></h3>

<table><thead><tr><th></th><th><code>input_embeddings</code>（手动相加）</th><th><code>model.embeddings(...)</code></th></tr></thead><tbody><tr><td>做了哪些操作</td><td>仅相加 ✅</td><td>相加 ✅ → <strong>LayerNorm ✅ → Dropout ✅</strong></td></tr><tr><td><code>grad_fn</code></td><td><code>&lt;AddBackward0&gt;</code></td><td><code>&lt;NativeLayerNormBackward0&gt;</code></td></tr><tr><td>数值特点</td><td>原始值较小、未归一化</td><td>经过标准化，范围更大</td></tr></tbody></table><p>所以，我们手动算的 <code>input_embeddings</code> 只是 <strong>半成品</strong>，<code>model.embeddings()</code> 返回的才是经过 LayerNorm 和 Dropout 后的 <strong>成品</strong> 🎯</p></section></section></section>
<section><h1>3. 后处理<a href="#3-后处理"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>embeddings </span><span>=</span><span> model.embeddings.</span><span>LayerNorm</span><span>(input_embeddings)</span></span></div></div><div><div><div>2</div></div><div><span><span>embeddings </span><span>=</span><span> model.embeddings.</span><span>dropout</span><span>(embeddings)</span></span></div></div><div><div><div>3</div></div><div><span>embeddings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tensor([[[ 0.1686, -0.2858, -0.3261,  ..., -0.0276,  0.0383,  0.1640],</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>[-0.6485,  0.6739, -0.0932,  ...,  0.4475,  0.6696,  0.1820],</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>[-0.6270, -0.0633, -0.3143,  ...,  0.3427,  0.4636,  0.4594],</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>...,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>[ 0.6010, -0.6970, -0.2001,  ...,  0.2960,  0.2060, -1.7181],</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>[ 0.8323,  0.2878,  0.0021,  ...,  0.2628, -1.1310, -1.2708],</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>[-0.1481, -0.2948, -0.1690,  ..., -0.5009,  0.2544, -0.0700]]],</span></div></div><div><div><div>8</div></div><div><span><span>       </span></span><span>grad_fn=&lt;NativeLayerNormBackward0&gt;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可见，与一开始 <code>model.embeddings()</code> 的输出是一致的。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-04/</id>
      <title type="text">04_torch_no_grad_vs_require_grad=False</title>
      <published>2026-06-09T00:00:00.000Z</published>
      <updated>2026-06-16T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-04/"/>
      <summary type="text">动手写 BERT 系列笔记第四节，特别篇</summary>
      <content type="html"><![CDATA[<p>本节主要探索这样一个问题：<strong>Pytorch 中，<code>torch.no_grad()</code> 与 <code>param.requires_grad==False</code> 有什么区别</strong>？</p>
<p><a href="https://stackoverflow.com/questions/63785319/pytorch-torch-no-grad-versus-requires-grad-false" target="_blank">原问题来源 Stack Overflow</a>：</p>
<hr />
<p>I’m following a <a href="https://github.com/bentrevett/pytorch-sentiment-analysis/blob/master/6%20-%20Transformers%20for%20Sentiment%20Analysis.ipynb" target="_blank">PyTorch tutorial</a> which uses the BERT NLP model (feature extractor) from the Huggingface Transformers library. There are two pieces of interrelated code for gradient updates that I don’t understand.</p>
<p>(1) <code>torch.no_grad()</code></p>
<p>The tutorial has a class where the <code>forward()</code> function creates a <code>torch.no_grad()</code> block around a call to the BERT feature extractor, like this:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>bert </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(</span></span><span>'bert-base-uncased'</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>class</span><span><span> </span><span>BERTGRUSentiment</span><span>(</span><span>nn</span><span>.</span><span>Module</span><span>)</span></span><span>:</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>bert</span></span><span>):</span></div></div><div><div><div>6</div></div><div><span>        </span><span>super</span><span>().</span><span>__init__</span><span>()</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.bert </span><span>=</span><span> bert</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>def</span><span> </span><span>forward</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>text</span></span><span>):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>embedded </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>bert</span><span>(text)[</span></span><span>0</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>(2) <code>param.requires_grad = False</code></p>
<p>There is another portion in the same tutorial where the BERT parameters are frozen.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> name, param </span><span>in</span><span><span> model.</span><span>named_parameters</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span><span> name.</span><span>startswith</span><span>(</span></span><span>'bert'</span><span>):</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>param.requires_grad </span><span>=</span><span> </span><span>False</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p><strong>When would I need (1) and/or (2)?</strong></p>
<ul>
<li>If I want to train with a frozen BERT, would I need to enable both?</li>
<li>If I want to train to let BERT be updated, would I need to disable both?</li>
</ul>
<p>Additionaly, I ran all four combinations and found:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with</span><span><span> torch.no_grad   requires_grad </span><span>=</span><span> </span></span><span>False</span><span>  Parameters  Ran</span></div></div><div><div><div>2</div></div><div><span>   </span><span>------------------</span><span>   </span><span>--------------------</span><span><span>-</span><span>  </span></span><span>----------</span><span>  </span><span>--</span><span>-</span></div></div><div><div><div>3</div></div><div><span>a. Yes                  Yes                      </span><span>3M</span><span>        Successfully</span></div></div><div><div><div>4</div></div><div><span>b. Yes                  No                     </span><span>112M</span><span>        Successfully</span></div></div><div><div><div>5</div></div><div><span>c. No                   Yes                      </span><span>3M</span><span>        Successfully</span></div></div><div><div><div>6</div></div><div><span>d. No                   No                     </span><span>112M</span><span><span>        </span><span>CUDA</span><span> out of memory</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p><strong>Can someone please explain what’s going on?</strong> Why am I getting <code>CUDA out of memory</code> for (d) but not (b)? Both have 112M learnable parameters.</p>
<hr />
<p>这是一个非常经典的问题。那四个组合的实验结果本身就很好地说明了区别，我们来逐一拆解：</p>
<hr />
<section><h2>1.1 本质区别<a href="#11-本质区别"><span>#</span></a></h2>

<table><thead><tr><th>概念</th><th>作用范围</th><th>本质</th></tr></thead><tbody><tr><td><code>param.requires_grad = False</code></td><td><strong>单个参数/张量</strong></td><td>告诉 autograd：“这个参数我不需要梯度”</td></tr><tr><td><code>torch.no_grad()</code></td><td><strong>整个代码块</strong></td><td>告诉 autograd：“这段代码里<strong>所有操作</strong>都不建计算图”</td></tr></tbody></table><hr /></section>
<section><h2>1.2 详细解释<a href="#12-详细解释"><span>#</span></a></h2><section><h3>1.2.1 <code>requires_grad = False</code> —— 参数级别的开关<a href="#121-requires_grad--false--参数级别的开关"><span>#</span></a></h3><p>这是<strong>某个张量的属性</strong>，表示 PyTorch 不需要为该张量计算梯度。</p><ul>
<li>✅ 该参数本身的梯度不会被计算（<code>param.grad</code> 为 <code>None</code>）</li>
<li>✅ 优化器（SGD、Adam 等）不会更新这个参数</li>
<li>❌ <strong>但经过该参数的计算仍然会构建计算图</strong>，只要下游还有需要梯度的张量</li>
<li>❌ <strong>仍然会消耗内存</strong>存储中间激活值，以备反传使用</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>x </span><span>=</span><span> torch.</span><span>randn</span><span>(</span></span><span>3</span><span>, </span><span>requires_grad</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>w </span><span>=</span><span> torch.</span><span>randn</span><span>(</span></span><span>3</span><span>, </span><span>requires_grad</span><span>=</span><span>False</span><span>)  </span><span># 这个参数不需要梯度</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>y </span><span>=</span><span> (w </span><span>*</span><span> x).</span><span>sum</span><span>()  </span></span><span># 计算图仍然会构建！</span></div></div><div><div><div>5</div></div><div><span><span>y.</span><span>backward</span><span>()       </span></span><span># x.grad 被计算，w.grad 仍为 None</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2.2 <code>torch.no_grad()</code> —— 全局上下文管理器<a href="#122-torchno_grad--全局上下文管理器"><span>#</span></a></h3><p>进入这个上下文后，<strong>所有操作都不再追踪梯度</strong>，完全不构建计算图。</p><ul>
<li>✅ <strong>彻底关闭计算图构建</strong>，不存储任何中间激活值</li>
<li>✅ 大幅<strong>节省 GPU 显存</strong>（backward 需要的内存 ≈ forward 的 2~3 倍）</li>
<li>✅ 无论张量的 <code>requires_grad</code> 是什么，都不计算梯度</li>
<li>❌ 如果你在 <code>no_grad()</code> 里修改了参数，优化器后续更新时也不会感知这些修改</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>x </span><span>=</span><span> torch.</span><span>randn</span><span>(</span></span><span>3</span><span>, </span><span>requires_grad</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>w </span><span>=</span><span> torch.</span><span>randn</span><span>(</span></span><span>3</span><span>, </span><span>requires_grad</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> (w </span><span>*</span><span> x).</span><span>sum</span><span>()  </span><span># 完全不建计算图！</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># y.backward()  ❌ 会报错，因为 y 没有 grad_fn</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section></section>
<section><h2>1.3 为什么组合 (d) 会 OOM，但 (b) 不会？<a href="#13-为什么组合-d-会-oom但-b-不会"><span>#</span></a></h2><p>回到原帖主提到的四个组合：</p>

<table><thead><tr><th>组合</th><th><code>no_grad</code></th><th><code>requires_grad</code></th><th>参数量</th><th>结果</th></tr></thead><tbody><tr><td>a</td><td>✅</td><td>✅ (False)</td><td>3M</td><td>✅</td></tr><tr><td>b</td><td>✅</td><td>❌ (True)</td><td>112M</td><td>✅</td></tr><tr><td>c</td><td>❌</td><td>✅ (False)</td><td>3M</td><td>✅</td></tr><tr><td>d</td><td>❌</td><td>❌ (True)</td><td>112M</td><td>❌ OOM</td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>╔══════════════════════════════════════════════════════════════╗</span></div></div><div><div><div>2</div></div><div><span>║  (b)  torch.no_grad() 激活                                   ║</span></div></div><div><div><div>3</div></div><div><span>║  ┌─────────────────────────────────────────────────────────┐ ║</span></div></div><div><div><div>4</div></div><div><span>║  │  BERT 前向传播 → 输出嵌入                                │ ║</span></div></div><div><div><div>5</div></div><div><span>║  │  (不建计算图，不存中间激活，省显存)                         │ ║</span></div></div><div><div><div>6</div></div><div><span>║  └─────────────────────────────────────────────────────────┘ ║</span></div></div><div><div><div>7</div></div><div><span>║  GRU + 分类头 → 计算图（只有 3M 参数需反传）                   ║</span></div></div><div><div><div>8</div></div><div><span>║  总显存 ≈ BERT 推理 + 3M 参数训练 ≈ 低                       ║</span></div></div><div><div><div>9</div></div><div><span>╚══════════════════════════════════════════════════════════════╝</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>╔══════════════════════════════════════════════════════════════╗</span></div></div><div><div><div>12</div></div><div><span>║  (d)  没有 torch.no_grad()                                   ║</span></div></div><div><div><div>13</div></div><div><span>║  ┌─────────────────────────────────────────────────────────┐ ║</span></div></div><div><div><div>14</div></div><div><span>║  │  BERT 前向传播                                           │ ║</span></div></div><div><div><div>15</div></div><div><span>║  │  (建计算图！12层 Transformer 的所有中间激活全部存入显存)     │ ║</span></div></div><div><div><div>16</div></div><div><span>║  │  需要保留：每层 Q/K/V/注意力分数/FFN 中间值...             │ ║</span></div></div><div><div><div>17</div></div><div><span>║  └─────────────────────────────────────────────────────────┘ ║</span></div></div><div><div><div>18</div></div><div><span>║  总显存 ≈ BERT 训练（正向 + 反向中间值）≈ 12GB+ → OOM         ║</span></div></div><div><div><div>19</div></div><div><span>╚══════════════════════════════════════════════════════════════╝</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>关键点</strong>：<code>requires_grad=True</code> 只是说”参数需要梯度”，但<strong>光靠它本身并不会让显存爆炸</strong>。真正让显存爆炸的是<strong>没有 <code>no_grad()</code> 时，PyTorch 会为 BERT 的每一层都保存中间激活值以便反传</strong>——12 层 Transformer 的中间结果是非常吃显存的。</p><hr /></section>
<section><h2>1.4 实际场景总结<a href="#14-实际场景总结"><span>#</span></a></h2>

<table><thead><tr><th>场景</th><th>需要 <code>no_grad</code>？</th><th>需要 <code>requires_grad=False</code>？</th><th>原因</th></tr></thead><tbody><tr><td><strong>冻结 BERT 做特征提取</strong></td><td>✅ 建议用</td><td>✅ 建议用</td><td><code>no_grad</code> 省显存，<code>requires_grad=False</code> 防止意外更新 + 优化器跳过</td></tr><tr><td><strong>微调 BERT</strong></td><td>❌ 不能用</td><td>❌ 不能用</td><td>需要计算图和梯度来更新 BERT</td></tr><tr><td><strong>仅推理/评估</strong></td><td>✅ 必须用</td><td>❌ 不需要</td><td><code>no_grad</code> 省显存提速，参数是否可训练无关紧要</td></tr></tbody></table><blockquote><p><strong>结论</strong>：<code>requires_grad=False</code> 控制<strong>是否更新参数</strong>（优化器角度），<code>torch.no_grad()</code> 控制<strong>是否建计算图</strong>（显存/速度角度）。在冻结 BERT 时，两者都用是最佳实践——就像教程中做的那样。</p></blockquote></section>
<section><h1>2. 什么是计算图<a href="#2-什么是计算图"><span>#</span></a></h1><p>计算图（Computation Graph）是 PyTorch 等深度学习框架的核心概念。它是 <strong>autograd（自动求导）的底层数据结构</strong>。</p><hr /><section><h2>2.1 直观理解<a href="#21-直观理解"><span>#</span></a></h2><p>计算图就是一个<strong>有向无环图（DAG）</strong>，记录了你的每一步数学运算：</p><ul>
<li><strong>节点（Node）</strong> = 张量（数据）或运算（函数）</li>
<li><strong>边（Edge）</strong> = 数据流向</li>
</ul><p>当执行 <code>z = (a + b) × c</code> 时，PyTorch 在背后偷偷建了这样一张图：</p><div><div><div><div><span><p>前向传播（从左到右建图）</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>a<br /><br />requires_grad=True</p></span></div><div><span><p>➕ Add<br /><br />grad_fn=</p></span></div><div><span><p>b<br /><br />requires_grad=True</p></span></div><div><span><p>✖️ Mul<br /><br />grad_fn=</p></span></div><div><span><p>c<br /><br />requires_grad=True</p></span></div><div><span><p>z = (a+b)×c</p></span></div>
</div><div><div><span><p>前向传播（从左到右建图）</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>a<br /><br />requires_grad=True</p></span></div><div><span><p>➕ Add<br /><br />grad_fn=</p></span></div><div><span><p>b<br /><br />requires_grad=True</p></span></div><div><span><p>✖️ Mul<br /><br />grad_fn=</p></span></div><div><span><p>c<br /><br />requires_grad=True</p></span></div><div><span><p>z = (a+b)×c</p></span></div>
</div></div></div><ul>
<li>每个运算都会创建一个 <strong><code>grad_fn</code></strong>（梯度函数），记录如何计算该运算的偏导</li>
<li>这张图就是 <strong>链式法则</strong> 的地图 —— 反向传播时 PyTorch 按图索骥，从 <code>z</code> 一路往回走，依次调用每个 <code>grad_fn</code> 算出梯度</li>
</ul><hr /></section><section><h2>2.2 用一个具体例子演示<a href="#22-用一个具体例子演示"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>a </span><span>=</span><span> torch.</span><span>tensor</span><span>([</span></span><span>2.0</span><span>], </span><span>requires_grad</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>b </span><span>=</span><span> torch.</span><span>tensor</span><span>([</span></span><span>3.0</span><span>], </span><span>requires_grad</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>c </span><span>=</span><span> torch.</span><span>tensor</span><span>([</span></span><span>5.0</span><span>], </span><span>requires_grad</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 前向传播：PyTorch 自动构建计算图</span></div></div><div><div><div>8</div></div><div><span><span>t </span><span>=</span><span> a </span><span>+</span><span> b          </span></span><span># t = 2 + 3 = 5</span></div></div><div><div><div>9</div></div><div><span><span>z </span><span>=</span><span> t </span><span>*</span><span> c          </span></span><span># z = 5 * 5 = 25</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># 查看 grad_fn，看到计算图的"足迹"</span></div></div><div><div><div>12</div></div><div><span>print</span><span>(t.grad_fn)   </span><span># &lt;AddBackward0 object&gt;</span></div></div><div><div><div>13</div></div><div><span>print</span><span>(z.grad_fn)   </span><span># &lt;MulBackward0 object&gt;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span># 反向传播：沿着计算图走，计算每个参数的梯度</span></div></div><div><div><div>16</div></div><div><span><span>z.</span><span>backward</span><span>()</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>print</span><span>(a.grad)      </span><span># ∂z/∂a = c = 5.0</span></div></div><div><div><div>19</div></div><div><span>print</span><span>(b.grad)      </span><span># ∂z/∂b = c = 5.0</span></div></div><div><div><div>20</div></div><div><span>print</span><span>(c.grad)      </span><span># ∂z/∂c = a + b = 5.0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>2.2.1 反向传播的路线图<a href="#221-反向传播的路线图"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>z.backward() 开始</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>MulBackward0: 计算 ∂z/∂t = c, ∂z/∂c = t</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>AddBackward0: 计算 ∂z/∂a = ∂z/∂t × ∂t/∂a = c × 1 = 5</span></div></div><div><div><div>6</div></div><div><span><span>              </span></span><span>计算 ∂z/∂b = ∂z/∂t × ∂t/∂b = c × 1 = 5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section></section><section><h2>2.3 计算图与内存<a href="#23-计算图与内存"><span>#</span></a></h2><p>这就是为什么之前说「建计算图很吃显存」—— 每个 <code>grad_fn</code> 必须记住它前向时的<strong>输入值</strong>（即中间激活），才能在反向时计算出梯度。</p><p>回到 BERT 的例子：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BERT 一层 Transformer 的计算图（简化）：</span></div></div><div><div><div>2</div></div><div><span>输入 → QKV 线性变换 → 注意力分数 → Softmax → 加权求和 → FFN → 输出</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>↓            ↓           ↓         ↓         ↓     ↓</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>存了 QKV    存了分数    存了概率    存了加权    存了中间  存了输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>12 层 × 每层多个中间值 × batch_size × 序列长度 × 768 维 = <strong>天文数字的显存</strong>。</p><p>而 <code>torch.no_grad()</code> 就是告诉 PyTorch：「这段代码我不需要反向传播，<strong>别建图，别存中间值</strong>」—— 于是前向跑完直接丢弃中间结果，显存瞬间降下来。</p><hr /></section><section><h2>2.4 一句话总结<a href="#24-一句话总结"><span>#</span></a></h2><blockquote><p><strong>计算图就是 PyTorch 的「记忆账本」—— 前向时一笔笔记录每步运算和中间结果，反向时按图索骥算出所有梯度。<code>torch.no_grad()</code> 就是告诉 PyTorch「这笔账不用记」，从而省下记帐的内存。</strong></p></blockquote></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-03/</id>
      <title type="text">03_bert_model_architecture_params</title>
      <published>2026-06-08T00:00:00.000Z</published>
      <updated>2026-06-15T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-03/"/>
      <summary type="text">动手写 BERT 系列笔记第三节</summary>
      <content type="html"><![CDATA[<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertModel, BertForSequenceClassification</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>model </span><span>=</span><span> BertModel.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>3</div></div><div><span><span>cls_model </span><span>=</span><span> BertForSequenceClassification.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>输出为：</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>23</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>27</div></div><div><span><span>          </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>(output): BertOutput(</span></div></div><div><div><div>30</div></div><div><span><span>          </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>(activation): Tanh()</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<section><h2>1.1 <code>BertModel</code> 的架构<a href="#11-bertmodel-的架构"><span>#</span></a></h2><p>当你在 HuggingFace Transformers 中打印一个 <code>BertModel</code> 对象时，它会递归显示模型的所有模块层（modules/layers），就像一个<strong>层层嵌套的目录树</strong>。每个缩进代表一层封装，每个 <code>()</code> 里的内容就是一个 PyTorch 的 <code>nn.Module</code> 子模块。</p><p>简单来说，这就是在展示 BERT 模型的「骨架图」—— 从最外层到最内层的每一个神经网络层。</p><p>BERT-base 由 3 大块组成：</p><ol>
<li><strong>Embeddings（嵌入层）</strong>—— 把文本转成向量：把输入的 token ID 转换成 768 维的稠密向量，并加上位置信息。</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(word_embeddings): Embedding(30522, 768)       # 词表大小 30522 → 768 维向量</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(position_embeddings): Embedding(512, 768)      # 位置编码，最多 512 个位置</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(token_type_embeddings): Embedding(2, 768)      # 句子类型（两句任务用）</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>(LayerNorm): LayerNorm(768)                      # 层归一化</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>(dropout): Dropout(p=0.1)                        # 随机丢弃 10% 防止过拟合</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li><strong>Encoder（编码器）</strong>—— 核心 Transformer 层</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(encoder): BertEncoder(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(0-11): 12 x BertLayer( ... )    # 12 层完全相同的 Transformer 块</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每层 <code>BertLayer</code> 又由 3 个子模块组成：</p><ul>
<li><strong>Attention（注意力机制）</strong>：让每个词「看到」句子中所有其他词，学习上下文关系。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(attention): BertAttention(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(self): BertSelfAttention(          # 多头自注意力</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(query/key/value): Linear(768→768)  # Q、K、V 三个线性变换</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>(output): BertSelfOutput(            # 注意力输出</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(dense): Linear(768→768)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>(LayerNorm): LayerNorm(768)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>(dropout): Dropout(p=0.1)</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>Intermediate（中间层）</strong></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(dense): Linear(768 → 3072)         # 扩展到 4 倍维度</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(intermediate_act_fn): GELUActivation()  # GELU 激活函数</span></div></div><div><div><div>4</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>Output（输出层）</strong></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(output): BertOutput(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(dense): Linear(3072 → 768)         # 压缩回 768 维</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(LayerNorm): LayerNorm(768)</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(dropout): Dropout(p=0.1)</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>中间层和输出层合称 <strong>FFN（前馈神经网络）</strong>：先扩张再压缩，增加模型的表达能力。</p><ol>
<li><strong>Pooler（汇聚层）</strong>—— 提取 <code>[CLS]</code> 向量：取出编码器输出中第一个位置 <code>[CLS]</code> 的向量，再经过一层线性变换 + tanh，作为整句话的语义表示，常用于分类任务。</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(pooler): BertPooler(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(dense): Linear(768 → 768)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(activation): Tanh()                # tanh 激活</span></div></div><div><div><div>4</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整体架构示意图：</p><p><img src="https://pub-e664ec87dac7457e9cc002e8813a9793.r2.dev/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20260605162104902.png" alt="" /></p><p>这个架构就是 BERT 的核心：<strong>双向的 Transformer 编码器</strong>，通过 MLM（遮蔽语言模型）和 NSP（下一句预测）两个任务进行预训练，学到了丰富的语言知识。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cls_model</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertForSequenceClassification(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(bert): BertModel(</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>(embeddings): BertEmbeddings(</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>(word_embeddings): Embedding(30522, 768, padding_idx=0)</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>(position_embeddings): Embedding(512, 768)</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>(token_type_embeddings): Embedding(2, 768)</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(encoder): BertEncoder(</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>(layer): ModuleList(</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(0-11): 12 x BertLayer(</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>(attention): BertAttention(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(self): BertSelfAttention(</span></div></div><div><div><div>15</div></div><div><span><span>              </span></span><span>(query): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>16</div></div><div><span><span>              </span></span><span>(key): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>17</div></div><div><span><span>              </span></span><span>(value): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>18</div></div><div><span><span>              </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>(output): BertSelfOutput(</span></div></div><div><div><div>21</div></div><div><span><span>              </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>22</div></div><div><span><span>              </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>23</div></div><div><span><span>              </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>(intermediate): BertIntermediate(</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=768, out_features=3072, bias=True)</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>(intermediate_act_fn): GELUActivation()</span></div></div><div><div><div>29</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>          </span></span><span>(output): BertOutput(</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>(dense): Linear(in_features=3072, out_features=768, bias=True)</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>34</div></div><div><span><span>          </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>      </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>(pooler): BertPooler(</span></div></div><div><div><div>39</div></div><div><span><span>      </span></span><span>(dense): Linear(in_features=768, out_features=768, bias=True)</span></div></div><div><div><div>40</div></div><div><span><span>      </span></span><span>(activation): Tanh()</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>(dropout): Dropout(p=0.1, inplace=False)</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>(classifier): Linear(in_features=768, out_features=2, bias=True)</span></div></div><div><div><div>45</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>1.2 BertForSequenceClassification 的输出<a href="#12-bertforsequenceclassification-的输出"><span>#</span></a></h2><p><code>BertModel</code>（基础模型）的架构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertModel</span></div></div><div><div><div>2</div></div><div><span>├── embeddings</span></div></div><div><div><div>3</div></div><div><span>├── encoder (12× BertLayer)</span></div></div><div><div><div>4</div></div><div><span>└── pooler</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>BertForSequenceClassification</code>（分类模型）的架构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertForSequenceClassification</span></div></div><div><div><div>2</div></div><div><span>├── bert (整个 BertModel)</span></div></div><div><div><div>3</div></div><div><span>│   ├── embeddings</span></div></div><div><div><div>4</div></div><div><span>│   ├── encoder (12× BertLayer)</span></div></div><div><div><div>5</div></div><div><span>│   └── pooler</span></div></div><div><div><div>6</div></div><div><span>├── dropout</span></div></div><div><div><div>7</div></div><div><span>└── classifier: Linear(768 → 2)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对比如下：</p>

<table><thead><tr><th>对比项</th><th><code>BertModel</code></th><th><code>BertForSequenceClassification</code></th></tr></thead><tbody><tr><td><strong>结构</strong></td><td>只有 backbone</td><td>在 backbone 基础上 + <strong>分类头</strong></td></tr><tr><td><strong>额外模块</strong></td><td>无</td><td><code>dropout</code> + <code>classifier: Linear(768 → 2)</code></td></tr><tr><td><strong>输出</strong></td><td>所有 token 的 768 维向量 + pooler 输出</td><td>2 个类别的 logits（如正面/负面）</td></tr><tr><td><strong>用途</strong></td><td>通用特征提取器</td><td>专用于<strong>文本分类任务</strong></td></tr></tbody></table><p>新增的两个模块：</p><ol>
<li><strong><code>dropout</code></strong> —— 随机丢弃 10% 的神经元，防止微调时过拟合。</li>
<li><strong><code>classifier: Linear(768 → 2)</code></strong> —— 将 pooler 输出的 <code>[CLS]</code> 向量（768 维）映射到 2 个分类标签上。</li>
</ol><p><strong>为什么这样设计？</strong></p><p>这背后是 <strong>“预训练 + 微调”</strong>（Pre-train then Fine-tune）范式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>预训练阶段:       BertModel (在大规模无标注语料上学习通用语言知识)</span></div></div><div><div><div>2</div></div><div><span><span>                       </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>微调阶段:    BertForSequenceClassification (用标注数据微调整个模型)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>
<p><strong><code>BertModel</code></strong> 就像是一个<strong>通用的「语言理解引擎」</strong>，通过 MLM 和 NSP 任务学会了词法、句法和语义知识，但它本身不针对任何具体任务。</p>
</li>
<li>
<p><strong><code>BertForSequenceClassification</code></strong> 则是在这个引擎上<strong>加了一个「任务专用头」</strong>（classifier head），用于具体的分类任务（如情感分析、垃圾邮件检测等）。</p>
</li>
<li>
<p>微调时，<strong>整个模型（bert backbone + 分类头）一起训练</strong>。Bert 部分做微小的参数调整来适配任务，而新加的 <code>classifier</code> 层则从头学习如何将 <code>[CLS]</code> 语义向量映射到目标类别。</p>
</li>
<li>
<p><code>in_features=768</code> 的 768 正是 <code>BertModel</code> 的 <code>hidden_size</code>，而 <code>out_features=2</code> 表示该任务有 2 个分类（例如正面/负面）。</p>
</li>
</ul><blockquote><p>简单类比：<strong>BertModel 像是一个大学毕业生（通才），BertForSequenceClassification 像是在这个毕业生基础上进行「岗前培训」，让他成为某个特定岗位的专家（专才）。</strong></p></blockquote></section>
<section><h2>1.3 为什么 BERT 没有 decoder<a href="#13-为什么-bert-没有-decoder"><span>#</span></a></h2><p>先回顾一下原始 Transformer 和 BERT 的关系：</p><hr /><section><h3>1.3.1 原始 Transformer = Encoder + Decoder<a href="#131-原始-transformer--encoder--decoder"><span>#</span></a></h3><p>原始的 Transformer（2017 年由 Google 提出）是 <strong>Encoder-Decoder 架构</strong>，专门用于<strong>序列到序列</strong>（seq2seq）任务，比如机器翻译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>原始 Transformer</span></div></div><div><div><div>2</div></div><div><span>├── Encoder（编码器：6层） ← 双向注意力（能看到所有词）</span></div></div><div><div><div>3</div></div><div><span>└── Decoder（解码器：6层） ← 掩码注意力（只能看左边的词）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>1.3.2 BERT 为什么只用 Encoder？<a href="#132-bert-为什么只用-encoder"><span>#</span></a></h3><p>因为 <strong>BERT 的定位是「语言理解」模型，而不是「语言生成」模型</strong>。具体来说：</p><section><h4>1. 预训练任务决定了架构<a href="#1-预训练任务决定了架构"><span>#</span></a></h4>

<table><thead><tr><th>模型</th><th>预训练任务</th><th>需要什么能力</th><th>选用哪部分</th></tr></thead><tbody><tr><td><strong>BERT</strong></td><td>MLM（遮蔽语言模型）+ NSP</td><td>理解上下文 → 预测被遮住的词</td><td><strong>Encoder</strong>（双向上下文）</td></tr><tr><td>GPT</td><td>自回归语言模型（预测下一个词）</td><td>从左到右依次生成</td><td><strong>Decoder</strong>（单向因果注意力）</td></tr><tr><td>T5 / BART</td><td>去噪自编码（如翻译、文本修复）</td><td>理解输入 + 生成输出</td><td><strong>Encoder + Decoder</strong></td></tr></tbody></table><ul>
<li><strong>MLM 任务</strong>：随机遮住 15% 的词，让模型根据<strong>左右两边的上下文</strong>来预测被遮住的词。</li>
<li>这就要求模型能同时看到某个词的<strong>左边和右边</strong>的信息——这正是 <strong>Encoder 的双向自注意力</strong>（bidirectional self-attention）的特性。</li>
</ul></section><section><h4>2. Decoder 的因果掩码（Causal Mask）不适合 BERT<a href="#2-decoder-的因果掩码causal-mask不适合-bert"><span>#</span></a></h4><p>Decoder 的核心特点是 <strong>因果注意力（causal attention）</strong>：每个 token 只能看到它自己和它左边的 token，看不到右边的 token。这是为了在生成时保证<strong>自回归</strong>性质（不能偷看未来的词）。</p><p>但 BERT 的任务（MLM）恰恰需要<strong>看到未来的词</strong>（右边的上下文）来预测当前遮住的词，所以 Decoder 的因果掩码反而会阻碍 BERT 的学习目标。</p><hr /><p>一张图总结：不同模型家族的架构选择</p><div><div><div><div><span><p>翻译/转换派 (Encoder-Decoder)</p></span></div><div><span><p>生成派 (Decoder-only)</p></span></div><div><span><p>理解派 (Encoder-only)</p></span></div><div><span><p>原始 Transformer</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Encoder<br /><br />双向注意力</p></span></div><div><span><p>Decoder<br /><br />因果注意力</p></span></div><div><span><p>BERT<br /><br />RoBERTa<br /><br />ALBERT</p></span></div><div><span><p>GPT<br /><br />LLaMA<br /><br />Qwen</p></span></div><div><span><p>T5<br /><br />BART<br /><br />mT5</p></span></div>
</div><div><div><span><p>翻译/转换派 (Encoder-Decoder)</p></span></div><div><span><p>生成派 (Decoder-only)</p></span></div><div><span><p>理解派 (Encoder-only)</p></span></div><div><span><p>原始 Transformer</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Encoder<br /><br />双向注意力</p></span></div><div><span><p>Decoder<br /><br />因果注意力</p></span></div><div><span><p>BERT<br /><br />RoBERTa<br /><br />ALBERT</p></span></div><div><span><p>GPT<br /><br />LLaMA<br /><br />Qwen</p></span></div><div><span><p>T5<br /><br />BART<br /><br />mT5</p></span></div>
</div></div></div>

<table><thead><tr><th>流派</th><th>代表模型</th><th>使用部分</th><th>典型任务</th></tr></thead><tbody><tr><td><strong>Encoder-only</strong></td><td>BERT, RoBERTa, ALBERT</td><td>只用 <strong>Encoder</strong></td><td>文本分类、情感分析、NER、阅读理解</td></tr><tr><td><strong>Decoder-only</strong></td><td>GPT, LLaMA, Qwen</td><td>只用 <strong>Decoder</strong></td><td>文本生成、对话、代码生成</td></tr><tr><td><strong>Encoder-Decoder</strong></td><td>T5, BART, mT5</td><td><strong>Encoder + Decoder</strong></td><td>翻译、摘要、文本修复</td></tr></tbody></table><hr /></section></section><section><h3>1.3.3 一句话总结<a href="#133-一句话总结"><span>#</span></a></h3><blockquote><p><strong>BERT 只用 Encoder 是因为它的目标是「理解」而不是「生成」——需要同时看左右上下文来理解语义，而这正是 Encoder 双向注意力的专长。Decoder 的因果掩码（只能看左边）反而会限制这种理解能力。</strong></p></blockquote></section></section>
<section><h1>2. 参数统计<a href="#2-参数统计"><span>#</span></a></h1><ul>
<li><code>total_params</code> ：总参数量</li>
<li><code>total__learnable_params</code> ：可学习参数量</li>
</ul><section><h2>2.1 参数形状<a href="#21-参数形状"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> name, param </span><span>in</span><span><span> model.</span><span>named_parameters</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>print</span><span>(name, </span><span>'-&gt;'</span><span>, param.shape)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>embeddings.word_embeddings.weight -&gt; torch.Size([30522, 768])</span></div></div><div><div><div>2</div></div><div><span>embeddings.position_embeddings.weight -&gt; torch.Size([512, 768])</span></div></div><div><div><div>3</div></div><div><span>embeddings.token_type_embeddings.weight -&gt; torch.Size([2, 768])</span></div></div><div><div><div>4</div></div><div><span>embeddings.LayerNorm.weight -&gt; torch.Size([768])</span></div></div><div><div><div>5</div></div><div><span>embeddings.LayerNorm.bias -&gt; torch.Size([768])</span></div></div><div><div><div>6</div></div><div><span>encoder.layer.0.attention.self.query.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>7</div></div><div><span>encoder.layer.0.attention.self.query.bias -&gt; torch.Size([768])</span></div></div><div><div><div>8</div></div><div><span>encoder.layer.0.attention.self.key.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>9</div></div><div><span>encoder.layer.0.attention.self.key.bias -&gt; torch.Size([768])</span></div></div><div><div><div>10</div></div><div><span>encoder.layer.0.attention.self.value.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>11</div></div><div><span>encoder.layer.0.attention.self.value.bias -&gt; torch.Size([768])</span></div></div><div><div><div>12</div></div><div><span>encoder.layer.0.attention.output.dense.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>13</div></div><div><span>encoder.layer.0.attention.output.dense.bias -&gt; torch.Size([768])</span></div></div><div><div><div>14</div></div><div><span>encoder.layer.0.attention.output.LayerNorm.weight -&gt; torch.Size([768])</span></div></div><div><div><div>15</div></div><div><span>encoder.layer.0.attention.output.LayerNorm.bias -&gt; torch.Size([768])</span></div></div><div><div><div>16</div></div><div><span>encoder.layer.0.intermediate.dense.weight -&gt; torch.Size([3072, 768])</span></div></div><div><div><div>17</div></div><div><span>encoder.layer.0.intermediate.dense.bias -&gt; torch.Size([3072])</span></div></div><div><div><div>18</div></div><div><span>encoder.layer.0.output.dense.weight -&gt; torch.Size([768, 3072])</span></div></div><div><div><div>19</div></div><div><span>encoder.layer.0.output.dense.bias -&gt; torch.Size([768])</span></div></div><div><div><div>20</div></div><div><span>encoder.layer.0.output.LayerNorm.weight -&gt; torch.Size([768])</span></div></div><div><div><div>21</div></div><div><span>encoder.layer.0.output.LayerNorm.bias -&gt; torch.Size([768])</span></div></div><div><div><div>22</div></div><div><span>encoder.layer.1.attention.self.query.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>23</div></div><div><span>encoder.layer.1.attention.self.query.bias -&gt; torch.Size([768])</span></div></div><div><div><div>24</div></div><div><span>encoder.layer.1.attention.self.key.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>25</div></div><div><span>encoder.layer.1.attention.self.key.bias -&gt; torch.Size([768])</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span>encoder.layer.11.output.LayerNorm.weight -&gt; torch.Size([768])</span></div></div><div><div><div>28</div></div><div><span>encoder.layer.11.output.LayerNorm.bias -&gt; torch.Size([768])</span></div></div><div><div><div>29</div></div><div><span>pooler.dense.weight -&gt; torch.Size([768, 768])</span></div></div><div><div><div>30</div></div><div><span>pooler.dense.bias -&gt; torch.Size([768])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出解析：</p><ul>
<li>Embeddings 层（3 个嵌入矩阵 + LayerNorm）
<ul>
<li>前缀 <code>embeddings</code> 说明在哪一层，后面的 <code>word_embeddings</code> 说明在层里的哪个模块中，<code>weight</code> 表示具体的参数是什么。</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>embeddings.word_embeddings.weight       -&gt; [30522, 768]   # 词嵌入矩阵：30522个词 × 768维</span></div></div><div><div><div>2</div></div><div><span>embeddings.position_embeddings.weight   -&gt; [512, 768]     # 位置嵌入：512个位置 × 768维</span></div></div><div><div><div>3</div></div><div><span>embeddings.token_type_embeddings.weight -&gt; [2, 768]       # 句子类型嵌入：2种类型 × 768维</span></div></div><div><div><div>4</div></div><div><span>embeddings.LayerNorm.weight             -&gt; [768]          # LayerNorm 的缩放参数（γ）</span></div></div><div><div><div>5</div></div><div><span>embeddings.LayerNorm.bias               -&gt; [768]          # LayerNorm 的偏移参数（β）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>第 0 层 Encoder 的 Attention 部分（以 layer.0 为例）</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Q、K、V 三个线性变换：每个都是 768 → 768</span></div></div><div><div><div>2</div></div><div><span>encoder.layer.0.attention.self.query.weight   -&gt; [768, 768]   # 权重矩阵 W_q</span></div></div><div><div><div>3</div></div><div><span>encoder.layer.0.attention.self.query.bias     -&gt; [768]        # 偏置 b_q</span></div></div><div><div><div>4</div></div><div><span>encoder.layer.0.attention.self.key.weight     -&gt; [768, 768]   # 权重矩阵 W_k</span></div></div><div><div><div>5</div></div><div><span>encoder.layer.0.attention.self.key.bias       -&gt; [768]        # 偏置 b_k</span></div></div><div><div><div>6</div></div><div><span>encoder.layer.0.attention.self.value.weight   -&gt; [768, 768]   # 权重矩阵 W_v</span></div></div><div><div><div>7</div></div><div><span>encoder.layer.0.attention.self.value.bias     -&gt; [768]        # 偏置 b_v</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 注意力输出层</span></div></div><div><div><div>10</div></div><div><span>encoder.layer.0.attention.output.dense.weight      -&gt; [768, 768]   # 线性变换</span></div></div><div><div><div>11</div></div><div><span>encoder.layer.0.attention.output.dense.bias        -&gt; [768]</span></div></div><div><div><div>12</div></div><div><span>encoder.layer.0.attention.output.LayerNorm.weight  -&gt; [768]</span></div></div><div><div><div>13</div></div><div><span>encoder.layer.0.attention.output.LayerNorm.bias    -&gt; [768]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>第 0 层 Encoder 的 FFN 部分</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Intermediate：扩展层 768 → 3072（4倍）</span></div></div><div><div><div>2</div></div><div><span>encoder.layer.0.intermediate.dense.weight -&gt; [3072, 768]</span></div></div><div><div><div>3</div></div><div><span>encoder.layer.0.intermediate.dense.bias   -&gt; [3072]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># Output：压缩层 3072 → 768</span></div></div><div><div><div>6</div></div><div><span>encoder.layer.0.output.dense.weight       -&gt; [768, 3072]</span></div></div><div><div><div>7</div></div><div><span>encoder.layer.0.output.dense.bias         -&gt; [768]</span></div></div><div><div><div>8</div></div><div><span>encoder.layer.0.output.LayerNorm.weight   -&gt; [768]</span></div></div><div><div><div>9</div></div><div><span>encoder.layer.0.output.LayerNorm.bias     -&gt; [768]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>第 1~11 层（结构与 layer.0 完全相同）</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>encoder.layer.1.attention.self.query.weight -&gt; [768, 768]</span></div></div><div><div><div>2</div></div><div><span>encoder.layer.1.attention.self.key.weight   -&gt; [768, 768]</span></div></div><div><div><div>3</div></div><div><span>...  # 每一层都重复同样的结构，共 12 层</span></div></div><div><div><div>4</div></div><div><span>encoder.layer.11.output.LayerNorm.weight    -&gt; [768]</span></div></div><div><div><div>5</div></div><div><span>encoder.layer.11.output.LayerNorm.bias      -&gt; [768]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>Pooler 层</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pooler.dense.weight -&gt; [768, 768]   # 将 [CLS] 向量再做一次线性变换</span></div></div><div><div><div>2</div></div><div><span>pooler.dense.bias   -&gt; [768]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>核心观察：</p>

<table><thead><tr><th>观察点</th><th>说明</th><th>原因</th></tr></thead><tbody><tr><td><strong>词嵌入矩阵最大</strong></td><td><code>[30522, 768]</code> ≈ 2340 万参数</td><td>30522 是整个词表大小，这是模型中最大的单一张量</td></tr><tr><td><strong>每层结构完全相同</strong></td><td>layer.0 ~ layer.11 的参数形状一样</td><td>12 层 Transformer 共享同一结构，参数独立</td></tr><tr><td><strong>Q/K/V 形状相同</strong></td><td>都是 <code>[768, 768]</code></td><td>多头注意力中 Q/K/V 维度一致</td></tr><tr><td><strong>Intermediate 是 4 倍</strong></td><td><code>[3072, 768]</code> = 768 × 4</td><td>这是 BERT 的设计惯例：FFN 中间层扩展到 4 倍隐藏维度</td></tr><tr><td><strong>线性层的 weight 形状是 <code>[out, in]</code></strong></td><td>例如 <code>[3072, 768]</code> 表示输出 3072、输入 768</td><td>PyTorch 的 <code>Linear</code> 层使用 <code>weight.shape = [out_features, in_features]</code></td></tr></tbody></table><p>你可以用这个输出手算 BERT-base 的总参数量（约 <strong>1.1 亿</strong>）：把每个 shape 的元素个数相乘再求和即可。比如 <code>[30522, 768]</code> 就是 30522 × 768 个参数。</p></section><section><h2>2.2 参数量<a href="#22-参数量"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>total_params </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>2</div></div><div><span><span>total_learnable_params </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>for</span><span> name, param </span><span>in</span><span><span> model.</span><span>named_parameters</span><span>():</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(name, </span><span>'-&gt;'</span><span>, param.shape, </span><span>'-&gt;'</span><span><span>, param.</span><span>numel</span><span>())</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span> param.requires_grad:</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>total_learnable_params </span><span>+=</span><span> param.</span><span>numel</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>total_params </span><span>+=</span><span> param.</span><span>numel</span><span>()</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>print</span><span>(</span><span>'Total parameters:'</span><span>, total_params)</span></div></div><div><div><div>11</div></div><div><span>print</span><span>(</span><span>'Total learnable parameters:'</span><span>, total_learnable_params)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>embeddings.word_embeddings.weight -&gt; torch.Size([30522, 768]) -&gt; 23440896</span></div></div><div><div><div>2</div></div><div><span>embeddings.position_embeddings.weight -&gt; torch.Size([512, 768]) -&gt; 393216</span></div></div><div><div><div>3</div></div><div><span>embeddings.token_type_embeddings.weight -&gt; torch.Size([2, 768]) -&gt; 1536</span></div></div><div><div><div>4</div></div><div><span>embeddings.LayerNorm.weight -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>5</div></div><div><span>embeddings.LayerNorm.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>6</div></div><div><span>encoder.layer.0.attention.self.query.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>7</div></div><div><span>encoder.layer.0.attention.self.query.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>8</div></div><div><span>encoder.layer.0.attention.self.key.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>9</div></div><div><span>encoder.layer.0.attention.self.key.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>10</div></div><div><span>encoder.layer.0.attention.self.value.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>11</div></div><div><span>encoder.layer.0.attention.self.value.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>12</div></div><div><span>encoder.layer.0.attention.output.dense.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>13</div></div><div><span>encoder.layer.0.attention.output.dense.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>14</div></div><div><span>encoder.layer.0.attention.output.LayerNorm.weight -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>15</div></div><div><span>encoder.layer.0.attention.output.LayerNorm.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>16</div></div><div><span>encoder.layer.0.intermediate.dense.weight -&gt; torch.Size([3072, 768]) -&gt; 2359296</span></div></div><div><div><div>17</div></div><div><span>encoder.layer.0.intermediate.dense.bias -&gt; torch.Size([3072]) -&gt; 3072</span></div></div><div><div><div>18</div></div><div><span>encoder.layer.0.output.dense.weight -&gt; torch.Size([768, 3072]) -&gt; 2359296</span></div></div><div><div><div>19</div></div><div><span>encoder.layer.0.output.dense.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>20</div></div><div><span>encoder.layer.0.output.LayerNorm.weight -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>21</div></div><div><span>encoder.layer.0.output.LayerNorm.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>22</div></div><div><span>encoder.layer.1.attention.self.query.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>23</div></div><div><span>encoder.layer.1.attention.self.query.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>24</div></div><div><span>encoder.layer.1.attention.self.key.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>25</div></div><div><span>encoder.layer.1.attention.self.key.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span>pooler.dense.weight -&gt; torch.Size([768, 768]) -&gt; 589824</span></div></div><div><div><div>28</div></div><div><span>pooler.dense.bias -&gt; torch.Size([768]) -&gt; 768</span></div></div><div><div><div>29</div></div><div><span>Total parameters: 109482240</span></div></div><div><div><div>30</div></div><div><span>Total learnable parameters: 109482240</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可见，可学习的参数量与总参数量是一致的，接近 1.1 亿。</p><p>我们接下来看看每层每个模块的参数量：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>total_embedding_params </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>2</div></div><div><span><span>total_encoder_params </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>3</div></div><div><span><span>total_pooler_params </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>for</span><span> name, param </span><span>in</span><span><span> model.</span><span>named_parameters</span><span>():</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> name.</span><span>startswith</span><span>(</span></span><span>'embeddings'</span><span>):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>total_embedding_params </span><span>+=</span><span> param.</span><span>numel</span><span>()</span></div></div><div><div><div>8</div></div><div><span>    </span><span>elif</span><span><span> name.</span><span>startswith</span><span>(</span></span><span>'encoder'</span><span>):</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>total_encoder_params </span><span>+=</span><span> param.</span><span>numel</span><span>()</span></div></div><div><div><div>10</div></div><div><span>    </span><span>elif</span><span><span> name.</span><span>startswith</span><span>(</span></span><span>'pooler'</span><span>):</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>total_pooler_params </span><span>+=</span><span> param.</span><span>numel</span><span>()</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>print</span><span>(</span><span>'Total embedding parameters:'</span><span>, total_embedding_params)</span></div></div><div><div><div>14</div></div><div><span>print</span><span>(</span><span>'Total encoder parameters:'</span><span>, total_encoder_params)</span></div></div><div><div><div>15</div></div><div><span>print</span><span>(</span><span>'Total pooler parameters:'</span><span>, total_pooler_params)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Total embedding parameters: 23837184</span></div></div><div><div><div>2</div></div><div><span>Total encoder parameters: 85054464</span></div></div><div><div><div>3</div></div><div><span>Total pooler parameters: 590592</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面是占比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>params </span><span>=</span><span> [total_embedding_params, total_encoder_params, total_pooler_params]</span></span></div></div><div><div><div>2</div></div><div><span>for</span><span> param </span><span>in</span><span> params:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>'</span><span>{</span><span><span>param </span><span>/</span><span> total_params</span></span><span>:.2%</span><span>}</span><span>'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>21.77%</span></div></div><div><div><div>2</div></div><div><span>77.69%</span></div></div><div><div><div>3</div></div><div><span>0.54%</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-02/</id>
      <title type="text">02_tokenizer_encode_plus_token_type_ids</title>
      <published>2026-06-07T00:00:00.000Z</published>
      <updated>2026-06-14T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-02/"/>
      <summary type="text">动手写 BERT 系列笔记第二节</summary>
      <content type="html"><![CDATA[<p>上一节我们通过情感分析的例子初探了 BERT 模型的一次完整推理。这一节，我们来看 BERT 的两个核心预训练任务：MLM 与 NSP. 我们同时也介绍 <code>token_type_ids</code> 的设计来源。</p>
<section><h1>1. 加载模型<a href="#1-加载模型"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> BertTokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'bert-base-uncased'</span></div></div><div><div><div>2</div></div><div><span><span>tokenizer </span><span>=</span><span> BertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tokenizer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>BertTokenizer(name_or_path='bert-base-uncased', vocab_size=30522, model_max_length=512, padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}, added_tokens_decoder={</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>0: AddedToken("[PAD]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>100: AddedToken("[UNK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>101: AddedToken("[CLS]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>102: AddedToken("[SEP]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>103: AddedToken("[MASK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),</span></div></div><div><div><div>7</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出解析：</p><p>打印 <code>tokenizer</code> 可以查看 tokenizer 的配置摘要：</p>

<table><thead><tr><th>字段</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>name_or_path</code></td><td><code>'bert-base-uncased'</code></td><td>加载自哪个 checkpoint</td></tr><tr><td><code>vocab_size</code></td><td><code>30522</code></td><td>词表大小</td></tr><tr><td><code>model_max_length</code></td><td><code>512</code></td><td>模型能接受的最大 token 数</td></tr><tr><td><code>padding_side</code></td><td><code>'right'</code></td><td>padding 加在句子右侧</td></tr><tr><td><code>truncation_side</code></td><td><code>'right'</code></td><td>超长时从右边截断</td></tr></tbody></table><p><code>special_tokens</code> 特殊标记及其对应的 token ID：</p>

<table><thead><tr><th>token</th><th>ID</th><th>作用</th></tr></thead><tbody><tr><td><code>[UNK]</code></td><td>100</td><td>遇到不认识的字时替换</td></tr><tr><td><code>[SEP]</code></td><td>102</td><td>句子分隔符/结束符</td></tr><tr><td><code>[PAD]</code></td><td>0</td><td>填充符，补齐长度用</td></tr><tr><td><code>[CLS]</code></td><td>101</td><td>分类标记，放在句首</td></tr><tr><td><code>[MASK]</code></td><td>103</td><td>掩码标记，BERT 预训练时遮住词用</td></tr></tbody></table><p><code>added_tokens_decoder</code> 特殊标记的详细注册信息：</p><p>每个特殊标记被包装成 <code>AddedToken</code> 对象，属性含义如下：</p><ul>
<li><code>rstrip=False</code> / <code>lstrip=False</code>：在标记前后不自动去除空格</li>
<li><code>single_word=False</code>：不视为独立单词</li>
<li><code>normalized=False</code>：不做归一化（如小写化）</li>
<li><code>special=True</code>：标记为特殊 token（不会被拆分成子词）</li>
</ul></section>
<section><h1>2. 认识文本语料<a href="#2-认识文本语料"><span>#</span></a></h1><ul>
<li><code>newsgroups_train.DESCR</code> ：<strong>数据集描述</strong>，一段长文本，介绍这个数据集的来源、内容、划分方式等。类似你看到的 README 文档。</li>
<li><code>newsgroups_train.data</code> ：<strong>原始文本数据</strong>，包含类型、长度和内容示例。</li>
<li><code>newsgroups_train.target</code> ：<strong>标签（数字）</strong>。</li>
<li><code>newsgroups_train.target_names</code> ：<strong>标签名（文本）</strong>。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> sklearn.datasets </span><span>import</span><span> fetch_20newsgroups</span></div></div><div><div><div>2</div></div><div><span><span>newsgroups_train </span><span>=</span><span> </span><span>fetch_20newsgroups</span><span>(</span></span><span>subset</span><span>=</span><span>'train'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>fetch_20newsgroups</code> 返回一个 Bunch 对象，这是一种在 Python（特别是 Scikit-learn 机器学习库）中广泛使用的容器对象。它的本质是一个字典（dict），但额外允许像访问对象属性一样来获取和修改里面的值。</p><p>特性：</p><ul>
<li>字典式访问：你可以像普通字典一样使用键来访问数据，例如：<code>bunch["data"]</code>.</li>
<li>属性式访问：你可以像调用对象属性一样直接获取值，例如：<code>bunch.data</code>.</li>
<li>动态扩展：支持随时通过属性赋值的方式添加新键值对（如 <code>bunch.new_key = "value"</code>）.</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> collections </span><span>import</span><span> Counter</span></div></div><div><div><div>2</div></div><div><span><span>Counter</span><span>(newsgroups_train.target)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Counter({np.int64(10): 600,</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>np.int64(15): 599,</span></div></div><div><div><div>3</div></div><div><span><span>         </span></span><span>np.int64(8): 598,</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>np.int64(9): 597,</span></div></div><div><div><div>5</div></div><div><span><span>         </span></span><span>np.int64(11): 595,</span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>np.int64(7): 594,</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>np.int64(13): 594,</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>np.int64(14): 593,</span></div></div><div><div><div>9</div></div><div><span><span>         </span></span><span>np.int64(5): 593,</span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>np.int64(2): 591,</span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>np.int64(12): 591,</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>np.int64(3): 590,</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>np.int64(6): 585,</span></div></div><div><div><div>14</div></div><div><span><span>         </span></span><span>np.int64(1): 584,</span></div></div><div><div><div>15</div></div><div><span><span>         </span></span><span>np.int64(4): 578,</span></div></div><div><div><div>16</div></div><div><span><span>         </span></span><span>np.int64(17): 564,</span></div></div><div><div><div>17</div></div><div><span><span>         </span></span><span>np.int64(16): 546,</span></div></div><div><div><div>18</div></div><div><span><span>         </span></span><span>np.int64(0): 480,</span></div></div><div><div><div>19</div></div><div><span><span>         </span></span><span>np.int64(18): 465,</span></div></div><div><div><div>20</div></div><div><span><span>         </span></span><span>np.int64(19): 377})</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们可以看到数据集中每个类别的样本数量有多少，比如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>np.int64(10): 600    ← 类别10（棒球）有 600 篇新闻，最多</span></div></div><div><div><div>2</div></div><div><span>np.int64(15): 599    ← 类别15（基督教）有 599 篇</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div><div><div><div>4</div></div><div><span>np.int64(19): 377    ← 类别19（宗教杂谈）只有 377 篇，最少</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其中：</p><ul>
<li><code>np.int64()</code> ：NumPy 的 64 位整数类型。<code>newsgroups_train.target</code> 是 NumPy 数组，它的元素类型是 <code>int64</code>，所以 Counter 统计时 key 保留了这个类型。
<ul>
<li><code>np.int64(10)</code> ：<code>(10)</code> 表示具体的类别编号，对应 20 个新闻组中的第 10 个。</li>
</ul>
</li>
</ul><p>一些其他的调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>type</span><span>(newsgroups_train.data) </span><span># list</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>len</span><span>(newsgroups_train.data) </span><span># 11314</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>len</span><span>(newsgroups_train.target) </span><span># 11314</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>len</span><span>(newsgroups_train.target_names) </span><span># 20</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>newsgroups_train.target_names</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>list # data 的类型为 list</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>11314 # 总共 11314 条数据</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>11314 # 总共 11314 个数字标签</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>20 # 总共 20 类</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>['alt.atheism',</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>'comp.graphics',</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>'comp.os.ms-windows.misc',</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>'comp.sys.ibm.pc.hardware',</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>'comp.sys.mac.hardware',</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>'comp.windows.x',</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>'misc.forsale',</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>'rec.autos',</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>'rec.motorcycles',</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>'rec.sport.baseball',</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>'rec.sport.hockey',</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>'sci.crypt',</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>'sci.electronics',</span></div></div><div><div><div>22</div></div><div><span><span> </span></span><span>'sci.med',</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>'sci.space',</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>'soc.religion.christian',</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>'talk.politics.guns',</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>'talk.politics.mideast',</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>'talk.politics.misc',</span></div></div><div><div><div>28</div></div><div><span><span> </span></span><span>'talk.religion.misc'] # 每一类的类别名称</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h1>3. tokenizer 补充<a href="#3-tokenizer-补充"><span>#</span></a></h1><ul>
<li><code>input_ids</code></li>
<li><code>attention_masks</code></li>
<li><code>encode_plus</code></li>
<li><code>token_type_ids</code></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>test_news </span><span>=</span><span> newsgroups_train.data[:</span></span><span>3</span><span>]</span></div></div><div><div><div>2</div></div><div><span>len</span><span>(test_news[</span><span>2</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1981</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个代表的是字节（字母）的数量。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer</span><span>(test_news, </span></span><span>truncation</span><span>=</span><span>True</span><span>, </span><span>max_length</span><span>=</span><span>32</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': [[101, 2013, 1024, 3393, 2099, 2595, 3367, 1030, 11333, 2213, 1012, 8529, 2094, 1012, 3968, 2226, 1006, 2073, 1005, 1055, 2026, 2518, 1007, 3395, 1024, 2054, 2482, 2003, 2023, 999, 1029, 102], [101, 2013, 1024, 3124, 5283, 2080, 1030, 9806, 1012, 1057, 1012, 2899, 1012, 3968, 2226, 1006, 3124, 13970, 2080, 1007, 3395, 1024, 9033, 5119, 8554, 1011, 2345, 2655, 12654, 1024, 2345, 102], [101, 2013, 1024, 1056, 29602, 6856, 1030, 14925, 1012, 14925, 2078, 1012, 19749, 1012, 3968, 2226, 1006, 2726, 1041, 12688, 1007, 3395, 1024, 1052, 2497, 3980, 1012, 1012, 1012, 3029, 1024, 102]], 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出解析：</p><ul>
<li><code>input_ids</code> ：是文本被 tokenizer 转化后的数字 ID 序列，也就是模型实际接收的”输入信号”（这里我们设置了 <code>truncation=True, max_length=32</code>，所以每个序列都被截断/补齐到了固定长度 32）。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>input_ids: [</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>[101, 2013, 1024, 3393, ..., 102],   ← 第1篇新闻，32个token</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>[101, 2013, 1024, 3124, ..., 102],   ← 第2篇新闻，32个token</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>[101, 2013, 1024, 1056, ..., 102]    ← 第3篇新闻，32个token</span></div></div><div><div><div>5</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>token_type_ids</code> ：可以看到 <code>token_type_ids</code> 全是 0. 这是因为我们的输入是<strong>单句分类任务</strong>。<code>token_type_ids</code> 是用来区分两个句子的：
<ul>
<li>单句分类（你的情况）：全为 0.</li>
<li>句子对任务（如问答）：第一个句子全 0，第二个句子全 1.</li>
</ul>
</li>
<li><code>attention_mask</code> ：可以看到 <code>attention_mask</code> 全是 1. 因为这三篇新闻都没有被 padding（没有补 0）。1 表示”这个位置是真实 token，模型要关注它”。
<ul>
<li>如果 batch 里有不同长度的句子，短的句子补了 <code>[PAD]</code>（ID=0），对应的 <code>attention_mask</code> 就会是 0.</li>
</ul>
</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer</span><span>(test_news[</span></span><span>0</span><span>], test_news[</span><span>1</span><span>], </span><span>truncation</span><span>=</span><span>True</span><span>, </span><span>padding</span><span>=</span><span>True</span><span>, </span><span>max_length</span><span>=</span><span>32</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>tokenizer.</span><span>decode</span><span>([</span></span><span>101</span><span>, </span><span>2013</span><span>, </span><span>1024</span><span>, </span><span>3393</span><span>, </span><span>2099</span><span>, </span><span>2595</span><span>, </span><span>3367</span><span>, </span><span>1030</span><span>, </span><span>11333</span><span>, </span><span>2213</span><span>, </span><span>1012</span><span>, </span><span>8529</span><span>, </span><span>2094</span><span>, </span><span>1012</span><span>, </span><span>3968</span><span>, </span><span>102</span><span>, </span><span>2013</span><span>, </span><span>1024</span><span>, </span><span>3124</span><span>, </span><span>5283</span><span>, </span><span>2080</span><span>, </span><span>1030</span><span>, </span><span>9806</span><span>, </span><span>1012</span><span>, </span><span>1057</span><span>, </span><span>1012</span><span>, </span><span>2899</span><span>, </span><span>1012</span><span>, </span><span>3968</span><span>, </span><span>2226</span><span>, </span><span>1006</span><span>, </span><span>102</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': [101, 2013, 1024, 3393, 2099, 2595, 3367, 1030, 11333, 2213, 1012, 8529, 2094, 1012, 3968, 102, 2013, 1024, 3124, 5283, 2080, 1030, 9806, 1012, 1057, 1012, 2899, 1012, 3968, 2226, 1006, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># 可以看到第一个句子和第二个句子</span></div></div><div><div><div>4</div></div><div><span>'[CLS] from : lerxst @ wam. umd. ed [SEP] from : guykuo @ carson. u. washington. edu ( [SEP]'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我们就能看出，对于句子对人物，第一个句子的 <code>token_type_ids</code> 全 0，第二个句子全 1.</p><p>关于句子对任务，其一般是用在 NSP(next sentence prediction) 任务中，也就是 BERT 的预训练任务。</p><p>顺带一提，BERT 的两大预训练任务分别是 NSP 和 MLM，其中 MLM 任务用的就是 mask.</p><section><h2>3.1 MLM(Masked Language Model) 掩码语言模型<a href="#31-mlmmasked-language-model-掩码语言模型"><span>#</span></a></h2><p><strong>是什么</strong>：随机遮住句子中 15% 的词，让 BERT 根据上下文去猜被遮住的是什么词。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入:  我今天去 [MASK] 了，天气真好</span></div></div><div><div><div>2</div></div><div><span><span>                    </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span><span>                  </span></span><span>BERT</span></div></div><div><div><div>4</div></div><div><span><span>                    </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>预测:  我今天去 [公园] 了，天气真好</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>和笔记的联系</strong>：你之前看到的 <code>tokenizer.vocab</code> 中的 <code>[MASK]</code>（ID=103）就是为此而生。MLM 让 BERT 学会了<strong>双向上下文理解</strong>——不仅要看左边的词，还要看右边的词，才能猜出被遮住的是什么。这是 BERT 的核心创新。</p><hr /></section><section><h2>3.2 NSP（Next Sentence Prediction）— 下一句预测<a href="#32-nspnext-sentence-prediction-下一句预测"><span>#</span></a></h2><p><strong>是什么</strong>：给 BERT 两个句子，让它判断第二句是不是第一句的”下一句”。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>句子A: 我今天去了公园                  ← 正例（是下一句）</span></div></div><div><div><div>2</div></div><div><span>句子B: 公园里有很多花和树              → 预测: IsNext</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>句子A: 我今天去了公园                  ← 负例（不是下一句）</span></div></div><div><div><div>5</div></div><div><span>句子B: 苹果是一种很好吃的水果           → 预测: NotNext</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>和笔记的联系</strong>：这就是你刚在代码中看到的！</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer</span><span>(test_news[</span></span><span>0</span><span>], test_news[</span><span>1</span><span><span>], </span><span>...</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>传入了两个句子（<code>text</code> + <code>text_pair</code>），所以输出中的 <code>token_type_ids</code> 变成了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[0, 0, 0, ..., 0, 1, 1, 1, ..., 1]</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>~~~~句子A~~~~ [SEP] ~~~~句子B~~~~</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>全 0</strong> = 第一个句子的 token</li>
<li><strong>全 1</strong> = 第二个句子的 token</li>
</ul><p>这个 <code>token_type_ids</code> 就是 BERT 用来区分两个句子的”标记”，让模型知道哪些 token 属于句子 A、哪些属于句子 B。</p><hr /></section><section><h2>3.3 两者与笔记的对应关系总结<a href="#33-两者与笔记的对应关系总结"><span>#</span></a></h2>

<table><thead><tr><th>预训练任务</th><th>笔记中的关联</th></tr></thead><tbody><tr><td><strong>MLM</strong></td><td><code>[MASK]</code> token（ID=103）—— 用来遮住词让模型预测</td></tr><tr><td><strong>NSP</strong></td><td><code>token_type_ids</code>（全 0 / 全 1）—— 用来区分两个句子</td></tr><tr><td></td><td><code>[SEP]</code> token（ID=102）—— 分隔两个句子</td></tr><tr><td></td><td><code>[CLS]</code> token（ID=101）—— NSP 的分类信号就取自它的输出</td></tr></tbody></table><p>BERT 先用 MLM 和 NSP 在大量无标签文本上<strong>预训练</strong>，然后再在你的情感分析任务上<strong>微调</strong>（即之前 <code>distilbert-base-uncased-finetuned-sst-2-english</code> 做的事情）。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/BERT-01/</id>
      <title type="text">01_tokenizer_sentiment_analysis</title>
      <published>2026-06-06T00:00:00.000Z</published>
      <updated>2026-06-14T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/BERT-01/"/>
      <summary type="text">动手写 BERT 系列笔记第一节</summary>
      <content type="html"><![CDATA[<section><h1>0. 写在前面<a href="#0-写在前面"><span>#</span></a></h1><p><strong>BERT</strong>（Bidirectional Encoder Representations from Transformers）是 Google 在 2018 年提出的<strong>预训练语言模型</strong>。它的核心思想可以概括为：</p><blockquote><p><strong>先在海量文本上”自学”语言规律（预训练），再针对具体任务”微调”（Fine-tuning）。</strong></p></blockquote><p>打个比方：BERT 就像一个已经读过整个维基百科和大量书籍的”语文大师”——它已经掌握了语法、词汇、上下文关系。你只需要在它上面加一层简单的分类器，就能让它做情感分析、问答、文本分类等任务，而不需要从零训练一个大模型。</p><p>它可以做：</p>

<table><thead><tr><th>应用场景</th><th>例子</th></tr></thead><tbody><tr><td>情感分析</td><td>判断评论是正面还是负面</td></tr><tr><td>文本分类</td><td>新闻分类、垃圾邮件过滤</td></tr><tr><td>问答系统</td><td>给定文章，回答问题</td></tr><tr><td>命名实体识别</td><td>提取人名、地名、组织名</td></tr><tr><td>语义搜索</td><td>理解搜索意图而非关键词匹配</td></tr></tbody></table><p>BERT 是 NLP 领域的<strong>奠基性工作</strong>，后来的 GPT、T5、LLaMA 等模型都在此基础上发展而来。可以说它是进入现代 NLP 和 LLM 世界的”必修课”。</p><hr /><p>这是 BERT 系列的第一节，用 DistilBERT（一个轻量版 BERT）做情感分析，手把手带你跑通整个推理流程。</p><p>整篇笔记分为<strong>两大块</strong>：我们首先看看 Tokenizer 是如何构造输入的，接着调用模型完成一次实际的推理过程。</p></section>
<section><h1>1. Tokenizer 构造输入<a href="#1-tokenizer-构造输入"><span>#</span></a></h1><ul>
<li>tokenizer, model ：相匹配，tokenizer outputs -&gt; model intput</li>
<li>Auto*Tokenizer, AutoModel* ：Generic type</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>test_sentences </span><span>=</span><span> [</span></span><span>'today is not that bad'</span><span>, </span><span>'today is so bad'</span><span>]</span></div></div><div><div><div>2</div></div><div><span><span>model_name </span><span>=</span><span> </span></span><span>'distilbert-base-uncased-finetuned-sst-2-english'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>1.1 Generic Type（通用类型）是什么意思？<a href="#11-generic-type通用类型是什么意思"><span>#</span></a></h2><p>这里的 <code>Auto*Tokenizer</code>、<code>AutoModel*</code> 被称为 Generic Type，意思是「根据你给的模型名称，自动猜出该用哪个具体类」。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoTokenizer, AutoModelForSequenceClassification</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>AutoTokenizer</code> 本身是一个”自动调度器”，你不需要手动指定：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># ❌ 如果不通用，你需要自己判断：</span></div></div><div><div><div>2</div></div><div><span>from</span><span> transformers </span><span>import</span><span> DistilBertTokenizer, DistilBertForSequenceClassification</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>tokenizer </span><span>=</span><span> DistilBertTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>5</div></div><div><span><span>model </span><span>=</span><span> DistilBertForSequenceClassification.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># ✅ 用 Auto 系列，框架自动帮你选：</span></div></div><div><div><div>8</div></div><div><span>from</span><span> transformers </span><span>import</span><span> AutoTokenizer, AutoModelForSequenceClassification</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_name)   </span></span><span># 自动选 DistilBertTokenizer</span></div></div><div><div><div>11</div></div><div><span><span>model </span><span>=</span><span> AutoModelForSequenceClassification.</span><span>from_pretrained</span><span>(model_name)  </span></span><span># 自动选 DistilBertForSequenceClassification</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Auto*</code> 系列就是 Hugging Face 提供的一个”万能接口”——你只需要告诉它模型名，它就自动匹配正确的 tokenizer 类和模型类，对初学者非常友好，不需要记每种模型的具体类名。</p></section><section><h2>1.2 Tokenizer 要与 Model 相匹配是什么意思？<a href="#12-tokenizer-要与-model-相匹配是什么意思"><span>#</span></a></h2><p>核心意思：Tokenizer 和 Model 必须来自同一个「预训练 checkpoint」，保证它们的”词汇表”一致。</p><div><div><div></div><div>什么是 checkpoint？</div></div><div><p>Checkpoint 本质上就是「训练过程中某一个时刻保存下来的完整模型快照」，包含模型权重、优化器状态、配置等。</p><p>你可以把它理解为一个游戏存档，模型训练到某个时刻保存下来，下次可以直接从这个状态继续训练或推理。</p><p>在下面的代码中，这个 <code>model_name</code> 就是 Hugging Face 模型中心（Hub）上的一个 checkpoint 的名称。它对应的是 Hugging Face 团队（或社区）在训练 DistilBERT 模型到最佳状态时保存下来的完整快照。</p><p>一个典型的 checkpoint 包含：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>distilbert-base-uncased-finetuned-sst-2-english/</span></div></div><div><div><div>2</div></div><div><span>├── config.json           ← 模型架构配置（几层transformer、隐藏层大小等）</span></div></div><div><div><div>3</div></div><div><span>├── model.safetensors     ← 模型权重参数（就是那个 104 个权重文件）</span></div></div><div><div><div>4</div></div><div><span>├── tokenizer_config.json ← tokenizer 配置</span></div></div><div><div><div>5</div></div><div><span>├── vocab.txt             ← 词表（每个词对应哪个数字ID）</span></div></div><div><div><div>6</div></div><div><span>└── ...                   ← 其他辅助文件</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div></div><p>用代码来说明：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer </span><span>=</span><span> AutoTokenizer.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div><div><div><div>2</div></div><div><span><span>model </span><span>=</span><span> AutoModelForSequenceClassification.</span><span>from_pretrained</span><span>(model_name)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 <code>tokenizer</code> 和 <code>model</code> 用的是同一个 <code>model_name</code>（同一个 <code>checkpoint</code>），这就是”相匹配”。</p><p>为什么需要匹配？</p><ul>
<li>Tokenizer 的工作是把文本变成数字 ID：<code>"today is not that bad"</code> → <code>[101, 2651, 2003, 2025, 2008, 2919, 102]</code></li>
<li>模型内部有一个 Embedding 层，它有一个固定的词表，比如第 2651 号位置存着 <code>"today"</code> 的向量表示。</li>
<li>如果 tokenizer 和 model 不匹配，比如用 BERT 的 tokenizer 给 DistilBERT 模型用——tokenizer 把 <code>"today"</code> 编成了编号 1234，但 DistilBERT 的第 1234 号位置存的是另一个词（比如 <code>"planet"</code>），那模型就会理解错，输出毫无意义的结果。</li>
</ul><p>所以 tokenizer outputs → model input 这条线意味着：tokenizer 产出的数字 ID 必须和 model 内部的词表一一对应，才能正确输入模型。</p><hr /><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 演示子词切分：遇到生词怎么办？</span></div></div><div><div><div>2</div></div><div><span><span>unknown_words </span><span>=</span><span> [</span></span><span>"today is not that bad"</span><span>, </span><span>"today is so bad"</span><span>, </span><span>"today is unbelievably bad"</span><span>, </span><span>"antidisestablishment"</span><span>]</span></div></div><div><div><div>3</div></div><div><span>for</span><span> w </span><span>in</span><span> unknown_words:</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>tokens </span><span>=</span><span> tokenizer.</span><span>tokenize</span><span>(w)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ids </span><span>=</span><span> tokenizer.</span><span>convert_tokens_to_ids</span><span>(tokens)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"原句: </span><span>{</span><span>w</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"  子词: </span><span>{</span><span>tokens</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"  IDs : </span><span>{</span><span>ids</span><span>}</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># 看看词表大小</span></div></div><div><div><div>11</div></div><div><span>print</span><span>(</span><span>f</span><span>"词表大小: </span><span>{</span><span>tokenizer.vocab_size</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>print</span><span>(</span><span>f</span><span>"unk_token: </span><span>{</span><span>tokenizer.unk_token</span><span>}</span><span> (ID=</span><span>{</span><span>tokenizer.unk_token_id</span><span>}</span><span>)"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>原句: today is not that bad</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>子词: ['today', 'is', 'not', 'that', 'bad'] # .tokenize() 的输出</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>IDs : [2651, 2003, 2025, 2008, 2919] # .convert_tokens_to_ids() 的输出</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>原句: today is so bad</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>子词: ['today', 'is', 'so', 'bad']</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>IDs : [2651, 2003, 2061, 2919]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>原句: today is unbelievably bad</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>子词: ['today', 'is', 'un', '##bel', '##ie', '##va', '##bly', 'bad']</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>IDs : [2651, 2003, 4895, 8671, 2666, 3567, 6321, 2919]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>原句: antidisestablishment</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>子词: ['anti', '##dis', '##est', '##ab', '##lish', '##ment']</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>IDs : [3424, 10521, 4355, 7875, 13602, 3672]</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>词表大小: 30522 # .vocab_size 的输出</span></div></div><div><div><div>18</div></div><div><span>unk_token: [UNK] (ID=100)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>batch_input </span><span>=</span><span> </span><span>tokenizer</span><span>(test_sentences, </span></span><span>padding</span><span>=</span><span>True</span><span>, </span><span>truncation</span><span>=</span><span>True</span><span>, </span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div><div><div><div>2</div></div><div><span>print</span><span>(batch_input)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[ 101, 2651, 2003, 2025, 2008, 2919,  102],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[ 101, 2651, 2003, 2061, 2919,  102,    0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>[1, 1, 1, 1, 1, 1, 0]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>参数解析：</p><ul>
<li><code>padding=True</code> ：<strong>补齐到相同长度</strong>。因为 <code>test_sentences</code> 是两个长度不同的句子，所以 <code>padding=True</code> 的作用是把短的句子用 0 补齐，让同一个 batch 里的所有句子长度相同，才能拼成一个矩阵输入模型。
<ul>
<li>从上面的输出中可以很清楚地看到，第一句：<code>[101, 2651, 2003, 2025, 2008, 2919, 102]</code>；第二句：<code>[ 101, 2651, 2003, 2061, 2919,  102,    0]</code>.</li>
</ul>
</li>
<li><code>truncation=True</code> ：<strong>超长截断</strong>。如果某个句子超过了模型能接受的最大长度（DistilBERT 是 512 个 token），就截掉超出的部分。对于你的短句不影响，但如果你处理一篇长文章时会非常重要。</li>
<li><code>return_tensors='pt'</code> ：<strong>告诉 tokenizer 返回的数据格式</strong>：
<ul>
<li><code>'pt'</code> → PyTorch 张量（我们用的框架）</li>
<li><code>'tf'</code> → TensorFlow 张量</li>
<li><code>'np'</code> → NumPy 数组</li>
<li>不指定 → Python 列表</li>
</ul>
</li>
</ul><p>输出解析：</p><ul>
<li><code>input_ids</code> ：<strong>每个词在词表中的编号</strong>。例如 101=CLS, 2651=today, 102=SEP, 0=PAD.
<ul>
<li><code>101=CLS</code> ：BERT 模型人为加入的特殊标记（Special Tokens），<strong>放在每个输入句子的最开头</strong>，它的最终隐藏状态（最后一层 Transformer 输出的向量）被设计用来代表整个句子的语义，用于分类任务。</li>
<li><code>102=SEP</code> ：<strong>放在每个句子的末尾</strong>，或者<strong>用于分隔两个句子</strong>。</li>
</ul>
</li>
<li><code>token_type_ids</code> ：<strong>区分不同句子的标记</strong>。全 0 说明只有一个句子（非句子对任务）。</li>
<li><code>attention_mask</code> ：<strong>1=真实词， 0=填充的无效位</strong>。第二行最后一个 0 表示那个位置是补的，模型应该忽略它。
<ul>
<li><code>attention_mask</code> 特别重要：它告诉模型”别管那些补的 0，它们不是真正的词”，这样模型计算注意力时就不会被无意义的 padding 干扰。</li>
</ul>
</li>
</ul><p>其他常见的特殊标记：</p><ul>
<li><code>[CLS]</code>=101：分类标记，在句首</li>
<li><code>[SEP]</code>=102：分隔/结束标记</li>
<li><code>[PAD]</code>=0：填充标记，用于补齐长度</li>
<li><code>[UNK]</code>=100：未知词标记，遇到不认识的字</li>
<li><code>[MASK]</code>=103：掩码标记，BERT 预训练时遮住词用</li>
</ul><p>一些其他的调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer</span><span>(test_sentences[</span></span><span>0</span><span>])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>tokenizer.</span><span>encode</span><span>(test_sentences[</span></span><span>0</span><span>])</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>tokenizer.</span><span>tokenize</span><span>(test_sentences[</span></span><span>0</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出分别为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': [101, 2651, 2003, 2025, 2008, 2919, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1]}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>[101, 2651, 2003, 2025, 2008, 2919, 102]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>['today', 'is', 'not', 'that', 'bad']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实际上我们可以看出 <code>tokenizer.encode() = tokenizer.tokenize() + tokenizer.convert_tokens_to_ids()</code>. 其中：</p><ul>
<li><code>tokenizer.encode()</code> 将单词编码为数字，且有特殊标记；</li>
<li><code>tokenizer.tokenize()</code> 是分词器，将句子拆成词；</li>
<li><code>tokenizer.convert_tokens_to_ids()</code> 将单词编码为数字，但不加特殊标记。</li>
</ul><hr /><p>接着我们来看解码的过程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer.</span><span>decode</span><span>([</span></span><span>101</span><span>, </span><span>2651</span><span>, </span><span>2003</span><span>, </span><span>2025</span><span>, </span><span>2008</span><span>, </span><span>2919</span><span>, </span><span>102</span><span>])</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>tokenizer.vocab</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>'[CLS] today is not that bad [SEP]'</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>{'constitutional': 6543,</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>'forearms': 27323,</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>'##ythe': 26688,</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>'##ל': 29799,</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>'[unused89]': 90,</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>'brothel': 27308,</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>'elsewhere': 6974,</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>'rey': 12569,</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>'##º': 29662,</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>...}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以看出，tokenizer 的原理就是 <code>tokenizer.vocab</code> 字典存储了 token -&gt; id 的映射关系，包括某些特殊的 token.</p><p><code>tokenizer</code> 的参数如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokenizer</span><span>(test_sentences, </span></span><span>max_length</span><span>=</span><span>512</span><span>, </span><span>padding</span><span>=</span><span>True</span><span>, </span><span>truncation</span><span>=</span><span>True</span><span>, </span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>tokenizer</span><span>(test_sentences, </span></span><span>max_length</span><span>=</span><span>512</span><span>, </span><span>padding</span><span>=</span><span>'max_length'</span><span>, </span><span>truncation</span><span>=</span><span>True</span><span>, </span><span>return_tensors</span><span>=</span><span>'pt'</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输入如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{'input_ids': tensor([[ 101, 2651, 2003, 2025, 2008, 2919,  102],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[ 101, 2651, 2003, 2061, 2919,  102,    0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0],</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1],</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>[1, 1, 1, 1, 1, 1, 0]])}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>{'input_ids': tensor([[ 101, 2651, 2003,  ...,    0,    0,    0],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>[ 101, 2651, 2003,  ...,    0,    0,    0]]), 'token_type_ids': tensor([[0, 0, 0,  ..., 0, 0, 0],</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[0, 0, 0,  ..., 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1,  ..., 0, 0, 0],</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>[1, 1, 1,  ..., 0, 0, 0]])}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可见：</p><ul>
<li>当同时设置 <code>max_length=512</code> 和 <code>padding=True</code> 时，<code>max_length</code> 的设置实质上没有意义，因为 padding 会自动遍历所有句子，找到最长的长度，然后补 0 到那个长度。</li>
<li>当 <code>padding='max_length'</code> 时，会补 0 直到 512 位（<code>max_length</code> 的值）。</li>
</ul></section></section>
<section><h1>2. 调用模型<a href="#2-调用模型"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> torch</span></div></div><div><div><div>2</div></div><div><span>import</span><span> torch.nn.functional </span><span>as</span><span> F</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>torch.nn.functional</code>（通常简写为 F）是 PyTorch 中包含各种神经网络函数的子模块。它提供了<strong>无状态的函数式 API</strong>，比如：</p>

<table><thead><tr><th>函数</th><th>作用</th></tr></thead><tbody><tr><td><code>F.softmax()</code></td><td>把分数转成概率</td></tr><tr><td><code>F.relu()</code></td><td>激活函数</td></tr><tr><td><code>F.sigmoid()</code></td><td>激活函数，输出 0~1</td></tr><tr><td><code>F.cross_entropy()</code></td><td>计算分类任务的损失</td></tr><tr><td><code>F.dropout()</code></td><td>随机丢弃神经元，防止过拟合</td></tr><tr><td><code>F.conv2d()</code></td><td>二维卷积操作</td></tr><tr><td>……</td><td>几十种常用函数</td></tr></tbody></table><p>和 <code>torch.nn</code> 的区别：</p>

<table><thead><tr><th></th><th><code>torch.nn</code></th><th><code>torch.nn.functional</code></th></tr></thead><tbody><tr><td>使用方式</td><td><code>nn.Linear(in, out)</code> 创建对象</td><td><code>F.linear(x, w, b)</code> 直接调用函数</td></tr><tr><td>是否有参数</td><td>✅ 带可学习参数（权重、偏置）</td><td>❌ 无参数，参数需自己传入</td></tr><tr><td>例子</td><td><code>layer = nn.Linear(10, 5)</code> → <code>layer(x)</code></td><td><code>F.linear(x, weight, bias)</code></td></tr></tbody></table><p>类比：</p><ul>
<li><code>nn.Linear</code> 像一个带水箱的电热水壶——插上电就能用，它自己管理水（参数）</li>
<li><code>F.linear</code> 像一个燃气灶上的水壶——你得自己倒水进去（手动传参数）</li>
</ul><p><code>F.softmax()</code> 是无参数的函数——它只是做个数学变换，不需要学习任何东西，所以这里用 functional 版本非常合适。如果是需要训练的层（如线性层、卷积层），则用 <code>nn.Module</code> 版本。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model.config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DistilBertConfig {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>"activation": "gelu",</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>"architectures": [</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>"DistilBertForSequenceClassification"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>"attention_dropout": 0.1,</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>"bos_token_id": null,</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>"dim": 768,</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>"dropout": 0.1,</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>"dtype": "float32",</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>"eos_token_id": null,</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>"finetuning_task": "sst-2",</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>"hidden_dim": 3072,</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>"id2label": {</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>"0": "NEGATIVE",</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>"1": "POSITIVE"</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>"initializer_range": 0.02,</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>"label2id": {</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>"NEGATIVE": 0,</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>"POSITIVE": 1</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>"max_position_embeddings": 512,</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>"model_type": "distilbert",</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>"n_heads": 12,</span></div></div><div><div><div>26</div></div><div><span>...</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>"tie_weights_": true,</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>"tie_word_embeddings": true,</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>"transformers_version": "5.9.0",</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>"vocab_size": 30522</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>简单来说，<code>model.config</code> 会打印出模型的完整配置信息——它对应的是 checkpoint 中的 <code>config.json</code> 文件，描述了模型的架构设计和任务相关信息。</p><p>挑几个关键字段解释：</p>

<table><thead><tr><th>字段</th><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>model_type</code></td><td><code>"distilbert"</code></td><td>模型架构类型</td></tr><tr><td><code>n_layers</code></td><td><code>6</code></td><td>Transformer 层数（BERT base 是 12 层，DistilBERT 精简了一半）</td></tr><tr><td><code>n_heads</code></td><td><code>12</code></td><td>每层的注意力头数</td></tr><tr><td><code>dim</code></td><td><code>768</code></td><td>隐藏层的维度（每个 token 被编码成 768 维的向量）</td></tr><tr><td><code>hidden_dim</code></td><td><code>3072</code></td><td>前馈网络的中间层维度</td></tr><tr><td><code>vocab_size</code></td><td><code>30522</code></td><td>词表大小——和 tokenizer 的 vocab_size 一致</td></tr><tr><td><code>max_position_embeddings</code></td><td><code>512</code></td><td>模型能处理的最大 token 长度</td></tr><tr><td><code>activation</code></td><td><code>"gelu"</code></td><td>激活函数类型</td></tr><tr><td><code>finetuning_task</code></td><td><code>"sst-2"</code></td><td>在 SST-2（斯坦福情感分析数据集）上微调过</td></tr><tr><td><code>id2label</code></td><td><code>{"0": "NEGATIVE", "1": "POSITIVE"}</code></td><td>数字 ID 到标签名的映射</td></tr><tr><td><code>label2id</code></td><td><code>{"NEGATIVE": 0, "POSITIVE": 1}</code></td><td>标签名到数字 ID 的反向映射</td></tr></tbody></table><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with</span><span><span> torch.</span><span>no_grad</span><span>():</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>outputs </span><span>=</span><span> </span><span>model</span><span>(**batch_input)</span></div></div><div><div><div>3</div></div><div><span>    </span><span>print</span><span>(outputs)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>scores </span><span>=</span><span> F.</span><span>softmax</span><span>(outputs.logits, </span><span>dim</span><span>=</span><span>1</span><span>) </span><span># dim=1 对每行 softmax</span></div></div><div><div><div>5</div></div><div><span>    </span><span>print</span><span>(scores)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>labels </span><span>=</span><span> torch.</span><span>argmax</span><span>(scores, </span><span>dim</span><span>=</span><span>1</span><span>) </span><span># dim=1 对每行取最大值的索引</span></div></div><div><div><div>7</div></div><div><span>    </span><span>print</span><span>(labels)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>labels </span><span>=</span><span> [model.config.id2label[</span><span>id</span><span>] </span><span>for</span><span> </span><span>id</span><span> </span><span>in</span><span><span> labels.</span><span>tolist</span><span>()] </span></span><span># 把数字标签翻译成人类可读的文本标签</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(labels)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SequenceClassifierOutput(loss=None, logits=tensor([[-3.4620,  3.6118],</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>[ 4.7508, -3.7899]]), hidden_states=None, attentions=None)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>tensor([[8.4632e-04, 9.9915e-01],</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>[9.9980e-01, 1.9531e-04]])</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>tensor([1, 0])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>['POSITIVE', 'NEGATIVE']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出解析：</p><ul>
<li><code>loss=None</code> ：<strong>损失值</strong>。
<ul>
<li>训练时：这里会是一个数字，表示预测和真实标签之间的差距，用来反向传播更新参数</li>
<li>推理时：因为没有真实标签，所以 loss=None</li>
</ul>
</li>
<li><code>logits</code> ：<strong>模型的原始预测分数（最关键的部分）</strong>。这是模型最终的输出，形状是 <code>(batch_size=2, num_labels=2)</code>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                 </span></span><span>句子1               句子2</span></div></div><div><div><div>2</div></div><div><span><span>            </span></span><span>┌──────────────┐  ┌──────────────┐</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>│ 负面   正面   │  │  负面   正面  │</span></div></div><div><div><div>4</div></div><div><span>logits =  [[-3.4620, 3.6118],  [4.7508, -3.7899]]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>hidden_states=None</code> ：<strong>每一层的隐藏状态</strong>。
<ul>
<li>如果你在调用模型时传 <code>output_hidden_states=True</code>，这里会返回每一层 Transformer 的输出向量</li>
<li>默认为 None，不返回（节省内存）</li>
</ul>
</li>
<li><code>attentions=None</code> ：<strong>注意力权重</strong>。
<ul>
<li>如果你传 <code>output_attentions=True</code>，这里会返回每一层的注意力分数，可以可视化看模型关注了哪些词</li>
<li>默认为 None，不返回</li>
</ul>
</li>
</ul><p>代码解析：</p><ul>
<li><code>with</code> ：<strong>进入 with 块时开启某种模式，退出时自动恢复原状</strong>，省去手动开关的麻烦。在这里，我们进入 <code>with</code> 块时开启“不记录梯度”的模式，退出 <code>with</code> 后，恢复正常的梯度记录模式。</li>
<li><code>torch.no_grad()</code> ：这是 PyTorch 中的<strong>推理模式</strong>，关闭梯度计算，有两个好处：
<ul>
<li>节省内存（最主要）—— 不记录计算图，显存占用大幅降低，对长文本或大 batch 尤其明显；</li>
<li>加速计算 —— 省去了构建计算图和跟踪梯度的开销。</li>
</ul>
</li>
<li><code>**batch_input</code> ：<strong><code>**</code> 是字典解包（Dictionary Unpacking）运算符</strong>。我们已经知道 <code>batch_input</code> 是一个字典，<code>model(**batch_input)</code> 中的 <code>**</code> 会把字典展开，等价于：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 不要执行此 cell</span></div></div><div><div><div>2</div></div><div><span><span>model</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>input_ids</span><span><span>=</span><span>tensor</span><span>([[</span><span>...</span><span>], [</span><span>...</span><span>]]),</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>token_type_ids</span><span><span>=</span><span>tensor</span><span>([[</span><span>...</span><span>], [</span><span>...</span><span>]]),</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>attention_mask</span><span><span>=</span><span>tensor</span><span>([[</span><span>...</span><span>], [</span><span>...</span><span>]])</span></span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以，<code>model(**batch_input)</code> 就是把 tokenizer 准备好的 3 个张量，作为关键字参数一次性传给模型，让模型进行前向计算。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/test/02_%E6%96%87%E7%AB%A0%E6%A8%A1%E6%9D%BF/</id>
      <title type="text">我的第一篇文章</title>
      <published>2025-01-01T00:00:00.000Z</published>
      <updated>2025-01-01T00:00:00.000Z</updated>
      <author><name>你的名字</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/test/02_%E6%96%87%E7%AB%A0%E6%A8%A1%E6%9D%BF/"/>
      <summary type="text">这是文章的简短描述</summary>
      <content type="html"><![CDATA[<section><h1>我的第一篇文章<a href="#我的第一篇文章"><span>#</span></a></h1><p>在这里开始写正文。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://hac.he.cn/posts/test/theorem-11/</id>
      <title type="text">测试文章</title>
      <published>2025-01-01T00:00:00.000Z</published>
      <updated>2025-01-01T00:00:00.000Z</updated>
      <author><name>HAC</name></author>
      <link rel="alternate" href="https://hac.he.cn/posts/test/theorem-11/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<p>该定理指出了极值点和不同阶导数之间的关系。</p>
<p>If <span><span>f(k)(x∗)=0f^{(k)}(x^*)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>k</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> for <span><span>k=1,2,...,nk=1,2,...,n</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>...</span><span>,</span><span></span><span>n</span></span></span></span>, <span><span>f(n+1)(x∗)≠0f^{(n+1)}(x^*)\neq 0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span>, and <span><span>f(n+1)(x)f^{(n+1)}(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> is continuous in a neighborhood of <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>, then <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> is a local extremum <span><span>iffn+1iff\enspace n+1</span><span><span><span></span><span>i</span><span>f</span><span>f</span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> is even.</p>
<p>Furthermore, for even <span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span>, if <span><span>f(n+1)(x∗)&gt;0f^{(n+1)}(x^*)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>, then <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> is a local minimum; if <span><span>f(n+1)(x∗)&lt;0f^{(n+1)}(x^*)&lt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span>, then <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> is a local maximum.</p>
<section><h1>通俗理解<a href="#通俗理解"><span>#</span></a></h1><p>该定理处理的是这样一种特殊情况：你找到了一个临界点 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> ，即一阶导数 <span><span>f′(x∗)=0f'(x^*)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> ，但当你用传统的二阶导数测试时，发现二阶导数竟然也是零，即 <span><span>f′′(x∗)=0f''(x^*)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 。</p><p>这时你该怎么办？这个定理告诉你，不要放弃，继续求更高阶的导数，直到找到一个​<strong>​不为零的导数​</strong>​。这个第一个不为零的导数的​<strong>​阶数​</strong>​ (<span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span>) 和它的​<strong>​正负号​</strong>​，将共同决定这个临界点的性质。</p><p>想象我们正开车沿着一条奇怪的函数曲线道路行驶：</p><ol>
<li><span><span>f′(x∗)=0f'(x^*)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你在 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 这个点停下了车，这是一个潜在的山顶或山谷底。</li>
<li><span><span>f′′(x∗)=0f''(x^*)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你本想看看车头的朝向（二阶导数）来判断是山顶还是谷底。但发现车头既没有朝上也没有朝下，它​<strong>​正好是水平的​</strong>​！传统的二阶导数测试法失效了。</li>
<li><span><span>f′′′(x∗)=0,...,fn(x∗)=0f'''(x^*)=0,...,f^n(x^*)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′′</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span>...</span><span>,</span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你继续检查更高阶的“姿态”。三阶、四阶…直到第 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶导数都显示车是“水平”的。</li>
<li><span><span>fn+1(x∗)≠0f^{n+1}(x^*)\neq 0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> ：终于，在第 <span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶导数，你发现了一个明确的信号！车终于不再是“水平”状态了，它有了一个明确的“倾斜”趋势。</li>
</ol><p>现在来到了问题的关键，<span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 是<strong>奇数还是偶数</strong>？</p><ul>
<li>如果是奇数，这意味着车的“倾斜”趋势是<strong>单方向</strong>，也就是说，你停车的点只是一个拐点，并不是极值点。</li>
<li>如果是偶数，这意味着车的“倾斜”趋势是<strong>对称的</strong>，说明你确实停在一个极值点上了。</li>
</ul><div></div><div>如何理解单方向和对称？</div><div></div><div><p>核心是泰勒展开，我们知道，函数 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span> 在点 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 附近的行为可以用一个多项式来近似。根据定理，前 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶导数都为零，所以函数在 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 附近的变化主要由第一个非零导数——即第 <span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶导数来决定。</p><p>近似公式为</p><span><span><span>f(x)≈f(x∗)+f(n+1)(x∗)(n+1)!(x−x∗)n+1f(x)\approx f(x^*)+\frac{f^{(n+1)}(x^*)}{(n+1)!}(x-x^*)^{n+1}</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>n</span><span></span><span>+</span><span></span><span>1</span><span>)!</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>这里，<span><span>(x−x∗)n+1(x-x^*)^{n+1}</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的奇偶性决定了函数在 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 左右两侧的行为。</p><p>当 <span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 是奇数时，<span><span>(x−x∗)n+1(x-x^*)^{n+1}</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 是一个奇函数，这意味着：
如果 <span><span>x&gt;x∗x&gt;x^*</span><span><span><span></span><span>x</span><span></span><span>&gt;</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> ，即 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 在 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 右侧，则 <span><span>(x−x∗)n+1&gt;0(x-x^*)^{n+1}&gt;0</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ；
如果 <span><span>x&lt;x∗x&lt;x^*</span><span><span><span></span><span>x</span><span></span><span>&lt;</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> ，即 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 在 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 左侧，则 <span><span>(x−x∗)n+1&lt;0(x-x^*)^{n+1}&lt;0</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ；</p><p>结合导数符号，假设 <span><span>f(n+1)(x∗)&gt;0f^{(n+1)}(x^*)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> （负数的情况类似）：
在右侧（<span><span>x&gt;x∗x&gt;x^*</span><span><span><span></span><span>x</span><span></span><span>&gt;</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>），<span><span>f(x∗)+f(n+1)(x∗)(n+1)!(x−x∗)n+1&gt;0f(x^*)+\frac{f^{(n+1)}(x^*)}{(n+1)!}(x-x^*)^{n+1}&gt;0</span><span><span><span></span><span>f</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)!</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ，所以 <span><span>f(x)&gt;f(x∗)f(x)&gt;f(x^*)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span></span></span></span> ；
在左侧（<span><span>x&lt;x∗x&lt;x^*</span><span><span><span></span><span>x</span><span></span><span>&lt;</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>），<span><span>f(x∗)+f(n+1)(x∗)(n+1)!(x−x∗)n+1&lt;0f(x^*)+\frac{f^{(n+1)}(x^*)}{(n+1)!}(x-x^*)^{n+1}&lt;0</span><span><span><span></span><span>f</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)!</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>+</span><span>1</span><span>)</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ，所以 <span><span>f(x)&lt;f(x∗)f(x)&lt;f(x^*)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span></span></span></span> ；</p><p>这意味着函数左右两侧的单调性相同，不是左右对称的，这就是<strong>单方向</strong>的含义。</p><p>同理，如果 <span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 是偶数，<span><span>(x−x∗)n+1(x-x^*)^{n+1}</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 就是一个偶函数，函数两侧的单调性不同，这就是<strong>对称</strong>的含义。</p></div><p>那么是哪种极值点？这就由 <span><span>fn+1(x∗)f^{n+1}(x^*)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 的<strong>正负号</strong>决定。</p><p>如果 <span><span>fn+1(x∗)&gt;0f^{n+1}(x^*)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ，这意味着车头开始翘起，准备进入一个上坡路段，所以你现在就在谷底，因此 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 是一个<strong>极小值点</strong>；
如果 <span><span>fn+1(x∗)&lt;0f^{n+1}(x^*)&lt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>n</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ，这意味着车头开始低下，准备进入一个下坡路段，所以你现在就在山顶，因此 <span><span>x∗x^*</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span> 是一个<strong>极大值点</strong>；</p><p>简而言之，上述定理可以<strong>概述为两句话</strong>：</p><ul>
<li>前奇数阶导数均为零，某偶数阶导数不得零则是极值点。</li>
<li>前偶数阶导数均为零，某奇数阶导数不得零则是拐点。</li>
</ul><section><h2>高阶导数的意义<a href="#高阶导数的意义"><span>#</span></a></h2><p>高阶导数并不仅仅是数学符号，它们实际上分别描述了函数行为中一层又一层精妙的“变化的变化”。</p><p>我们继续用一个经典的比喻来贯穿始终：想象你正在开车，而函数 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span> 描述的是你的车在时间 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 时行驶的距离。</p><section><h3>什么是一阶导数<a href="#什么是一阶导数"><span>#</span></a></h3><p>一阶导数反映的是你的<strong>瞬时速度</strong>，它告诉你在某一时刻，你开得有多快（导数的大小），以及你的方向（正负号）。</p><ul>
<li><span><span>f′(x)&gt;0f'(x)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你在向前开，即位置 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span> 在增加；</li>
<li><span><span>f′(x)&lt;0f'(x)&lt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你在向后开，即位置 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span> 在减少；</li>
<li><span><span>f′(x)=0f'(x)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你瞬间停止，车停了。</li>
</ul><p><span><span>f′(x)f'(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> 越大，你在时间 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 时的速度就越大；反之就越小。</p><p>总之，一阶导数回答的问题是：它正在变化吗？有多快？什么方向？</p></section><section><h3>什么是高阶导数<a href="#什么是高阶导数"><span>#</span></a></h3><p>二阶导数就是一阶导数的导数，它直接告诉你速度是如何变化的，也就是所谓的<strong>加速度</strong>：</p><ul>
<li><span><span>f′′(x)&gt;0f''(x)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你正在踩油门，即速度 <span><span>f′(x)f'(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> 在增加；</li>
<li><span><span>f′′(x)&lt;0f''(x)&lt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ：你正在踩刹车，即速度 <span><span>f′(x)f'(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> 在减少；</li>
<li><span><span>f′′(x)=0f''(x)=0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> ：速度 <span><span>f′(x)f'(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> 在这一瞬间没有变化，可能是匀速，也可能是速度变化的转折点。</li>
</ul><p>而对于原函数，二阶导数揭示了函数的<strong>弯曲方向</strong>或<strong>凹凸性</strong>：</p><ul>
<li><span><span>f′′(x)&gt;0f''(x)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ：原函数是“凹下”的，像一个大碗的形状 <span><span>∪\cup</span><span><span><span></span><span>∪</span></span></span></span> ，这意味着函数在该点的切线始终位于函数曲线​<strong>​下方​</strong>​。在开车比喻中，即使你在刹车（速度 <span><span>f′(x)f'(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> 在减），但如果加速度是正的（<span><span>f′′(x)&gt;0f''(x)&gt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>），意味着你刹车的力度在变小，车即将重新加速；</li>
<li><span><span>f′′(x)&lt;0f''(x)&lt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> ：原函数是“凸起”的，像一个小山的形状 <span><span>∩\cap</span><span><span><span></span><span>∩</span></span></span></span> ，这意味着函数在该点的切线始终位于函数曲线​<strong>​上方​</strong>​。在开车比喻中，即使你在加速（速度 <span><span>f′(x)f'(x)</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span></span></span></span> 在增加），但如果加速度是负的（<span><span>f′′(x)&lt;0f''(x)&lt;0</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span>)</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span>），意味着你踩油门的力度在变小，车即将重新减速；</li>
</ul><hr /><p>三阶及更高阶的导数则代表了更精细的描述：</p><p>对于三阶导数，它是二阶导数的导数，描述<strong>加速度</strong>是如何变化的；另一方面，从原函数的角度来说，三阶导数描述了函数<strong>凹凸性的变化率</strong>，它告诉你函数从凹下变为凸起（或反之）的过程是突然的还是渐进的。</p><p>而更高阶导数则描述了更复杂的弯曲。</p><hr /><p>最终，所有这些高阶导数的意义都凝聚在<strong>泰勒展开</strong>这个强大的工具中：</p><p>泰勒公式告诉我们，函数在一点附近的行为，可以由它在这一点各阶导数的信息完美地重构出来。第一个非零的高阶导数，就像泰勒级数中的“主导项”，决定了函数在无穷小邻域内的最主要形状。这就是 Theorem 1.1 成立的深层原因。</p></section></section></section>]]></content>
    </entry>
</feed>
