<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆</title>
	<atom:link href="https://blog.gmem.cc/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Thu, 16 Apr 2026 11:50:59 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>人工智能理论知识 - 智能体</title>
		<link>https://blog.gmem.cc/ai-knowledge-quick-ref-4</link>
		<comments>https://blog.gmem.cc/ai-knowledge-quick-ref-4#comments</comments>
		<pubDate>Wed, 15 Apr 2026 12:47:13 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=42155</guid>
		<description><![CDATA[<p>这一篇处理模型之外的系统层问题，包括上下文工程、Harness Engineering、检索增强生成（RAG）与智能体。前一篇讲的是模型本体及其训练和推理，这一篇转向“模型如何被放进真实系统里工作”：上下文如何组织，工具与知识库如何接入，验证闭环如何建立，以及多步推理与多智能体协作如何落地。 上下文工程（Context Engineering） 从提示词工程到上下文工程 提示词工程（Prompt Engineering）最早强调的是“怎么写一句更有效的话”，关注点集中在措辞、顺序、示例和限制条件。随着大模型应用从单轮问答扩展到智能体（AI Agent）、RAG、多工具调用与长会话系统，工程重点逐渐转向上下文工程（Context Engineering）：真正决定效果的，不再只是某一句提示词本身，而是系统提示、用户输入、示例、检索结果、对话历史、工具返回、结构化状态与输出约束如何被整体组织成一次模型调用。 因此，上下文工程可以看作比提示词工程更大的外层概念。提示词仍然重要，但它只是上下文中的一个组件；在生产系统里，更关键的问题通常是“哪些信息应该进入上下文、以什么顺序进入、保留多久、如何压缩、何时替换、如何约束输出”。这一变化反映的不是术语时尚，而是应用形态已经从“写一句 prompt”演进为“设计一套输入装配系统”。 上下文学习 上下文学习（In-context Learning）指模型在不更新参数的前提下，仅凭当前输入中的任务说明、示例与约束完成新任务。它依赖的是上下文中的条件信息，而不是额外微调。 零样本（Zero-shot） 零样本（Zero-shot）是在不给示例的情况下，直接通过任务描述让模型完成目标。例如“判断以下影评是正面还是负面，只输出标签”。它的优点是成本低、迁移快；缺点是任务边界一旦含糊，模型更容易自由发挥。 少样本（Few-shot） 少样本（Few-shot）是在提示词中附带少量示例，让模型在当前上下文里归纳输入输出模式。它特别适合标签定义不够直观、格式要求严格或任务带有领域习惯的场景。示例的价值不只是“给答案”，更是显式定义任务边界、异常情况与输出风格。 提示词结构 一个可维护的提示词通常由若干功能块组合而成，而不是一段散乱文本。把这些块拆开，能够显著提高复用性、可调试性与一致性。 角色定位 角色定位（Role）定义模型在本次任务中的身份与职责边界，例如“你是严谨的法律信息抽取器”或“你是面向儿童解释科学概念的讲解者”。它的作用是设定默认行为策略，而不是替代具体任务指令。 <a class="read-more" href="https://blog.gmem.cc/ai-knowledge-quick-ref-4">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-4">人工智能理论知识 - 智能体</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>这一篇处理模型之外的系统层问题，包括上下文工程、Harness Engineering、检索增强生成（RAG）与智能体。前一篇讲的是模型本体及其训练和推理，这一篇转向“模型如何被放进真实系统里工作”：上下文如何组织，工具与知识库如何接入，验证闭环如何建立，以及多步推理与多智能体协作如何落地。</p>
<div class="blog_h1"><span class="graybg">上下文工程（Context Engineering）</span></div>
<div class="blog_h2"><span class="graybg">从提示词工程到上下文工程</span></div>
<p>提示词工程（Prompt Engineering）最早强调的是“怎么写一句更有效的话”，关注点集中在措辞、顺序、示例和限制条件。随着大模型应用从单轮问答扩展到智能体（AI Agent）、RAG、多工具调用与长会话系统，工程重点逐渐转向上下文工程（Context Engineering）：真正决定效果的，不再只是某一句提示词本身，而是<span style="background-color: #c0c0c0;">系统提示、用户输入、示例、检索结果、对话历史、工具返回、结构化状态与输出约束如何被整体组织成一次模型调用</span>。</p>
<p>因此，上下文工程可以看作比提示词工程更大的外层概念。提示词仍然重要，但它只是上下文中的一个组件；在生产系统里，更关键的问题通常是“哪些信息应该进入上下文、以什么顺序进入、保留多久、如何压缩、何时替换、如何约束输出”。这一变化反映的不是术语时尚，而是应用形态已经从“写一句 prompt”演进为“设计一套输入装配系统”。</p>
<div class="blog_h2"><span class="graybg">上下文学习</span></div>
<p>上下文学习（In-context Learning）指模型在不更新参数的前提下，仅凭当前输入中的任务说明、示例与约束完成新任务。它依赖的是上下文中的条件信息，而不是额外微调。</p>
<div class="blog_h3"><span class="graybg">零样本（Zero-shot）</span></div>
<p>零样本（Zero-shot）是在不给示例的情况下，直接通过任务描述让模型完成目标。例如“判断以下影评是正面还是负面，只输出标签”。它的优点是成本低、迁移快；缺点是任务边界一旦含糊，模型更容易自由发挥。</p>
<div class="blog_h3"><span class="graybg">少样本（Few-shot）</span></div>
<p>少样本（Few-shot）是在提示词中附带少量示例，让模型在当前上下文里归纳输入输出模式。它特别适合标签定义不够直观、格式要求严格或任务带有领域习惯的场景。示例的价值不只是“给答案”，更是显式定义任务边界、异常情况与输出风格。</p>
<div class="blog_h2"><span class="graybg">提示词结构</span></div>
<p>一个可维护的提示词通常由若干功能块组合而成，而不是一段散乱文本。把这些块拆开，能够显著提高复用性、可调试性与一致性。</p>
<div class="blog_h3"><span class="graybg">角色定位</span></div>
<p>角色定位（Role）定义模型在本次任务中的身份与职责边界，例如“你是严谨的法律信息抽取器”或“你是面向儿童解释科学概念的讲解者”。它的作用是设定默认行为策略，而不是替代具体任务指令。</p>
<div class="blog_h3"><span class="graybg">指令</span></div>
<p>指令（Instruction）直接说明模型要完成什么任务，是提示词中最核心的控制块。好的指令应当明确目标、操作步骤、禁止事项与完成标准，避免把多个含糊目标混在同一层表达里。</p>
<div class="blog_h3"><span class="graybg">上下文</span></div>
<p>上下文（Context）提供完成任务所需的背景信息，例如产品文档、会话历史、用户约束、检索片段或前一步工具返回。它回答的是“模型基于什么信息工作”，而不是“模型该怎么工作”。</p>
<div class="blog_h3"><span class="graybg">输出格式</span></div>
<p>输出格式（Output Format）规定结果应该长什么样，例如纯文本、项目符号、JSON、表格或固定字段。格式要求越明确，后续系统越容易解析、验证与链接到其它模块。</p>
<div class="blog_h3"><span class="graybg">受众</span></div>
<p>受众（Audience）决定解释深度、术语密度与默认背景知识。例如 ELI5（Explain Like I'm 5）指面向完全没有背景知识的读者，需要更浅白的表达；面向资深工程师时，则应保留专业术语、边界条件与实现细节。</p>
<div class="blog_h3"><span class="graybg">语气</span></div>
<p>语气（Tone）控制表达风格，例如正式、简洁、鼓励式、审慎式或客服式。它影响的是话语风格和风险表达方式，但不应改变事实本身或任务判断标准。</p>
<div class="blog_h3"><span class="graybg">数据</span></div>
<p>数据（Data）指与任务本身直接相关的材料，例如待分类文本、待总结文档、用户表单、日志片段、表格记录或检索回来的证据。很多提示词失败，根源在于真正决定任务结果的数据并没有进入上下文。</p>
<div class="blog_h2"><span class="graybg">上下文构建</span></div>
<div class="blog_h3"><span class="graybg">系统提示（System Prompt）</span></div>
<p>系统提示（System Prompt）是整个上下文的最高层行为约束，通常用于定义角色、原则、边界和长期稳定规则。它应该稳定、简短且高信号，承载通用策略，而不适合塞入频繁变化的任务细节。</p>
<div class="blog_h3"><span class="graybg">对话历史与记忆</span></div>
<p>多数大模型 API 是无状态（Stateless）的：模型在一次调用中只“看到”你发送的输入（Prompt），不会自动记住上一次调用的对话内容。因此对话应用通常需要把对话历史（Conversation History）连同当前用户输入一起发给模型。</p>
<p>这会带来两个直接后果：</p>
<ul>
<li>token 占用会随着历史增长而上升（直到触达上下文窗口上限）。</li>
<li>并非“发得越多越好”：冗余历史会稀释注意力、提高成本，并可能引入过时或冲突信息。</li>
</ul>
<p>工程上常见的记忆（Memory）分层是：</p>
<ul>
<li>短期记忆（Short-term Memory）：保留最近 N 轮原文对话，确保局部连贯。</li>
<li>工作记忆（Working Memory）：把对话状态压缩成结构化摘要，例如用户偏好、已确认事实与当前任务约束。</li>
<li>长期记忆（Long-term Memory）：把历史片段写入外部存储（向量库/数据库），需要时检索回填。</li>
</ul>
<div class="blog_h3"><span class="graybg">工具调用上下文</span></div>
<p>工具调用上下文（Tool Context）指模型在调用搜索、数据库、代码执行器或业务 API 后得到的中间结果。它的关键不只是“把结果贴回模型”，而是把工具返回整理成模型真正可用的状态：保留必要字段、去掉噪声、标明来源与时间，并避免把原始日志整段塞进上下文。</p>
<div class="blog_h3"><span class="graybg">渐进式披露</span></div>
<p>渐进式披露（Progressive Disclosure）强调：不要在一开始把所有文档、规则、工具说明和历史对话一次性塞进上下文，而应只注入当前步骤真正需要的那一层信息。模型先看到最小可行上下文，只有当任务推进到下一层时，才继续展开更具体的约束、领域知识或执行细节。</p>
<p>这样做的原因不只是节省 token，更是为了保持推理质量。多组工程实践都观察到，上下文利用率一旦超过大约 40%，模型就可能从“聚焦求解”进入“信息过载”状态，开始出现幻觉、循环、格式错误或低质量实现。渐进式披露的目标，就是让 Agent 长时间停留在这个甜蜜区，而不是被无关材料拖进噪声区。</p>
<div class="blog_h2"><span class="graybg">上下文窗口管理</span></div>
<div class="blog_h3"><span class="graybg">Token 预算</span></div>
<p>Token 预算（Token Budget）是把上下文窗口（Context Window）当作一种稀缺资源来管理：一次请求的输入 token 与输出 token 都要计入模型的最大长度限制，并直接影响延迟与成本。</p>
<p>经验做法是把 prompt 切成可控的几块：系统提示尽量短且稳定；对话历史只保留近期原文；把长期信息通过检索（Retrieval）按需注入。</p>
<div class="blog_h3"><span class="graybg">压缩与摘要</span></div>
<p>压缩与摘要（Compression &amp; Summarization）的核心不是把文本“变短”，而是把信息从逐字转录（Transcript）变成可被模型继续使用的状态（State）。常见策略：</p>
<ul>
<li>滚动摘要（Rolling Summary）：每轮对话后更新一段固定长度的当前状态。</li>
<li>层级摘要（Hierarchical Summarization）：长文先分块总结，再总结摘要。</li>
<li>结构化记忆（Structured Memory）：用 JSON 或表格保存关键字段（偏好、约束、已决策项），避免自然语言摘要漂移。</li>
</ul>
<p>摘要要可验证：优先保留可操作事实（约束、数值、名称、决策），少写主观评价。</p>
<div class="blog_h3"><span class="graybg">长文本处理策略</span></div>
<p>当最新一次对话依赖很久以前的信息时，最可靠的方法不是无限追加历史，而是检索增强：把历史切成片段并建立索引（向量索引、关键词索引、主题标签），在每次请求前先检索与当前问题最相关的片段，再把这些片段注入上下文。</p>
<p>在当前智能体开发里，最主流的组合通常是：<span style="background-color: #c0c0c0;">滑动窗口 + 滚动摘要 + 向量检索（RAG）</span>。早期的外部记忆网络（Memory Networks / Neural Turing Machine）更多是研究范式，工程落地上更常见的是向量数据库与检索管线。</p>
<div class="blog_h2"><span class="graybg">结构化输出</span></div>
<div class="blog_h3"><span class="graybg">JSON Schema 约束</span></div>
<p>JSON Schema 约束把“输出应该长什么样”前置为机器可验证的结构定义，例如字段名、字段类型、必填项、枚举值与嵌套关系。它的价值在于把格式控制从“模型尽量照做”提升到“系统可以检查对不对”，从而显著降低后处理复杂度。</p>
<div class="blog_h3"><span class="graybg">函数调用（Function Calling）</span></div>
<p>函数调用（Function Calling）把模型输出从自由文本转成“选择哪个工具、填写哪些参数”的结构化决策。它的工作方式是让模型先生成一个可被程序消费的调用意图，再由外部系统负责权限校验、真实执行与结果回填。</p>
<div class="blog_h2"><span class="graybg">高级提示词策略</span></div>
<p>这里讨论的高级提示词策略，指的是仅通过提示词设计改变模型求解过程，而不借助额外框架、搜索控制器或推理编排器。它们本质上仍属于上下文工程：通过改变输入结构，让模型在一次或少数几次调用中显式展开中间推理步骤。</p>
<div class="blog_h3"><span class="graybg">思维链（Chain-of-Thought）</span></div>
<p>思维链（Chain-of-Thought, CoT）的核心做法是：在提示词中明确要求模型先分步分析，再给出最终答案。例如，可以把任务写成“先列出关键事实，再逐步推理，最后只输出结论”。若任务本身较复杂，也可以在 few-shot 示例里直接展示“问题 ➡ 分步推理 ➡ 最终答案”的格式，让模型在上下文中模仿这种求解模式。</p>
<p>在纯提示词使用方式下，CoT 的关键不是一句固定咒语，而是把推理过程结构化地写进输出要求。例如，可以把输出格式限定为“步骤 1 / 步骤 2 / 结论”，或要求模型先检查条件、再排除候选、最后汇总结论。这样做通常有利于多步算术、条件判断、规则推导与长链依赖任务；但对简单任务、严格结构化输出任务或成本敏感场景，强行要求展开思维链反而会增加 token 开销与噪声。</p>
<div class="blog_h3"><span class="graybg">思维树（Tree of Thoughts）</span></div>
<p>思维树（Tree of Thoughts, ToT）可以看作思维链的分支化版本：提示词不只要求模型给出一条线性推理路径，而是要求它先生成多个候选思路，再比较这些思路，最后选择更优的一条继续展开。在不借助框架的前提下，这种策略仍可以通过提示词实现，例如要求模型“先给出三个可能方案，分别说明优缺点，再选择最合理的方案给出最终答案”。</p>
<p>纯提示词版 ToT 的本质，是把“分支生成 + 分支比较 + 继续展开”都压进一次或少数几次模型调用里。它适合开放式规划、方案比较、复杂写作提纲和策略搜索这类任务，因为这类任务往往不存在唯一直接路径。代价同样明显：提示词更长，输出更长，模型也更容易在分支之间漂移。因此，ToT 更适合高价值、需要比较多个候选方案的任务，而不适合作为所有请求的默认模式。</p>
<div class="blog_h1"><span class="graybg">Harness Engineering</span></div>
<p>Harness Engineering（驾驭工程）研究的是：当模型推理与生成能力已经足够强时，决定 Agent 能否稳定完成复杂任务的，往往是模型外围的整套工作系统。Harness 指模型之外的所有代码、配置与执行逻辑——工具接口、状态管理、上下文装配、架构约束、验证机制、回滚策略与持续清理，全部属于这一层。裸模型只能接收输入、输出文本；只有当 Harness 为它提供状态、工具、约束和反馈回路后，它才能成为一个持续干活的 Agent。</p>
<p>目前 Harness Engineering 落地最广、讨论最充分的场景仍然是 Coding Agent，因此本章中的很多方法首先来自代码生成、调试、验证、跨会话交接和多 Agent 编排的工程实践。但它并不局限于写代码。凡是 Agent 需要长期持有状态、调用外部工具、在约束下执行任务、接受反馈并持续纠偏的场景，都可以应用同样的方法论，例如研究 Agent、数据分析 Agent、运维自动化 Agent、业务流程 Agent 以及多模态交互系统。随着 Agent 从“生成回答”走向“持续执行任务”，Harness Engineering 也会从 Coding Agent 的经验集合，扩展为更一般的 Agent 系统工程。</p>
<p>下文将沿着“为什么 Agent 会失效 ➡ 如何搭建控制面 ➡ 如何长期治理系统 ➡ 工程师角色如何变化”这条主线展开。</p>
<div class="blog_h2"><span class="graybg">三层模型和五大支柱</span></div>
<p>Prompt Engineering（提示词工程）、Context Engineering（上下文工程）与 Harness Engineering 构成了层层外扩的系统模型：前者决定如何表达任务，中间层决定模型能看到什么，最外层决定整个系统如何执行、纠偏与长期维持质量。</p>
<div class="blog_h3"><span class="graybg">三层模型</span></div>
<div class="blog_h4"><span class="graybg">提示词工程</span></div>
<p>提示词工程回答的是“如何把任务说清楚”。它关注指令措辞、角色设定、Few-shot 示例、输出格式和推理引导，目标是在单次调用里把任务表达得足够明确，让模型更容易走向期望行为。</p>
<div class="blog_h4"><span class="graybg">上下文工程</span></div>
<p>上下文工程回答的是“模型应该看到什么”。它关注记忆注入、检索回填、窗口压缩、状态摘要和工具返回整理，目标是控制输入信息的相关性与密度，使模型在有限上下文里获得完成当前任务所需的关键材料。</p>
<div class="blog_h4"><span class="graybg">Harness Engineering</span></div>
<p>Harness Engineering 回答的是“模型在什么系统里工作”。它处理的不再只是输入文本，而是工具接口、状态持久化、架构边界、验证回路、错误恢复、执行顺序与持续清理。到了这一层，关注点已经从“如何让模型回答得更好”转向“如何让 Agent 在真实系统中稳定完成工作”。</p>
<p>Harness 并不等同于某一句系统提示，也不等同于某个框架的名称。框架（Framework）、运行时（Runtime）和工具链都可以参与实现 Harness，但 Harness 这个词强调的是<span style="background-color: #c0c0c0;">模型外围整套工作系统</span>，而不是其中任何一个单独组件。</p>
<div class="blog_h3"><span class="graybg">五大支柱</span></div>
<p>从系统设计视角，一个可用的 harness 通常由五类承重件组成：上下文管理（Context Management）、工具编排（Tool Orchestration）、安全护栏（Safety Guardrails）、反馈回路（Feedback Loops）与可读性/可观测性（Legibility / Observability）。前两者解决“Agent 知道什么、能做什么”，中间两者解决“何时纠偏、如何避免越界”，最后一类解决“系统当前到底发生了什么”。</p>
<div class="blog_h4"><span class="graybg">上下文管理</span></div>
<p>上下文管理决定 Agent 在每一步究竟能看到什么信息。它的目标不是堆砌更多材料，而是把系统提示、短期记忆、长期知识、工具返回和任务状态按层组织，让模型始终工作在信息足够但不过载的区间。</p>
<div class="blog_h4"><span class="graybg">工具编排</span></div>
<p>工具编排决定 Agent 能做什么，以及如何把动作接回推理链路。搜索、数据库、代码执行器、浏览器自动化和业务 API 只有在权限、输入输出格式、调用顺序与结果回填都被设计清楚后，才会从“外挂能力”变成稳定的执行系统。</p>
<div class="blog_h4"><span class="graybg">安全护栏</span></div>
<p>安全护栏负责限制 Agent 的越界空间。它包括写权限边界、架构约束、审批节点、沙箱、敏感操作限制与结构化状态约束，作用是把“模型可能做错事”转化为“系统不允许它在关键位置随意犯错”。</p>
<div class="blog_h4"><span class="graybg">反馈回路</span></div>
<p>反馈回路负责告诉 Agent 当前结果究竟对不对。测试、lint、evaluator、Sprint Contract、健康检查和人工验收都属于这一层。没有反馈回路，Agent 只能凭语言流畅性误判成功；有了反馈回路，系统才能把错误重新送回执行链路中修复。</p>
<div class="blog_h4"><span class="graybg">可读性与可观测性</span></div>
<p>可读性与可观测性负责让 Agent 看见系统真实状态。UI、日志、指标、追踪、截图、DOM 快照和运行时事件，都是 Agent 判断“系统发生了什么”的依据。若系统对 Agent 不可见，很多问题即使存在，也只能靠盲目试错去碰。</p>
<p>Harness 的成熟度，本质上就是这五类控制面被工程化到什么程度。后文的各个部分，基本都可以看作对这五类支柱的展开：先看 Agent 为什么会系统性失效，再分别讨论上下文、编排、知识、约束、可读性、验证与熵控制如何把这些失效压回系统边界之内。</p>
<div class="blog_h2"><span class="graybg">长运行 Agent 的系统性失效</span></div>
<p>长运行 Agent（Long-Running Agent）必须跨多个上下文窗口持续工作，每个新会话在启动时对前一会话发生的事情没有任何记忆。即使使用最强的前沿模型，如果只给一个高级别的提示词，Agent 也无法稳定构建出生产质量的复杂系统。实践观察到四类典型失效：</p>
<ul>
<li><span style="background-color: #c0c0c0;">过早宣告完成（Premature Victory Declaration）</span>：在项目进行到一定阶段后，Agent 环视已完成的工作，宣告整个项目已经完成，忽略仍未实现的功能。</li>
<li><span style="background-color: #c0c0c0;">脏状态遗留（Dirty State Handoff）</span>：会话结束时遗留未修复的 bug 或未记录的进度，下一个会话必须先花大量 token 恢复工作环境，而不是推进新功能。</li>
<li><span style="background-color: #c0c0c0;">伪完成标记（False Completion）</span>：Agent 完成代码改动并运行单元测试或 curl 命令后，在没有进行端到端验证的情况下把功能标记为已完成，而该功能实际上并不能正常工作。</li>
<li><span style="background-color: #c0c0c0;">上下文焦虑（Context Anxiety）</span>：部分模型在上下文窗口填充到一定程度后，会主动收尾、提前结束任务——即便任务尚未完成，这种过早的"善后行为"会导致中途截断的工作状态。</li>
</ul>
<p>这四类失效都不能靠"换一个更好的模型"自动解决。它们是系统性问题，需要 Harness 层面的工程设计来对抗。</p>
<div class="blog_h3"><span class="graybg">失败驱动的 Harness 演化</span></div>
<p>Harness 是通过失败持续生长的控制层。一个重要的工程判断是：当 Agent 在某类任务上反复失败时，应先把失败当作 Harness 缺口的定位信号。失败可能意味着工具接口缺失、文档不可达、架构边界不清、状态交接不足，或验收标准仍停留在模糊自然语言层面。</p>
<p>因此，Harness Engineering 的关键动作是把失败外化成新的系统组件：增加脚本、补充文档、收紧 lint 规则、拆出更明确的 Contract、补上 evaluator 或可观测性接入。每次失败若都能回写为新的约束与支撑物，Agent 系统就会逐步从“靠经验驱动”演化为“靠机制驱动”。</p>
<div class="blog_h2"><span class="graybg">上下文管理策略</span></div>
<div class="blog_h3"><span class="graybg">上下文焦虑与两种应对策略</span></div>
<p>上下文焦虑（Context Anxiety）是指模型在感知到上下文窗口即将耗尽时，主动把任务包装成"已完成"状态——即便还有大量工作未做。Anthropic 在 Claude Sonnet 4.5 上观察到这一现象尤为明显。</p>
<p>应对上下文耗尽有两种互相对立的策略：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">策略</td>
<td style="text-align: center;">机制</td>
<td style="text-align: center;">保留连续性</td>
<td style="text-align: center;">消除上下文焦虑</td>
<td style="text-align: center;">代价</td>
</tr>
</thead>
<tbody>
<tr>
<td>压缩（Compaction）</td>
<td>将早期对话摘要替换原文，同一 Agent 继续运行</td>
<td>是</td>
<td>否（焦虑可能持续）</td>
<td>摘要信息损失</td>
</tr>
<tr>
<td>重置（Context Reset）</td>
<td>清空上下文，新 Agent 从结构化交接物（Handoff Artifact）重启</td>
<td>否（全新起点）</td>
<td>是</td>
<td>需要构造高质量交接物；增加编排复杂度与延迟</td>
</tr>
</tbody>
</table>
<p>对于 Sonnet 4.5，仅靠 Compaction 不足以支撑长任务，Context Reset 成为 Harness 设计的必要组件。随着 Opus 4.6 的推出，其长上下文检索能力显著提升、上下文焦虑基本消除，Context Reset 可以从 Harness 中移除——这正是 Harness 组件应随模型能力动态调整的典型示例。</p>
<p>一个很有价值的经验规律是：上下文窗口并不是“越满越好”。多组实践都观察到，随着无关文档、工具说明和历史对话不断堆积，Agent 的推理质量会先升后降。工程上可以把这理解为上下文利用率的“甜蜜区间”：模型需要足够信息才能稳定工作，但一旦被冗余材料淹没，就会出现幻觉、循环、格式错误或低质量实现。Harness 的任务不是把更多 token 塞进去，而是让 Agent 在任意时刻只看到当前步骤真正需要的信息。</p>
<div class="blog_h3"><span class="graybg">跨会话状态传递</span></div>
<p>长运行 Agent 在会话之间传递状态，不能依赖模型的内部记忆，必须依靠外化的持久化制品（Persistent Artifacts）。实践中形成了一套固定的状态传递套件：</p>
<ul>
<li><span style="background-color: #c0c0c0;">进度文件（Progress File）</span>：纯文本日志，记录每个会话完成了什么、遇到了什么问题、下一步是什么。每个会话开始时读取，结束时更新。</li>
<li><span style="background-color: #c0c0c0;">功能列表文件（Feature List）</span>：结构化的功能需求清单，初始全部标记为未完成，Agent 逐项实现并通过测试后方可标记通过。使用 JSON 而非 Markdown——实验发现模型不当覆写或修改 JSON 文件的概率远低于 Markdown 文件。</li>
<li><span style="background-color: #c0c0c0;">启动脚本（init.sh）</span>：由初始化代理预先写好的环境恢复入口，负责安装依赖、启动必要服务并打印访问入口。后续 fresh session 不再重新猜测项目如何运行，而是把它作为标准恢复脚本，在需要时检查并调用。</li>
<li><span style="background-color: #c0c0c0;">Git 历史</span>：每个会话以描述性的 commit message 结束。Git 历史既是进度时间线，也是恢复机制——Agent 可以通过 <pre class="crayon-plain-tag">git revert</pre> 从错误改动中恢复。</li>
</ul>
<p>这套设计借鉴的是人类软件工程师的轮班交接实践：进度文件对应交班笔记，git commit 对应有记录的工作移交，init.sh 对应标准化的环境搭建步骤，功能列表对应待办看板。</p>
<p>当单个 Agent 已经能够跨会话持续推进后，下一个问题就不再是“如何记住过去”，而是“如何把复杂任务拆给更合适的执行者”。这正是多 Agent 架构出现的背景。</p>
<div class="blog_h2"><span class="graybg">多 Agent 架构模式</span></div>
<div class="blog_h3"><span class="graybg">GAN 启发的 Generator–Evaluator 结构</span></div>
<p>让单个 Agent 对自己的输出进行评估会产生<span style="background-color: #c0c0c0;">自评偏差（Self-Evaluation Bias）</span>：即使输出质量明显一般，模型也倾向于给出积极评价。这一问题在主观任务上尤为突出，在可验证任务上同样存在。</p>
<p>一个有效的应对结构来自生成对抗网络（Generative Adversarial Network, GAN）的启发：将生成者和评估者分离成两个独立 Agent。</p>
<pre class="crayon-plain-tag">[Generator Agent] &larr;── feedback ──[Evaluator Agent]
        │                                 │
        └──────── output artifact ────────┘</pre>
<p>关键发现：调教一个独立的评估者使其保持怀疑态度，远比让生成者对自己的输出保持批判性更为可行。评估者仍然是 LLM，仍然有宽容倾向，但针对评估者进行专项提示调优的效果可以稳定收敛，而自评调优往往无效。</p>
<div class="blog_h3"><span class="graybg">Planner–Generator–Evaluator 三 Agent 架构</span></div>
<p>在 Generator–Evaluator 基础上增加一个 Planner Agent，构成完整的三 Agent 架构：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center; width: 15%;">Agent</td>
<td style="text-align: center; width: 20%;">输入</td>
<td style="text-align: center; width: 20%;">输出</td>
<td style="text-align: center;">设计要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Planner</td>
<td>1–4 句用户提示</td>
<td>完整产品规格书</td>
<td>只关注产品上下文与高层技术设计，不指定实现细节（过度具体的规格会将错误级联到下游）；被提示在规格中寻找机会融入 AI 功能</td>
</tr>
<tr>
<td>Generator</td>
<td>产品规格 + Sprint Contract</td>
<td>可运行代码</td>
<td>按 Sprint 增量构建（早期模型）或持续运行（强模型）；完成 Sprint 后先自评，再交 Evaluator 审查；拥有版本控制权限</td>
</tr>
<tr>
<td>Evaluator</td>
<td>运行中的应用 + Sprint Contract 验收标准</td>
<td>通过/失败判定 + 具体可行的问题报告</td>
<td>通过 Playwright MCP 实际操作运行中的应用；设有硬性阈值，任一标准未达到则 Sprint 失败并返回修复</td>
</tr>
</tbody>
</table>
<p>Agent 之间的通信完全基于文件：一个 Agent 写文件，另一个读文件并响应。这种方式简单、可追溯、不依赖框架内部状态。</p>
<div class="blog_h3"><span class="graybg">初始化 Agent 与编码 Agent 的分离</span></div>
<p>对于需要跨多个上下文窗口持续工作的场景，一种实用模式是将第一次会话与后续会话用不同的提示驱动：</p>
<ul>
<li><span style="background-color: #c0c0c0;">初始化 Agent（Initializer Agent）</span>：仅在第一个会话运行，负责构建整套脚手架：根据项目规格生成 init.sh、功能列表 JSON、初始 git commit 和进度文件，把“项目如何启动、功能如何追踪、状态如何恢复”一次性外化成可执行制品。</li>
<li><span style="background-color: #c0c0c0;">编码 Agent（Coding Agent）</span>：从第二个会话起运行，先读取进度文件、功能列表和最近的 git 历史，再按需调用 init.sh 恢复环境，每次只实现一个功能，并以"干净状态（Clean State）"结束会话。</li>
</ul>
<p>两个"Agent"在技术上是同一套系统提示和工具集，区别仅在于初始用户提示不同——这是一种提示策略，而非架构分离。</p>
<p>这种设计的关键，不是简单多了一个 shell 脚本，而是把“如何把项目跑起来”从高 token 成本的推断问题，转成低成本、可重复、可审计的脚本执行问题。后续每个 fresh session 的标准启动序列因此可以稳定下来：先用 <pre class="crayon-plain-tag">pwd</pre> 与最近的 <pre class="crayon-plain-tag">git log</pre> 确认当前工作状态，读取进度文件和功能列表定位下一项任务，再检查并在需要时调用 <pre class="crayon-plain-tag">init.sh</pre> 恢复依赖、服务和访问入口。模型不再反复猜测包管理器、启动顺序、端口和运行命令，上下文预算因此可以集中到真正的实现与验证上。</p>
<div class="blog_h3"><span class="graybg">Sprint Contract：预协议的完成定义</span></div>
<p>Sprint Contract 是 Generator 与 Evaluator 在任何代码被写下之前，对"这一阶段完成的标准"达成共识的协议制品：Generator 提出将要构建什么以及如何验证成功，Evaluator 审查并确认，双方迭代直到达成一致。其目的是弥合高层用户故事与可测试实现之间的鸿沟，防止模糊需求在实现阶段演变为争议。</p>
<p>任务拆分解决了“谁来做”，但还没有解决“知识从哪里来、如何保持最新”。一旦 Agent 工作跨越多个会话、多个角色和多个代码域，知识组织方式本身就会成为 Harness 的一部分。</p>
<div class="blog_h2"><span class="graybg">知识库工程（Repository as System of Record）</span></div>
<div class="blog_h3"><span class="graybg">给 Agent 地图而非手册</span></div>
<p>“给 Agent 地图而非手册”讨论的是知识入口（Knowledge Entry Point）的设计原则：在大型、持续演化的代码库中，Agent 需要的是一套能够把它稳定引导到正确信息源的导航结构，而不是一份试图囊括所有细节的单体说明书。知识库工程的关键目标是让上下文可定位、可更新、可裁剪，而不是一次性塞满。</p>
<p>因此，这里的首要挑战是"如何让 Agent 在需要时找到正确信息"。"一个巨型 AGENTS.md 文件"的方案会失败，原因是多方面的：</p>
<ul>
<li>上下文是稀缺资源。一个巨大的指令文件会挤占任务本身、代码和相关文档的空间，导致 Agent 要么遗漏关键约束，要么对错误的目标进行优化。</li>
<li>过多的指导等于没有指导。当所有内容都被标记为"重要"时，Agent 会退化成局部模式匹配，而不是有目的地导航。</li>
<li>单体文档会迅速腐烂。人类停止维护，Agent 无法分辨哪些规则仍然有效，文件变成一个充满过期规则的吸引力陷阱。</li>
</ul>
<p>有效的替代方案是：<span style="background-color: #c0c0c0;">AGENTS.md 充当目录（Table of Contents），而非百科全书</span>。一份约 100 行的精简 AGENTS.md 作为上下文的入口，通过指针将 Agent 引导至结构化 docs/ 目录中的具体知识源。</p>
<p>AGENTS.md 还有一个更重要的角色：它不应是一次写完后长期冻结的静态文档，而应是失败驱动的活反馈循环。每当 Agent 因为命令使用错误、目录理解偏差、架构约束遗漏或工具接入方式不清而出错，最直接的修复方式往往就是把这类经验回写进 AGENTS.md 或其指向文档。这样，文档不再只是说明书，而是把历史失败压缩成未来会话可直接继承的系统记忆。</p>
<pre class="crayon-plain-tag">AGENTS.md            &larr; 约100行，地图角色
ARCHITECTURE.md
docs/
├── design-docs/
│   ├── index.md
│   └── core-beliefs.md
├── exec-plans/
│   ├── active/
│   └── completed/
├── generated/
│   └── db-schema.md
├── product-specs/
│   └── index.md
├── references/
│   ├── design-system-reference-llms.txt
│   └── ...
├── DESIGN.md
├── FRONTEND.md
├── PLANS.md
└── QUALITY_SCORE.md</pre>
<p>这种结构实现了渐进披露（Progressive Disclosure）：Agent 从一个小而稳定的入口开始，被引导到它需要的具体知识，而不是在启动时被所有信息淹没。</p>
<div class="blog_h3"><span class="graybg">组织级 Golden Path</span></div>
<p>当团队反复构建相似类型的系统时，Harness 不应只停留在单项目经验，而应上升为组织级的服务模板（Service Template）或黄金路径（Golden Path）。现实中的软件形态并不是无限多样的，常见项目通常集中在少数几类技术拓扑：前端应用、后端服务、数据流水线、内部工具。若能把每类拓扑常用的目录结构、启动脚本、验证流水线、可观测性接入、架构约束和 Agent 指令打包成模板，新项目就不必从零设计 Harness。</p>
<p>这类模板的价值不只是“脚手架复用”，更在于组织把高频工程判断沉淀成标准化控制面。团队成员在真实项目中学到的约束、回滚经验和验证方法，可以持续回流到模板；模板更新后，又会反过来提升后续所有项目的默认质量。Harness 一旦进入这个阶段，便不再只是某个工程师的个人技巧，而是组织级生产力资产。</p>
<div class="blog_h3"><span class="graybg">Agent 可见性边界</span></div>
<p>从 Agent 的视角来看，任何它在运行时无法在上下文中访问的知识，实际上不存在。存在于 Google Docs、Slack 线程或人脑中的决策，对 Agent 来说与从未发生过没有区别。唯一对 Agent 有效的知识是代码库中版本化的制品：代码、Markdown、Schema、可执行计划。</p>
<p>这一约束倒逼团队把越来越多的上下文推入代码库：对齐团队架构决策的 Slack 讨论、产品原则、工程规范，都需要以 Agent 可读的形式在仓库中存在。这不仅是文档实践，更是系统设计约束。</p>
<div class="blog_h3"><span class="graybg">机械化知识维护</span></div>
<p>知识库的有效性需要机械化强制执行，而不能依靠人工自律。专用的 linter 和 CI 任务验证知识库是否最新、交叉引用是否完整、结构是否正确。一个周期性运行的"文档园丁 Agent（doc-gardening agent）"扫描文档与实际代码行为之间的偏差，发现过期或不一致的内容后自动开启修复 Pull Request。</p>
<p>知识工程解决的是“找得到信息”，架构约束工程解决的是“即使知道该做什么，也不能随意乱做”。两者结合，Agent 才既有方向感，也有边界感。</p>
<div class="blog_h2"><span class="graybg">架构约束工程</span></div>
<div class="blog_h3"><span class="graybg">分层领域架构</span></div>
<p>Agent 在具有严格边界和可预测结构的环境中工作效果最好。在全 Agent 生成的代码库中，这一原则需要在工程层面提前落地：等到有数百名工程师时再引入架构规则，通常已经太晚了——而在 Agent 驱动的代码库中，混乱会以比人类团队快得多的速度蔓延。</p>
<p>一种行之有效的结构是<span style="background-color: #c0c0c0;">分层领域架构（Layered Domain Architecture）</span>：每个业务域被划分为固定的层集合，依赖方向被严格验证，仅允许有限的跨层边。例如，在一个业务域内部，代码只能沿固定顺序向前依赖（Types → Config → Repo → Service → Runtime → UI）；跨切面关注点（认证、连接器、遥测、特性标志）通过单一显式接口进入。其他所有依赖方向都被机械地禁止。</p>
<div class="blog_h3"><span class="graybg">机械化强制执行</span></div>
<p>架构规则通过定制 linter 和结构测试强制执行，而不是依赖文档和代码审查中的人工判断。关键实践：</p>
<ul>
<li>linter 的错误信息被设计成直接包含修复指令，从而将约束违反转化为 Agent 可消费的上下文。</li>
<li>在对人类团队而言显得迂腐的细粒度规则，对 Agent 来说是乘数效应：一旦编码，立即在所有代码上生效，无需逐一审查。</li>
<li>约束划定边界，边界内部的实现方式允许 Agent 自由选择——这类似于大型平台工程组织的管理模式：集中强制边界，局部授权自治。</li>
</ul>
<p>这套约束还包括"品味不变量（Taste Invariants）"：结构化日志、Schema 与类型命名规范、文件大小限制、平台特定的可靠性要求。这些规则用静态分析强制执行，将人类工程师的审美判断固化成可机器检查的规则。</p>
<div class="blog_h3"><span class="graybg">结构化状态与写权限约束</span></div>
<p>对 Agent 可以修改的内容施加精确的写权限，是防止状态腐蚀的重要手段。典型模式：功能列表文件中，Agent 只允许修改 <pre class="crayon-plain-tag">passes</pre> 字段，不得删除或改写任何测试条目。这种狭义写权限用强措辞的指令约束来传达："删除或编辑测试是不可接受的行为，这会导致功能遗漏或引入 bug。"</p>
<p>此外，在数据边界处解析数据形状（而不是 YOLO 式地推断数据结构），使用类型化 SDK 或 Schema 验证库，是保持 Agent 可以安全推理的代码库形态的基础约束。</p>
<p>边界清晰之后，系统仍然需要对 Agent “可见”。否则即使约束被写得很严，Agent 也只能在黑箱里试错，无法高效定位运行时问题。</p>
<div class="blog_h2"><span class="graybg">Agent 可读性（Legibility）</span></div>
<p>Agent 可读性（Agent Legibility）指的是：系统的 UI、日志、状态和指标对 Agent 来说是否足够可见、可解析、可操作，从而使模型能够直接观察应用行为、复现问题并验证修复结果。它关注的是系统是否为推理提供了足够清晰的外部反馈面。</p>
<p>随着代码吞吐量提升，人工 QA 容量会成为瓶颈。此时，关键路径不再是增加人力，而是让应用自身的 UI、日志和指标对 Agent 直接可读，使 Agent 能够自主复现 bug、验证修复并对应用行为进行推理。</p>
<div class="blog_h3"><span class="graybg">应用可读性</span></div>
<p>使应用对 Agent 可读的关键手段：</p>
<ul>
<li><span style="background-color: #c0c0c0;">每个 git worktree 独立启动一个应用实例</span>：Agent 可以针对自己的变更启动隔离的应用版本，互不干扰。</li>
<li><span style="background-color: #c0c0c0;">Chrome DevTools Protocol（CDP）接入</span>：将 CDP 接入 Agent 运行时，并封装操作 DOM 快照、截图和页面导航的技能，使 Agent 能够驱动浏览器、复现 bug 并验证修复，而不依赖人工截图传递。</li>
<li><span style="background-color: #c0c0c0;">Playwright/Puppeteer MCP</span>：在多 Agent 架构中，评估 Agent 使用浏览器自动化工具实际操作运行中的应用，而不是静态分析代码。这使得"应用看起来工作"与"应用实际可用"之间的差距得以被发现。需注意：浏览器原生弹窗（alert modal）对这类工具不可见，这是已知盲区。</li>
</ul>
<div class="blog_h3"><span class="graybg">可观测性栈</span></div>
<p>将完整的可观测性栈暴露给 Agent，使其可以查询日志、指标和追踪数据，是将模糊性能目标转化为 Agent 可执行任务的关键。每个 worktree 配备临时的本地可观测性栈（完成后自动销毁），Agent 可通过 LogQL 查询日志、PromQL 查询指标、TraceQL 查询链路追踪。</p>
<p>有了这种接入能力，"确保服务启动在 800ms 以内完成"或"这四条关键用户路径中没有 span 超过两秒"这类提示就变得可执行——Agent 可以直接查询数据、定位问题根源并实施修复，而不需要人工解读日志然后描述问题。</p>
<p>不过，看得见系统状态还不等于能正确判断任务是否完成。可读性回答的是“发生了什么”，验证闭环回答的是“这样是否算成功”。</p>
<div class="blog_h2"><span class="graybg">验证闭环</span></div>
<div class="blog_h3"><span class="graybg">主观评判的量化</span></div>
<p>对于没有二元正确性检验的任务（如界面设计、用户体验），需要将主观判断转化为具体的、可评分的标准，才能构建有效的验证闭环。一套已验证有效的前端设计评分框架包含四个维度：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">评估内容</td>
<td style="text-align: center;">权重</td>
</tr>
</thead>
<tbody>
<tr>
<td>Design Quality（设计质量）</td>
<td>配色、字体、布局、视觉意象是否融合为一个有清晰气质和独特身份的整体，而非各部分的简单堆砌</td>
<td>高</td>
</tr>
<tr>
<td>Originality（原创性）</td>
<td>设计决策是否经过定制，还是使用了模板布局、库默认值、AI 生成惯例模式（紫色渐变白色卡片、未经修改的 stock 组件）</td>
<td>高</td>
</tr>
<tr>
<td>Craft（工艺）</td>
<td>字体层级、间距一致性、颜色对比度等技术执行质量</td>
<td>低（模型默认已做得较好）</td>
</tr>
<tr>
<td>Functionality（功能性）</td>
<td>用户能否理解界面功能、找到主要操作、完成任务而无需猜测</td>
<td>低（模型默认已做得较好）</td>
</tr>
</tbody>
</table>
<p>重要发现：评估者提示中的措辞本身（如"最好的设计是博物馆级别的"）会直接影响生成器的输出风格——不仅仅在评估反馈阶段生效，在第一次生成时就已经开始引导模型远离通用默认值。评估者需通过 Few-shot 示例加详细评分解析进行校准，以保持判断一致性并减少分数漂移。</p>
<div class="blog_h3"><span class="graybg">端到端测试与健康检查门控</span></div>
<p>单元测试和 curl 命令验证的是代码片段，而不是用户体验。端到端测试的原则是：<span style="background-color: #c0c0c0;">像人类用户一样测试</span>——通过浏览器自动化工具实际操作运行中的应用，才能发现"代码逻辑看似正确但功能整体不可用"的问题。</p>
<p>实施健康检查门控（Health-Check Gate）：在每个会话开始时，Agent 必须先运行基础端到端功能测试，确认应用处于正常工作状态，再开始实现新功能。如果发现应用处于损坏状态，应先修复，否则新功能会在损坏的基础上叠加，问题会更难追溯。</p>
<p>干净状态（Clean State）的定义：每个会话结束时，代码库应处于可合并到主分支的状态——无重大 bug，代码有序且有文档，后续开发者（或 Agent）可以直接开始新功能，而无需先清理遗留的乱局。</p>
<div class="blog_h3"><span class="graybg">功能验证缺口</span></div>
<p>Harness 案例里常见一个容易被忽略的盲点：系统也许已经很好地约束了内部质量，却未必充分验证了外部行为是否真的符合产品意图。目录结构、架构 lint、单元测试、Clean State 和持续回收都很重要，但这些机制首先保证的是内部一致性，而不是最终功能一定可用。</p>
<p>因此，成熟的 Harness 必须把<span style="background-color: #c0c0c0;">功能验证（Functionality Verification）</span>视为独立承重件，而不是默认附属物。真正可靠的系统需要对“用户是否真的能完成任务”建立单独的验证回路，例如任务级验收标准、浏览器自动化、独立评估 Agent、人工 spot check，或基于运行时信号的行为检查。能持续写代码只是起点；能持续验证行为，才是 Harness 真正闭环的标志。</p>
<p>即使单次任务已经具备了约束、可读性和验证，长期运行后的系统仍会累积漂移。Harness 因此不仅要管每一轮执行是否正确，还要管整个代码库是否在时间尺度上持续失真。</p>
<div class="blog_h2"><span class="graybg">熵控制与垃圾回收</span></div>
<p>Agent 会复制代码库中已存在的模式——即使这些模式并不理想。随着时间推移，全 Agent 生成的代码库会发生不可避免的漂移：重复工具函数扩散、文档与代码行为不同步、架构规则被逐渐侵蚀。早期的应对方式是每周花费 20% 的团队时间手动清理，但这完全不可扩展。</p>
<p>有效的替代方案是将"黄金原则（Golden Principles）"直接编码进代码库，并建立周期性的自动化垃圾回收流程：</p>
<ul>
<li>在代码库中明确记录有主见的、机械可检查的原则（如：优先使用共享工具包而非手写 helper；必须在边界处解析数据形状，不得基于猜测的结构构建逻辑）。</li>
<li>周期性运行后台 Agent 任务，扫描原则违反、更新质量评分、开启针对性重构 Pull Request。</li>
<li>大多数这类 PR 可以在一分钟内完成审查并自动合并。</li>
</ul>
<p>技术债务与高息贷款相似：持续小额偿还几乎总是优于积累后集中清算。人类工程师的品味判断被捕获一次，随后在每一行代码上持续强制执行——这是 Agent 驱动开发模式下特有的杠杆效应。</p>
<p>但这些组件本身并不是一成不变的。任何垃圾回收策略、验证门控或多 Agent 编排，背后都隐含着对当前模型能力的判断；一旦模型变化，Harness 的承重结构也需要重新评估。</p>
<div class="blog_h2"><span class="graybg">Harness 随模型演化（Load-Bearing Analysis）</span></div>
<p>Harness 中的每一个组件都编码了一个假设：模型目前无法独立完成这件事，所以需要这层脚手架。这些假设有两个失效原因：</p>
<ul>
<li>假设从一开始就是错的——模型其实能做到，但工程师没有测试验证就加了脚手架。</li>
<li>假设因模型能力提升而过时——昨天需要 Sprint 分解的任务，今天的模型可以持续完成而不失去连贯性。</li>
</ul>
<p>因此，评估哪些 Harness 组件是真正的<span style="background-color: #c0c0c0;">承重结构（Load-Bearing Components）</span>——每当新模型发布时，剥除不再有效的部分，添加利用新能力的新部分——是 Harness 工程师持续的工作职责。</p>
<p>一个典型的演化轨迹：Claude Sonnet 4.5 需要 Context Reset + Sprint 分解 + 完整三 Agent 架构；Opus 4.5 在三 Agent 架构下表现更好，Sprint 结构依然必要；Opus 4.6 消除了上下文焦虑，Generator 可以持续运行超过两小时而无需 Sprint 分解，Evaluator 从"始终必要"变为"在任务超出模型原生能力时才有价值"。这套 Harness 对于 Sonnet 4.5 来说恰到好处，对于 Opus 4.6 来说则是过度设计。</p>
<p>一个重要的元原则：<span style="background-color: #c0c0c0;">随着模型能力提升，有趣的 Harness 组合空间不会缩小，而是移动</span>。工程师的工作不是等待模型强大到不需要 Harness，而是持续找到下一个新颖的组合。</p>
<p>一旦系统的主要难点从“写出代码”转移到“设计并维护这套控制面”，工程师的职责也会随之改变。</p>
<div class="blog_h2"><span class="graybg">工程师角色的演变</span></div>
<p>在 Harness Engineering 语境下，工程师的工作重心发生了系统性上移。原本直接产出代码的工作，转变为设计 Agent 工作的环境、指定意图与验收标准、构建使 Agent 能可靠工作的反馈闭环，以及将人类判断和品味持续编码进系统。</p>
<p>核心原则可以概括为一句话：<span style="background-color: #c0c0c0;">Humans steer, agents execute</span>。这意味着人类在更高的抽象层次上工作：确定优先级、将用户反馈转化为验收标准、验证结果。当 Agent 遇到障碍时，工程师更应追问"缺少什么能力？如何让这个需求变得对 Agent 可理解且可强制执行？"</p>
<p>随着代码吞吐量的提升，许多传统工程规范变得适得其反。PR 在高吞吐系统中应该是短命的；测试 flakiness 有时更适合通过后续补丁修复而非阻塞合并；纠错的成本极低，等待的成本极高。这与低吞吐人工团队的工作假设完全相反。</p>
<p>不过，这些方法目前验证得最充分的，仍然主要是绿地项目或受控实验环境。一旦进入历史包袱深重的老系统，Harness 的建设顺序与成本结构都会发生变化。</p>
<div class="blog_h2"><span class="graybg">棕地改造的难题</span></div>
<p>当前公开的成功 Harness 案例大多发生在绿地项目、受控实验或可以从零搭脚手架的环境中。真正更难的问题，是如何把 Harness Engineering 引入一个已经运行多年、缺少架构边界、测试质量不稳定、文档残缺且历史包袱沉重的棕地代码库。这里最大的风险不是 Agent 不会写代码，而是现有系统本身已经缺乏足够清晰的控制面，Agent 一旦接手，只会更快复制并放大原有混乱。</p>
<p>因此，棕地改造不能照搬绿地模板，通常需要渐进式引入：先补最关键的边界约束，再建立最小可用的知识入口、验证门控和可观测性接入，最后才考虑多 Agent 编排或更高自治级别。换言之，Harness 在棕地场景里的首要任务不是提升速度，而是先把代码库变成一个对 Agent 可理解、可约束、可验证的环境。</p>
<div class="blog_h2"><span class="graybg">参考案例</span></div>
<p>OpenAI Codex 内部实验（2026 年 2 月）：三名工程师在约五个月内，通过零人工手写代码的流程，构建了一个拥有内部日常用户和外部 alpha 测试用户的内部 beta 产品。代码库规模约一百万行，覆盖应用逻辑、测试、CI 配置、文档、可观测性和内部工具。约 1500 个 PR 被合并，平均吞吐为每名工程师每天 3.5 个 PR，且随团队扩张吞吐还在提升。核心投入集中在：精简的 AGENTS.md 目录结构、分层领域架构 + 机械化 linter、应用 + 可观测性对 Agent 的完整可读性、黄金原则与周期性垃圾回收 Agent。</p>
<p>Anthropic 复古游戏编辑器对比实验（2026 年）：使用 Claude Opus 4.5，对同一个 prompt "构建一个 2D 复古游戏制作工具"进行两种模式的比较。单 Agent 单次运行：20 分钟，花费 9 美元，结果是布局浪费空间、核心玩法无法运行。完整三 Agent Harness（Planner + Generator + Evaluator，含 Sprint Contract）：6 小时，花费 200 美元，结果是 Planner 将一句 prompt 扩展为横跨 10 个 Sprint 的 16 项功能规格，游戏核心机制正常运行，并额外实现了精灵动画、AI 辅助关卡设计等超出原始 prompt 的功能。成本约为单次运行的 22 倍，但输出质量远超。</p>
<p>Anthropic 浏览器 DAW 实验（2026 年）：使用 Claude Opus 4.6（已移除 Sprint 分解），通过 prompt "构建一个基于 Web Audio API 的全功能浏览器 DAW"，三轮构建 + QA 循环总计 3 小时 50 分钟，花费 124.70 美元。最终产物包含可用的编排视图、混音台和传输控制；内置 AI Agent 可通过自然语言提示设置节拍/调性、演奏旋律、建立鼓轨、调节混音和添加混响，完成一段完整的短曲创作。</p>
<div class="blog_h1"><span class="graybg">检索增强生成（RAG）</span></div>
<p>检索增强生成（Retrieval-Augmented Generation, RAG）把“外部知识”通过检索在推理时注入到上下文里：先检索，再生成。它解决的不是模型能力问题，而是信息可得性与可控性问题：把知识放在可更新的外部存储里，而不是指望模型参数记住一切。</p>
<p>真正落地到生产后，RAG 很快会从“模型 + 向量库”的玩具结构，变成一套完整的知识处理系统。一个更接近现实的抽象是：<span style="background-color: #c0c0c0;">知识源接入、文档获取、分块、增强、索引、召回、融合、重排、上下文回拼、生成</span>。其中任何一步做得粗糙，最终回答质量都会明显退化。</p>
<p>从工程视角看，RAG 的核心是把知识库建成一个可持续演化的外部记忆系统，而不是简单把文本切块后丢进向量数据库。这里通常至少会区分三层对象：知识库（Base）负责来源配置、召回策略与索引策略；文档（Document）负责内容文件、语言、状态与分块配置；块（Chunk）负责可检索的最小语义单元及其摘要、问题、向量、命中统计等元数据。再往上，很多系统还会加一个目录（Catalog）层，把知识按业务树、产品线、权限域或租户边界组织起来。这种分层的意义在于：<span style="background-color: #c0c0c0;">RAG 检索的不是“一个文本文件夹”，而是一个带治理、带生命周期、带配置继承的知识对象系统</span>。</p>
<div class="blog_h2"><span class="graybg">生产 RAG 的系统视角</span></div>
<p>一个成熟的知识库系统通常会显式区分知识源（Source）与检索目标（Target）。知识源回答“内容从哪里来”，例如上传文件、网页抓取、代码仓库、目录扫描；检索目标回答“内容最终由谁提供召回能力”，可以是系统自建索引，也可以是外部知识平台。这样设计的好处是把“接入来源”与“服务出口”解耦：同一套文档采集和治理流程，可以接到不同的检索后端，而不用把采集逻辑和向量库实现绑死。</p>
<p>另一个关键决策是把知识处理做成异步流水线，而不是同步上传即就绪。现实中的知识库往往需要经历扫描、下载、解析、分块、摘要生成、问题生成、向量化、索引构建等多个阶段。把这些阶段拆开，一方面是因为它们耗时、依赖不同资源且失败模式不同；另一方面是为了支持恢复、重试、并发控制与增量更新。于是，生产 RAG 往往天然带有一个状态机：知识库有自己的状态，文档有自己的状态，块也有自己的状态。这个状态机的作用是把长链路处理过程显式化，使每个阶段都能被观察、重跑和局部修复。</p>
<p>教程中的“上传文档 ➡ embedding ➡ 检索 ➡ 回答”只覆盖了最小闭环。真实系统里的知识库会持续新增、修改、下线、重建索引，并不断调整切分方式和召回参数。因此，RAG 更接近“搜索系统 + 知识处理中台 + 生成模型”的组合，而不是一次性的离线脚本。</p>
<div class="blog_h2"><span class="graybg">分块策略（Chunking）</span></div>
<p>分块（Chunking）决定了“检索的最小单位”。块太大：召回相关信息但携带大量噪声；块太小：召回片段零散，缺上下文导致生成不稳。分块应与文档结构、问题类型、上下文窗口预算一起设计。</p>
<div class="blog_h3"><span class="graybg">固定大小分块</span></div>
<p>固定大小分块（Fixed-size Chunking）按 token/字符数切片，简单稳定，适合结构较弱的纯文本与大规模离线构建。常配合重叠窗口（Overlap）避免跨边界信息断裂。</p>
<div class="blog_h3"><span class="graybg">语义分块</span></div>
<p>语义分块（Semantic Chunking）用段落/标题/语义边界切分，目标是让一个 chunk 自洽（Self-contained）。它通常更适合技术文档与带层级结构的内容，但需要更复杂的解析与规则。</p>
<div class="blog_h3"><span class="graybg">递归分块</span></div>
<p>递归分块（Recursive Chunking）先按大结构切（章节/标题），再对子块继续切（段落/句子/固定大小），兼顾结构与长度约束，是很多工程实现的折中方案。</p>
<p>生产系统里的分块通常远比这三类更细。除了固定大小、递归和语义切分，还会按 Markdown、HTML、PDF、代码、表格、JSON、LaTeX、句子、段落、滑动窗口等内容类型选择不同 chunker。这背后的设计判断非常重要：<span style="background-color: #c0c0c0;">分块不是一个通用算法参数，而是内容理解策略的一部分</span>。代码和 API 文档更适合按语法结构或标题层级切；表格和 PDF 更需要保留版面边界；Markdown/HTML 则需要保留层次结构，甚至形成父子 chunk 树。</p>
<p>这类设计会直接改变召回质量。若把所有内容都按固定长度切块，标题、层级、页面边界、代码块和表格结构都会被抹平；若系统保存 chunk 的父子关系、边界分隔符、正文起止位置、重叠前后文与结构元数据，那么检索命中的就不再只是孤立片段，而是文档结构中的一个明确位置。此时的 RAG 更接近结构化文档检索。</p>
<p>Agentic Chunking 进一步把 LLM 引入切分阶段，由模型判断哪些内容应属于同一语义单元。它在复杂文档上可能带来更强的语义完整性，但代价同样明显：构建成本高、可重复性差、调试难度大，提示词质量会直接影响索引结果。因此它更适合作为高价值内容的增强型切分，而不适合作为默认的全量基础设施。</p>
<div class="blog_h2"><span class="graybg">向量数据库</span></div>
<p>向量数据库（Vector Database）存储每个 chunk 的向量表示（Embedding Vector）及其元数据（Metadata），支持近似最近邻（Approximate Nearest Neighbor, ANN）检索。向量并不“包含全文”，它是语义相似度的索引键；检索结果仍然需要回源拿到原文片段。</p>
<p>工程上需要区分“向量索引（Vector Index）”与“向量数据库（Vector Database）”：前者强调 ANN 检索算法与数据结构；后者强调持久化、增量更新、元数据过滤（Metadata Filtering）、分片/副本与多租户（Multi-tenancy）等系统能力。</p>
<p>生产知识库通常会同时维护多个索引平面：chunk 正文索引、chunk 摘要索引、chunk 问题索引，必要时还有文档摘要索引。它们对应的是不同的检索语义：正文索引擅长直接召回原文；摘要索引更适合长文压缩后的主题级匹配；问题索引则把 chunk 改写成更贴近用户查询的可检索意图。因此，生产 RAG 的索引设计本质上是“同一内容的多种检索视图”。</p>
<p>双后端架构也是常见现实。高质量知识库常把 Milvus/Qdrant 一类向量系统与 Elasticsearch/OpenSearch 一类全文检索系统并行使用：前者负责稠密向量、稀疏向量与 ANN 检索，后者负责 BM25、字段过滤和业务搜索。这种组合对应的是信号分工：向量擅长语义匹配，词项系统擅长精确关键词与过滤。真正的难点随之转移到一致性和融合上：索引命名要对齐，更新要同步，删除要清理，跨系统分数不能直接比较。</p>
<div class="blog_h3"><span class="graybg">实现形态与选型</span></div>
<p>向量检索组件通常有三种部署形态：向量索引库（如 FAISS/ScaNN）偏“库”；向量数据库（如 Milvus/Qdrant/Weaviate）偏“服务”；全文检索引擎（如 Elasticsearch/OpenSearch）则以 BM25 为核心并逐步补齐向量检索能力。许多生产系统采用“双引擎”组合：<span style="background-color: #c0c0c0;">向量库负责高性能 ANN，全文检索负责复杂过滤与关键词召回</span>，最终在融合/重排阶段统一排序。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">形态</td>
<td style="text-align: center;">代表实现</td>
<td style="text-align: center;">优势</td>
<td style="text-align: center;">局限</td>
<td style="text-align: center;">适用场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>向量索引库（In-process Vector Index）</td>
<td>FAISS / ScaNN</td>
<td>单机性能极强；集成简单；易做离线批构建</td>
<td>分布式/多租户/权限/运维能力弱；元数据过滤能力有限</td>
<td>原型验证；单机检索；离线候选生成</td>
</tr>
<tr>
<td>向量数据库（Vector Database Service）</td>
<td>Milvus / Qdrant / Weaviate</td>
<td>持久化 + CRUD；支持元数据过滤；更易做水平扩展</td>
<td>需要运维；延迟/吞吐取决于索引与集群配置</td>
<td>生产 RAG；增量更新频繁；需要过滤/权限</td>
</tr>
<tr>
<td>全文检索 + 向量检索（Hybrid Search Engine）</td>
<td>Elasticsearch / OpenSearch</td>
<td>BM25 + 过滤能力强；生态成熟；适合业务检索</td>
<td>向量能力与性能细节高度依赖具体版本/配置；向量索引形态可选项较少</td>
<td>强关键词依赖；复杂过滤/字段检索；混合检索一体化</td>
</tr>
<tr>
<td>组合架构（Vector DB + Search Engine）</td>
<td>Milvus/Qdrant + ES</td>
<td>各取所长；向量与词项召回分别优化；融合/重排可控</td>
<td>系统更复杂；需要去重、打分对齐与一致性策略</td>
<td>对检索质量要求高，且有复杂过滤/排序逻辑</td>
</tr>
<tr>
<td>托管服务（Managed Service）</td>
<td>Pinecone / 各云厂商向量服务</td>
<td>运维成本低；弹性扩缩容；通常带权限与监控</td>
<td>成本较高；可控性与部署形态受限</td>
<td>快速上线；团队缺乏检索基础设施经验</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">ANN 索引与过滤</span></div>
<p>ANN 索引（ANN Index）在“速度、召回率（Recall）与内存”之间权衡。检索侧常见痛点是“向量相似度可算，但业务过滤很难做快”：元数据过滤会破坏 ANN 的近邻结构，使得系统需要在“先过滤再向量检索”和“先向量检索再过滤”之间做策略选择。</p>
<p>在生产知识库里，过滤通常不是可选项，而是第一等公民。目录树（Catalog Tree）、启用状态、文档类型、租户边界、语言、内容类型、业务域前缀，都可能成为过滤条件。也正因为如此，许多系统会把 catalog 前缀过滤、enabled 标志、source_type/source_id 等字段同时写入 Milvus 和 ES，两边都能先做过滤再做召回。否则，召回质量再高，也可能把“本不该返回的内容”混进上下文。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">索引/策略</td>
<td style="text-align: center;">核心思路</td>
<td style="text-align: center;">优点</td>
<td style="text-align: center;">代价</td>
</tr>
</thead>
<tbody>
<tr>
<td>Flat（Exact）</td>
<td>全量计算相似度</td>
<td>精确；实现最简单</td>
<td>规模上去后延迟/成本不可接受</td>
</tr>
<tr>
<td>HNSW</td>
<td>分层小世界图近邻搜索</td>
<td>高召回、低延迟；对在线增量友好</td>
<td>内存占用较高；过滤条件复杂时性能波动</td>
</tr>
<tr>
<td>IVF / IVF-PQ</td>
<td>先粗聚类（倒排），再在桶内搜索；PQ 进一步压缩</td>
<td>更省内存；适合大规模离线构建</td>
<td>更新成本高；召回/延迟依赖参数调优</td>
</tr>
<tr>
<td>DiskANN / Hybrid Memory</td>
<td>把索引/向量放到 SSD，内存放导航结构</td>
<td>降低内存压力；可支持更大规模</td>
<td>I/O 成为瓶颈；工程复杂度更高</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">检索策略</span></div>
<div class="blog_h3"><span class="graybg">稠密检索（向量相似度）</span></div>
<p>稠密检索（Dense Retrieval）用向量相似度（如余弦相似度或内积）做 top-k 召回，擅长语义匹配与同义改写，但可能漏掉精确关键词（如错误码、版本号）。</p>
<p>它还有一个常被低估的前提：embedding 模型的训练分布必须与目标领域足够接近。若把主要基于互联网通用语料训练出来的嵌入模型，直接拿去检索法律、医疗、专利或企业内部术语密集的文本，向量空间中的“相似”往往就不再等于业务上的“相关”，召回质量会明显下降。此时更稳妥的做法通常是引入领域微调 embedding、混合检索，或让 sparse / full-text 路径共同兜底。</p>
<div class="blog_h3"><span class="graybg">全文检索（BM25 / ES）</span></div>
<p>全文检索（Lexical Retrieval）以 BM25 等词项匹配为核心，擅长精确匹配与关键词召回。它对拼写、专有名词、数字更敏感，但对语义改写不够鲁棒。</p>
<div class="blog_h3"><span class="graybg">稀疏检索（Sparse Retrieval / SPLADE）</span></div>
<p>稀疏检索（Sparse Retrieval）用“稀疏向量”表示文本：向量维度对应词项（或子词），权重表示该词项对匹配的贡献。与 BM25 相比，稀疏检索的权重来自模型学习而非纯统计；与稠密检索相比，它保留了词项级可解释性与对罕见词/数字的敏感性。</p>
<p>典型路线包括 SPLADE（Sparse Lexical and Expansion Model）：通过学习得到“词项扩展（Lexical Expansion）”，让语义相关但不共词的文本仍能在词项空间相遇。工程上，稀疏检索常作为混合检索的一路召回信号，而不是单独替代 BM25。</p>
<div class="blog_h3"><span class="graybg">混合检索</span></div>
<p>混合检索（Hybrid Retrieval）把稠密检索与全文检索结合：先分别召回，再融合排序（如加权、去重、学习排序）。这是目前最稳健的通用策略之一。</p>
<p>在更完整的知识库系统里，混合检索不只是“dense + BM25”两路，而可能是 <span style="background-color: #c0c0c0;">chunk / summary / question</span> 三类检索目标与 <span style="background-color: #c0c0c0;">dense / sparse / fulltext</span> 三类检索路径的组合矩阵。换句话说，生产 RAG 召回的是多个“信号通道”，而不是单一相似度函数。它的优势是覆盖面更强：原文命中、主题命中、问句改写命中可以互补；但代价也很直接：路径暴涨、分数更不可比、日志更难读、回归测试更复杂。</p>
<p>这类系统往往还会按知识库能力做“有效方法裁剪”。例如，用户请求 fullhybrid，但某个知识库只建了 dense 和 sparse 索引，那么实际就只能退化成 hybrid；某个库没有开启 question recall，就不该在该路径上浪费资源。这个细节反映出一个成熟的 RAG 判断：<span style="background-color: #c0c0c0;">检索策略不是全局写死的，而是知识库级配置与请求级策略共同决定的</span>。</p>
<div class="blog_h4"><span class="graybg">分数融合（Score Fusion）</span></div>
<p>混合检索的难点不在“多路召回”，而在“分数不可比（Incomparable Scores）”：BM25 分数、余弦相似度/内积分数、稀疏向量分数往往不在同一尺度，不能直接相加。常见融合策略：</p>
<ul>
<li>归一化后加权：对各路分数做 min-max / z-score 等归一化，再做加权求和或加权乘积。</li>
<li>基于排序的融合：不依赖原始分数尺度，例如 RRF（Reciprocal Rank Fusion）：<span displaypfx="inline-" class="mathjax-container">\(\mathrm{score}(d)=\sum_{s}\frac{1}{k+\mathrm{rank}_s(d)}\)</span>。</li>
<li>学习排序（Learning to Rank）：把各路分数与特征（BM25、embedding sim、字段匹配、长度等）作为特征，训练一个排序模型做融合。</li>
</ul>
<p>生产系统通常更偏爱基于排序的融合，而不是直接拼原始分数。这是因为 ES 的 <span displaypfx="inline-" class="mathjax-container">\(_score\)</span>、Milvus 稠密距离、Milvus 稀疏 BM25 分数往往来自完全不同的评分体系。RRF 或加权 RRF 的价值就在于鲁棒：它不要求各后端分数可比，只要求各路径给出相对顺序。若再往上追求精度，则会在粗召回后接一个模型重排器，对融合后的候选做统一判断。</p>
<div class="blog_h3"><span class="graybg">相关性阈值</span></div>
<p>相关性阈值（Relevance Threshold）决定的是：召回结果里哪些片段值得继续送给模型，哪些片段应该被直接丢弃。很多 RAG 系统的问题并不是“完全没召回到东西”，而是把大量边缘相关、低置信度甚至明显错误的片段一并塞进上下文，结果让生成阶段被噪声牵着走。</p>
<p>因此，top-k 本身通常不够；工程上还需要结合相似度阈值、分数差值阈值或最小证据数量规则共同判断。阈值设得过低，会把噪声片段混进上下文；阈值设得过高，又可能让系统在真实有答案时错误地进入“未检索到结果”分支。成熟系统往往会把阈值当作知识库级和请求级都可调的参数，并配合离线标注集或在线反馈持续校准。</p>
<div class="blog_h2"><span class="graybg">查询改写</span></div>
<p>查询改写（Query Rewriting）解决的问题是：用户真正表达的检索意图，经常并不等于用户字面上输入的那句话。尤其当用户提问冗长、上下文依赖强、代词很多、问题里混有解释性语句而不是检索关键词时，直接拿原问题去召回，往往不如先把问题改写成更适合检索的形式。</p>
<p>查询改写的核心是把问题重新组织成更适合索引命中的检索表达，例如补全省略主语、显式写出实体名、拆开复合问题、提取关键词、或把对话上下文中的隐含约束展开成可检索文本。</p>
<div class="blog_h3"><span class="graybg">多查询 RAG</span></div>
<p>多查询 RAG（Multi-Query RAG）会先把一个原始问题改写成多个语义相近但表述不同的检索查询，再分别召回并合并结果。它的动机很直接：单一路径的 query 很容易受措辞影响，而多个改写版本可以覆盖不同的关键词、别名、问题角度与表达方式，从而提高召回覆盖率。</p>
<div class="blog_h3"><span class="graybg">多跳 RAG</span></div>
<p>多跳 RAG（Multi-hop RAG）适合那些答案依赖多段证据链的查询。它通常不会指望一次检索就拿到最终答案，而是先检索第一跳证据，再根据第一跳结果生成下一跳 query，继续检索后续证据。于是，检索过程本身就变成了一条“检索 ➡ 读证据 ➡ 改写下一问 ➡ 再检索”的链式推理流程。</p>
<div class="blog_h3"><span class="graybg">智能体 RAG</span></div>
<p>智能体 RAG（Agentic RAG）则把查询改写提升为一个可反复决策的过程：模型不只改写一次 query，而是根据当前召回质量、证据缺口、工具反馈与上下文状态，决定是否继续搜索、换一种问法、切换知识库、触发重排，或干脆停止检索进入回答阶段。它的重点已经不只是“改写 query”，而是让检索成为 Agent 控制流的一部分。</p>
<div class="blog_h2"><span class="graybg">重排序（Reranking）</span></div>
<p>重排序（Reranking）在“召回”（Recall）之后提升“相关性排序”（Precision@k）：用更强但更慢的模型对候选片段重新打分。</p>
<p>成熟 RAG 系统里的 rerank 还常分成两层。第一层是每个知识库内部的局部重排：对该库自己的候选做加权、RRF 或模型精排。第二层是多知识库结果合并后的全局重排：当查询同时打到多个知识库时，再在总候选集合上做一次统一排序。这样做的动机很现实：单库内部的分数可比较，并不代表跨库结果也天然可比较；若不做第二层，全局 top-k 往往会被某个打分体系更“激进”的知识库垄断。</p>
<div class="blog_h3"><span class="graybg">Cross-Encoder Reranker</span></div>
<p>Cross-Encoder Reranker 把 query 与候选 chunk 拼接后输入同一个编码器，用注意力做细粒度匹配，相关性通常更高，但计算更慢，适合对 top-k（例如 50→10）做精排。</p>
<p>典型输入形式是 <pre class="crayon-plain-tag">[CLS] query [SEP] doc [SEP]</pre>（或对应模型的特殊 token）。在 BERT 类模型中，token type embeddings（也称 segment embeddings）可用于区分两段文本；随后 Transformer 的全连接自注意力允许 query token 与 doc token 在每一层发生深度交互，这正是 cross-encoder “cross” 的本质。</p>
<p>打分通常取 <span displaypfx="inline-" class="mathjax-container">\([\mathrm{CLS}]\)</span> 位置的最终隐向量 <span displaypfx="inline-" class="mathjax-container">\(h_{\mathrm{CLS}}\)</span> 过一个线性层得到相关性分数（或二分类 logits）。与双编码器（Bi-Encoder / Dual Encoder）相比，cross-encoder 更准但无法离线预计算候选向量，因此在线成本更高。</p>
<div class="blog_h3"><span class="graybg">LLM Reranker</span></div>
<p>LLM Reranker 用大模型直接判断相关性或做对比选择，能融合更复杂的语义与任务约束，但成本更高且需要防止“自信乱判”。工程上常用它做少量候选的最终筛选。</p>
<div class="blog_h3"><span class="graybg">生成式重排序（Generative Reranking）</span></div>
<p>生成式重排序（Generative Reranking）把“相关性打分”转写为生成任务：把 query 与 doc 拼接输入解码器（Decoder-only）或编码器-解码器（Encoder–Decoder），让模型生成一个标签 token（例如 <pre class="crayon-plain-tag">Yes/No</pre>）或一个短评分文本，并用生成概率作为分数。</p>
<p>常见的点式打分形式是二分类 log-odds：令 <span displaypfx="inline-" class="mathjax-container">\(y\in\{0,1\}\)</span> 表示是否相关，则</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{score}(q,d)=\log p_\theta(y=1\mid q,d)-\log p_\theta(y=0\mid q,d)\]</span>
<p>相比传统 BERT 类 cross-encoder，生成式 reranker 往往更擅长处理长文本匹配、复杂约束与隐含推理；工程上常用“轻量级 LLM + 领域数据微调”来获得显著增益。</p>
<div class="blog_h3"><span class="graybg">列表式重排序（Listwise Reranking）</span></div>
<p>列表式重排序（Listwise Reranking）把多个候选 doc 一次性送入模型，在同一个上下文窗口中共同建模“相对顺序”，而不是对每个（query, doc）独立跑一次 forward。直觉上，它让候选之间在 self-attention 中“同台竞争”，从而提升排序一致性，并降低多次推理带来的总开销。</p>
<p>实现上常见做法是：把候选 doc 按截断长度拼接成一个 list，要求模型输出每个 doc 的分数或名次；也有实现会在末尾使用一个专门的“汇聚 token”对每个 doc 读取表示。Listwise 方法的工程约束更强：候选数、每段 doc 的截断长度与模型上下文窗口会直接决定可吞吐的 top-k。</p>
<div class="blog_h3"><span class="graybg">训练目标与 Hard Negatives</span></div>
<p>精排训练的难点是“区分相似但不相关”：需要硬负样本（Hard Negatives）来逼迫模型学习细粒度差异。硬负样本通常来自第一阶段召回：对同一 query，取检索 top-k 中不相关的候选作为 negatives（也可混入 BM25 negatives、in-batch negatives、或对抗式 negatives）。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">目标</td>
<td style="text-align: center;">形式</td>
<td style="text-align: center;">直觉</td>
<td style="text-align: center;">常见风险</td>
</tr>
</thead>
<tbody>
<tr>
<td>点式（Pointwise）</td>
<td>学习 <span displaypfx="inline-" class="mathjax-container">\(s(q,d)\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(p(y=1\mid q,d)\)</span></td>
<td>把相关性当作二分类/回归</td>
<td>分数标定（Calibration）难；对“相对顺序”监督较弱</td>
</tr>
<tr>
<td>对式（Pairwise）</td>
<td>学习 <span displaypfx="inline-" class="mathjax-container">\(s(q,d^+)&gt;s(q,d^-)\)</span></td>
<td>直接优化排序边际（Margin）</td>
<td>负样本质量决定上限；采样偏差可能放大</td>
</tr>
<tr>
<td>列表式（Listwise）</td>
<td>对候选列表联合优化（如 softmax over list）</td>
<td>更贴近 NDCG/MRR 等排序指标</td>
<td>计算与实现复杂；受上下文窗口约束</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">蒸馏（Knowledge Distillation）</span></div>
<p>蒸馏（Knowledge Distillation）把一个强但慢的教师模型（Teacher）产生的打分/偏好，转移给一个更快的学生模型（Student）。在重排序里，蒸馏最常见的目标不是“压缩参数”，而是<span style="background-color: #c0c0c0;">把 LLM 级别的相关性判断能力下放到可规模化的 reranker</span>，使线上能承载更大的 top-k、更高的 QPS 或更低的 P99。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Teacher → Student</td>
<td style="text-align: center;">监督信号</td>
<td style="text-align: center;">常见损失</td>
<td style="text-align: center;">收益</td>
<td style="text-align: center;">主要风险</td>
</tr>
</thead>
<tbody>
<tr>
<td>LLM / 生成式 reranker → Cross-Encoder</td>
<td>软分数（log-odds / graded）或偏好对</td>
<td>回归（MSE）/ 对式（pairwise logistic）</td>
<td>显著降成本；保留较强语义与推理能力</td>
<td>学生上限受教师约束；教师偏差会被复制</td>
</tr>
<tr>
<td>Cross-Encoder → Bi-Encoder</td>
<td>相对顺序/分数</td>
<td>对比学习（InfoNCE）/ 蒸馏排序边际</td>
<td>把精排能力“下放”到召回，提高召回质量</td>
<td>需要大量 hard negatives；对领域漂移敏感</td>
</tr>
<tr>
<td>LLM → 数据标注（作为训练集构造器）</td>
<td>弱标注标签 + 解释/证据</td>
<td>按目标任务训练（点式/对式/列表式）</td>
<td>快速构造大规模领域数据；迭代快</td>
<td>噪声标注；需抽样人工复核与在线 A/B 验证</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">返回内容不是只给命中文本</span></div>
<p>RAG 检索阶段的最终产物并不一定只是“命中的那一小段 chunk”。很多高质量系统会把返回内容模式做成显式策略：只返回命中块正文、返回去掉 overlap 的核心正文、返回命中块加邻近上下文、或按文档中的位置范围把相关块递归拼回更完整的片段。这样做的原因是：模型生成真正需要的是<span style="background-color: #c0c0c0;">足够完整且边界清晰的证据上下文</span>，而不是孤立句子。</p>
<p>这里的关键判断是：检索命中的是索引单元，但喂给模型的应是生成单元。索引单元倾向短小、便于召回；生成单元则要兼顾上下文完整性、token 预算和可引用性。若两者完全绑定，系统通常会在召回精度与生成可读性之间来回拉扯。</p>
<div class="blog_h2"><span class="graybg">从项目设计能看到的几条经验</span></div>
<p>第一，RAG 的上限不只由 embedding 模型决定，而是由“分块质量、索引视图、融合策略、回拼策略、状态治理”共同决定。把注意力只放在换一个更强 embedding 上，通常抓不住主矛盾。</p>
<p>第二，知识库不是静态文件集合，而是带状态机的外部知识资产。只有显式区分 base、document、chunk，并让它们有自己的生命周期，系统才可能支持增量更新、失败恢复、重建和审计。</p>
<p>第三，生产 RAG 常常天然走向双后端和多路径召回。这个方向的收益是更稳健的覆盖率，代价是系统复杂度、索引一致性成本和观测难度都会明显上升。因此它适合高价值知识库，不一定适合所有团队的第一版系统。</p>
<p>第四，把摘要和问题也视作可检索对象，是一个非常务实的设计。它等于承认“原文表述”与“用户提问方式”经常不一致，于是用知识增强知识本身的可检索性。但它也会带来额外索引、额外 embedding 成本与额外一致性问题，因此只有在召回质量确实成为瓶颈时，这种多视图索引才值得引入。</p>
<p>第五，外部知识平台同步并不等于“复制一份数据就结束”。一旦系统允许一部分知识由外部平台托管、另一部分知识由内部系统自建索引，那么“谁是事实源、谁负责更新、谁负责删除、谁负责召回”就必须在架构上说清楚。否则最容易出现的不是召回不到，而是召回出重复、过期或跨系统不一致的内容。</p>
<div class="blog_h3"><span class="graybg">主流选型对比</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">阶段</td>
<td style="text-align: center;">常见架构</td>
<td style="text-align: center;">代表实现</td>
<td style="text-align: center;">核心逻辑</td>
<td style="text-align: center;">速度</td>
<td style="text-align: center;">精度</td>
</tr>
</thead>
<tbody>
<tr>
<td>召回</td>
<td>双编码器（Bi-Encoder）</td>
<td>BGE-Embedding<br />text-embedding-3</td>
<td>向量相似度</td>
<td>极快</td>
<td>中</td>
</tr>
<tr>
<td>精排（稳健基线）</td>
<td>交叉编码器（Cross-Encoder）</td>
<td>BAAI<br />bge-reranker-v2-m3</td>
<td><span displaypfx="inline-" class="mathjax-container">\([q;d]\)</span> 深度交互</td>
<td>中</td>
<td>高</td>
</tr>
<tr>
<td>精排（顶配）</td>
<td>生成式 / 列表式（LLM-based）</td>
<td>BGE-Reranker-v2-Gemma<br />Jina-Reranker-v3</td>
<td>生成式打分或 listwise 竞争</td>
<td>较慢</td>
<td>极高</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">生成与融合</span></div>
<p>生成与融合（Generation &amp; Fusion）的关键在于：把检索到的证据（Evidence）以可控格式注入 prompt，并要求模型“基于证据回答”。常见做法包括：</p>
<ul>
<li>Stuffing：把 top-k 片段直接拼接进上下文（最简单，最易超预算）。</li>
<li>Map-Reduce：先对各片段分别提炼要点，再汇总生成最终答案（更稳，成本更高）。</li>
<li>Refine：先用少量证据生成初稿，再逐片段增量修订（适合长证据链）。</li>
</ul>
<div class="blog_h1"><span class="graybg">智能体（Agent）</span></div>
<p>检索增强生成（RAG）解决的是“模型缺少外部知识”的问题；智能体（Agent）解决的是“模型如何在真实环境中持续完成任务”的问题。一个真正可用的 Agent 不再是单次 prompt 的包装，而是一个带有状态（State）、工具（Tools）、记忆（Memory）与控制流（Control Flow）的运行时系统。它能够观察环境、决定下一步动作、调用外部能力、根据反馈修正策略，并在多轮交互中维持任务连续性。</p>
<div class="blog_h2"><span class="graybg">核心概念</span></div>
<p>从工程视角看，Agent 的本质是“以大模型为决策核心的软件系统”。模型负责理解、推理与生成；外层系统负责注入上下文、执行动作、保存状态、施加约束，并把环境反馈重新送回模型。只有这几层协同起来，模型才会从一次性回答器变成可持续工作的执行体。</p>
<div class="blog_h3"><span class="graybg">计划-执行</span></div>
<p>计划-执行（Plan-Execute）是智能体最常见的控制流之一：先把“用户目标”分解为一组可操作步骤（计划），再逐步调用工具/模型完成这些步骤（执行），并把中间结果写回状态（State）。当环境反馈与计划假设不一致时，触发再计划（Replan）。工程上，它的价值是把一次长输出变成可观测（Observable）、可重试（Retryable）、可中断（Interruptible）的多步过程。</p>
<div class="blog_h3"><span class="graybg">感知-推理-行动循环</span></div>
<p>感知-推理-行动（Perception-Reasoning-Action）循环是 Agent 的基本闭环。感知阶段读取环境状态，例如用户输入、网页 DOM、数据库返回值、日志、文件内容或工具回执；推理阶段根据当前目标、约束和历史状态决定下一步策略；行动阶段把决策落实为具体操作，例如搜索、调用 API、写文件、点击界面或向下游智能体发消息；随后再读取新反馈，进入下一轮循环。这个闭环的关键不在“会不会思考”，而在“每一轮是否拿到了正确观测、是否执行了正确动作、是否把结果正确写回状态”。</p>
<p>因此，Agent 设计首先是状态机（State Machine）设计。很多失败并不是模型推理能力不足，而是观测不完整、动作不可验证、状态写回混乱，导致下一轮推理建立在错误世界模型之上。一个好的 Agent runtime 会显式记录当前目标、最近动作、动作结果、异常信息与下一步计划，从而让每一轮循环都建立在稳定状态上。</p>
<div class="blog_h3"><span class="graybg">工具使用（Tool Use）</span></div>
<p>工具使用（Tool Use）让语言模型突破“只会生成文本”的边界，把外部系统接成自己的感官与执行器。工具既可以是只读型能力，例如搜索、数据库查询、文件读取；也可以是动作型能力，例如发送请求、执行 shell、操作浏览器、写入代码仓库。模型本身不直接执行这些动作，而是输出结构化调用意图，再由外层系统负责真正执行、捕获结果并返回。</p>
<p>工程上，Tool Use 的难点不在“把工具接上”，而在“把工具定义清楚”。一个好的工具接口需要明确输入 schema、输出 schema、权限边界、失败语义和重试策略。工具定义越模糊，模型越容易产生参数幻觉（Argument Hallucination）、错误调用和无效循环；工具定义越稳定，模型越容易学会把工具当作程序原语来组合。</p>
<div class="blog_h3"><span class="graybg">记忆系统</span></div>
<p>记忆系统（Memory System）负责解决跨步骤与跨会话连续性问题。没有记忆的模型每次调用都像“重新开机”；有记忆的 Agent 才能累积上下文、复用历史决策、根据过去错误调整行为。记忆并不等同于“把更多 token 塞进上下文窗口”，而是要把不同时间尺度、不同重要性的状态放进不同存储层，再按需检索。</p>
<div class="blog_h4"><span class="graybg">短期记忆（上下文窗口）</span></div>
<p>短期记忆（Short-term Memory）通常由上下文窗口（Context Window）承载，保存当前任务最活跃的信息：系统指令、最近几轮对话、正在执行的计划、刚拿到的工具结果、当前工作草稿等。它的优势是读取成本低、和当前推理绑定最紧；它的限制也同样明显：容量昂贵、注意力会稀释、信息越多不代表效果越好。</p>
<p>因此，短期记忆的核心工作不是“堆满”，而是“裁剪”。优秀的 Agent 会把原始轨迹压缩成摘要（Summary）、状态（State）和待办项（Next Actions），只保留当前决策真正需要的变量。短期记忆承担的是工作记忆（Working Memory）的职责：它服务当前这一轮推理，而不是承担长期知识库的角色。</p>
<div class="blog_h4"><span class="graybg">长期记忆（外部存储）</span></div>
<p>长期记忆（Long-term Memory）存放在上下文窗口之外，常见载体包括向量数据库、关系数据库、对象存储、文档库、日志系统、代码仓库、任务看板与事件流。它保存的是“现在不必全部加载，但未来可能需要回忆”的信息，例如用户偏好、历史案例、项目规范、过去失败总结、已验证的事实与中间产物。</p>
<p>长期记忆要解决三个问题：写什么、怎么检索、何时遗忘。写入过多会让存储退化为噪声池；只做向量相似度检索又容易漏掉结构化约束与时间关系。成熟做法通常会把结构化状态、全文检索、向量召回与人工定义的索引结合起来，让 Agent 既能“语义回忆”，也能“精确定位”。</p>
<div class="blog_h4"><span class="graybg">记忆类型：情景 / 语义 / 程序</span></div>
<p>目前主流 Agent 设计常借用认知科学中的三类记忆划分：情景记忆（Episodic Memory）、语义记忆（Semantic Memory）与程序记忆（Procedural Memory）。这三类记忆对应的是“发生过什么”“知道什么”“怎么做”。把它们混在一个存储里，检索和写入策略很快就会失控；分层建模则更容易设计稳定的读写规则。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">记忆类型</td>
<td style="text-align: center;">保存内容</td>
<td style="text-align: center;">典型载体</td>
<td style="text-align: center;">工程价值</td>
</tr>
</thead>
<tbody>
<tr>
<td>情景记忆（Episodic）</td>
<td>具体事件、行动轨迹、失败案例、会话历史</td>
<td>任务日志、轨迹摘要、审计记录</td>
<td>支持回放、复盘、反思与重试</td>
</tr>
<tr>
<td>语义记忆（Semantic）</td>
<td>稳定知识、规则、事实、领域概念</td>
<td>知识库、文档索引、结构化事实表</td>
<td>支持检索、问答、约束判断</td>
</tr>
<tr>
<td>程序记忆（Procedural）</td>
<td>工作流程、技能脚本、工具使用模式</td>
<td>系统提示、工具说明、可复用 playbook</td>
<td>把“会做事”沉淀为稳定操作习惯</td>
</tr>
</tbody>
</table>
<p>其中，程序记忆对工程系统尤其关键。很多所谓“Agent 经验”最终都会沉淀成程序记忆，例如某个 API 的调用顺序、某类错误的排查流程、某个项目的提交流程。Harness Engineering 本质上就是把这些隐性经验逐步外化成可持续复用的程序记忆。</p>
<div class="blog_h2"><span class="graybg">推理与规划</span></div>
<p>推理（Reasoning）解决的是“当前这一步怎么想”；规划（Planning）解决的是“整个任务怎么走”。现代 Agent 往往同时需要二者：局部步骤要有足够的推理精度，长期任务要有阶段化计划与中途重规划能力。近两年的研究基本沿着四条路线收敛：交错式思考与行动、反思式自我修正、显式规划后求解，以及基于搜索的多路径探索。</p>
<div class="blog_h3"><span class="graybg">ReAct</span></div>
<p>ReAct（Reason + Act）是当前 Agent 范式最有影响力的起点之一。它把“思考轨迹”和“动作轨迹”交错起来：模型先给出一小步 reasoning，再发起 action，读取 observation 后继续下一步 reasoning。与只做链式思维（Chain-of-Thought）的做法相比，ReAct 的优势在于每一步都能接触外部环境，因此能在事实不确定、需要探索、需要工具辅助的任务上持续修正路径。</p>
<p>ReAct 的核心价值并不是让模型“想得更多”，而是让推理过程与环境反馈形成闭环。它也有明显代价：若没有停机条件、步数上限和工具约束，轨迹很容易冗长、循环或过度调用工具。因此，ReAct 更像 Agent 控制流的原型范式，生产系统通常会在其外层再加计划、验证、预算控制与错误恢复机制。</p>
<div class="blog_h3"><span class="graybg">Reflection（自我反思）</span></div>
<p>Reflection（自我反思）把“错误”变成下一轮决策的输入。其基本思想是：一次执行失败后，不只返回原始错误信号，还额外生成一段高信息密度的经验总结，说明失败原因、已验证无效的路径、下一次应当修改的策略。这样，模型获得的不是一个抽象的 reward，而是一段可以直接进入上下文的文字化梯度。</p>
<p>从研究谱系看，Reflexion 将这种思路系统化为 verbal reinforcement learning：把环境反馈转写成文本反思，并存入 episodic memory，供后续回合检索。工程上，这类方法通常以“执行后 Critic”“失败总结卡片”“经验回放摘要”等形式落地。它最适合高代价试错任务，例如代码修复、网页操作、长任务代理；前提是系统能可靠地区分“失败是策略问题”还是“失败只是偶然噪声”。</p>
<div class="blog_h3"><span class="graybg">Plan-and-Solve</span></div>
<p>Plan-and-Solve 的思想很直接：先显式列出求解计划，再根据计划执行。它针对的是零样本链式思维常见的 missing-step 问题，即模型直接开始求解时容易跳步骤、漏约束或在中途漂移。把“先规划、后执行”做成两个阶段后，模型的注意力会先聚焦在任务分解，再聚焦在逐步完成，从而显著降低长任务中的结构性遗漏。</p>
<p>这一思路在工程系统里几乎已经成为默认模式。很多 Coding Agent、Research Agent 与 Workflow Agent 都把 planner 和 executor 拆成不同阶段，甚至拆成不同角色。原因很简单：计划是一种低成本、高杠杆的中间产物。它既能让人类在写代码前审查方向，也能让系统在执行中持续对照“当前动作是否仍服务于原始目标”。</p>
<div class="blog_h3"><span class="graybg">MCTS（蒙特卡洛树搜索）</span></div>
<p>MCTS（Monte Carlo Tree Search，蒙特卡洛树搜索）把 Agent 的决策过程从“单路径生成”升级为“多路径探索”。系统不再只让模型给出一个下一步动作，而是展开多个候选分支，对不同轨迹进行模拟、评估和回传分值，再把计算预算集中到更有希望的分支上。对于需要长程依赖、路径选择、试探性探索的任务，这种显式搜索比单次贪心生成更稳健。</p>
<p>Language Agent Tree Search（LATS）是这一路线的代表性工作，它把 MCTS 与语言模型结合，让模型同时承担候选动作生成、状态评估和自我反思等角色。代价同样明确：搜索带来更高 token 成本、更复杂的状态管理和更重的工程编排。因此，MCTS 更适合高价值、高不确定性任务，例如复杂推理、博弈式决策、困难编程与网页任务，而不是每个简单问答都默认启用的通用套路。</p>
<div class="blog_h2"><span class="graybg">工具与行动</span></div>
<p>如果说推理模块决定“应该做什么”，那么工具层决定“究竟能做到什么”。当前主流 Agent 平台基本收敛出几类高频动作接口：结构化函数调用、受控代码执行、Web 检索、图形界面操作，以及把外部工具和资源标准化接入的协议层。它们共同构成了 Agent 的执行面。</p>
<div class="blog_h3"><span class="graybg">Function Calling</span></div>
<p>Function Calling（函数调用）是最基础、最通用的 Tool Use 形式。模型输出的不再是自然语言描述，而是一个满足 schema 的调用请求，例如函数名、参数对象、调用意图；应用侧接收到请求后真正执行代码，并把结果作为 tool result 返回。这样，模型负责“决定调用什么”，应用负责“保证执行正确”。</p>
<p>Function Calling 的真正意义是把自由文本接口收窄为结构化程序接口。只要参数 schema 足够清晰，模型就能稳定学会把外部系统当成可组合原语。这也是今天大部分 Agent 框架的底层抽象：上层看起来是“智能体会用工具”，底层通常仍是“模型在做结构化函数选择与参数填充”。</p>
<div class="blog_h3"><span class="graybg">Code Interpreter</span></div>
<p>Code Interpreter（代码解释器）让模型在受控沙箱中执行代码，从而把“语言推理”转换为“可验证计算”。它特别适合数据清洗、表格处理、统计分析、绘图、文件转换、轻量脚本自动化与需要精确数值结果的任务。与纯文本推理相比，Code Interpreter 的优势在于中间状态可执行、可复现、可检查。</p>
<p>这类能力本质上是把 Python 或其他运行时变成模型的外部工作台。模型负责提出程序，沙箱负责执行，系统再把 stdout、文件产物、异常栈和图像反馈给模型。只要隔离、资源限制和文件边界设置得当，Code Interpreter 往往是让 Agent “从会解释变成会计算”的最直接跃迁。</p>
<div class="blog_h3"><span class="graybg">Web 搜索</span></div>
<p>Web 搜索（Web Search）解决的是模型知识静态化问题。基础模型参数中固化的是训练时刻之前的分布，而 Agent 经常需要回答最新价格、最新政策、最新产品文档、最新论文或当前网页状态。把搜索能力接入后，模型可以先检索，再基于来源生成回答，并在必要时附带引用。</p>
<p>工程上，Web 搜索不是“把搜索结果贴给模型”这么简单。系统还需要处理查询重写、来源筛选、去重、可信度排序、片段抽取与多源融合。对于需要最新信息的任务，搜索应当被视为一等能力，而不是可有可无的外挂；否则 Agent 的错误会表现为“推理看起来正确，但事实已经过时”。</p>
<div class="blog_h3"><span class="graybg">Computer Use</span></div>
<p>Computer Use（计算机使用）让 Agent 直接在图形界面层面操作软件与网页，例如看截图、定位元素、移动鼠标、键入内容、滚动页面与读取屏幕变化。它的价值在于：当目标系统没有现成 API，或者真正业务流程本来就发生在浏览器/桌面界面中时，Agent 仍然可以像人类操作员一样完成任务。</p>
<p>这类能力也是最脆弱的一类动作接口。界面结构变化、网络延迟、弹窗遮挡、验证码、权限确认和安全风险都会放大失败概率。因此，Computer Use 应被视为“最后一层兼容性接口”：没有 API 时才使用，并始终运行在隔离环境中，为高风险动作保留人工确认与回滚机制。</p>
<div class="blog_h3"><span class="graybg">MCP（模型上下文协议）</span></div>
<p>MCP（Model Context Protocol，模型上下文协议）试图把“给模型接工具与上下文”这件事标准化。它定义了一套客户端-服务端协议，让宿主应用可以连接多个 MCP server，并以统一方式暴露工具（Tools）、资源（Resources）与提示模板（Prompts）。这样，模型不必为每个外部系统重新学习私有接线方式，应用也不必为每种模型重复造一套集成层。</p>
<p>在工程语义上，MCP 解决的是“工具可移植性”和“上下文供给标准化”。一个搜索 server、一个数据库 server、一个设计文档 server，只要遵循同一协议，就能被不同 Agent host 复用。当前生态已经把 MCP 从“工具协议”扩展成“上下文协议”：除了调用动作，也强调让模型按需读取结构化资源，而不是把所有材料一次性塞进 prompt。</p>
<p>截至 2026 年 3 月，MCP 官方稳定协议版本为 2025-11-25，2026 路线图正在推进会话扩展（session handling）、Server Cards、Registry 与 agent-to-agent 协作能力。对应用开发者而言，更重要的不是版本号本身，而是它标志着一件事：Agent 运行时正在从各家私有 Tool API，逐步走向可协商、可复用、可观测的开放接口层。</p>
<div class="blog_h2"><span class="graybg">多智能体系统</span></div>
<p>多智能体系统（Multi-agent System）指的是：把一个复杂任务分解给多个具有不同职责或不同上下文边界的 agent，由它们通过委派、协作、验证和状态传递共同完成目标。它关注的核心是如何把规划、执行、审查和并行探索写成一个可控的协作结构。</p>
<p>是否拆成多个 agent，取决于任务是否存在天然的角色分工、上下文隔离需求、并行空间与独立验证价值。很多简单任务用一个强单体 agent 加好工具即可；只有当单一上下文开始拥塞、单角色难以兼顾规划与执行、或多个分支可以并行推进时，多智能体系统才真正体现优势。</p>
<div class="blog_h3"><span class="graybg">角色分工</span></div>
<p>角色分工（Role Decomposition）指的是：在多智能体系统中，为不同 agent 分配稳定且边界清晰的职责，使每个 agent 只处理自己最应该承担的那一类决策与操作。它的作用是把复杂任务拆成多个可管理的视角与责任域，降低单一上下文同时兼顾规划、执行和审查的负担。</p>
<p>最常见的基本三角是 Planner、Executor 与 Critic：Planner 负责分解目标，Executor 负责实际操作，Critic 负责验证与挑错。这个结构的本质是在系统内部显式制造不同视角，避免同一个上下文同时承担规划、执行与审查，最终让错误在更早阶段暴露出来。</p>
<div class="blog_h4"><span class="graybg">Planner</span></div>
<p>Planner 负责把模糊目标转成结构化任务图，例如目标拆解、依赖关系、优先级、完成标准与失败回退条件。一个好的 Planner 不直接沉迷于实现细节，而是尽快产出可被执行层消费的中间表示，例如任务列表、阶段计划、验收清单或执行 DAG。这样，后续 agent 的上下文会更短、更稳定。</p>
<div class="blog_h4"><span class="graybg">Executor</span></div>
<p>Executor 负责把计划变成具体动作：调用 API、写代码、运行测试、检索资料、修改文件、操作界面。它通常需要最严格的权限控制，因为真正改变外部世界的是执行层，而不是规划层。把 Executor 的工具范围、写入范围和预算边界限定清楚，往往比继续优化模型 prompt 更能提升整体可靠性。</p>
<div class="blog_h4"><span class="graybg">Critic</span></div>
<p>Critic 负责验证“结果是否正确”，而不是复述“过程看起来合理”。它可以检查事实一致性、运行测试、比对输出格式、审查架构约束、回放浏览器流程，或要求执行层补充证据。Critic 的价值在于给系统引入独立的否决权：没有独立验证的多智能体系统，本质上仍然只是把同一种错误复制了几遍。</p>
<div class="blog_h3"><span class="graybg">协作模式</span></div>
<p>角色定义清楚之后，下一步是决定这些角色如何协作。常见模式主要有顺序执行、并行执行与层级委派。三者区别不在名称，而在状态如何传递、谁拥有控制权、失败后如何收敛，以及系统是否允许多个 agent 同时写入同一世界状态。</p>
<div class="blog_h4"><span class="graybg">顺序执行</span></div>
<p>顺序执行（Sequential Execution）是最稳的模式：上一步完成并产出明确结果后，下一步才开始。它最适合依赖链强、可验证点清晰的流程，例如“先规划，再实现，再审查，再修复”。优点是状态简单、问题定位容易；缺点是吞吐量受限，前一环的错误会整体阻塞后续流程。</p>
<div class="blog_h4"><span class="graybg">并行执行</span></div>
<p>并行执行（Parallel Execution）用于把独立子任务同时推进，例如多个资料检索、多个候选方案探索、多个代码模块并行实现。它能显著提高吞吐，但前提是任务切分足够干净，或者系统拥有足够好的合并策略。并行化的真正难点在于如何避免它们争抢同一上下文、重复劳动或互相覆盖结果。</p>
<div class="blog_h4"><span class="graybg">层级委派</span></div>
<p>层级委派（Hierarchical Delegation）由一个上层 supervisor 或 manager 统一掌控目标和全局状态，再把子任务委派给不同 specialist。它适合长任务和复杂流程，因为全局控制权集中在上层，局部上下文压力则下沉到子 agent。代价是 supervisor 很容易成为瓶颈，因此需要明确委派粒度、回收机制与升级路径，避免系统退化成“一个总管在不停转述消息”。</p>
<div class="blog_h2"><span class="graybg">主流框架</span></div>
<p>Agent Framework 解决的是“如何把模型能力组织成长期可运行的软件系统”。主流框架的差异主要体现在三个层面：控制流表达方式、状态管理方式，以及多智能体协作的原生支持程度。选型时最重要的是它是否契合你的系统拓扑：你需要图式工作流、事件驱动编排、还是平台托管的工具执行与追踪能力。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">框架/形态</td>
<td style="text-align: center;">控制流模型</td>
<td style="text-align: center;">强项</td>
<td style="text-align: center;">代价</td>
<td style="text-align: center;">适用</td>
</tr>
</thead>
<tbody>
<tr>
<td>LangChain / LangGraph</td>
<td>链式/图式（Graph）</td>
<td>组件化；易组合 RAG、工具与记忆；LangGraph 适合复杂状态机</td>
<td>抽象层较多；需要明确工程边界</td>
<td>生产 RAG/Agent pipeline</td>
</tr>
<tr>
<td>AutoGen</td>
<td>事件驱动多智能体（Event-driven）</td>
<td>消息传递清晰；适合团队式协作与研究</td>
<td>系统设计自由度高；需要自定治理边界</td>
<td>多智能体实验；复杂协作流程</td>
</tr>
<tr>
<td>CrewAI</td>
<td>角色 + 任务流水线</td>
<td>任务编排直观；适合“岗位分工”式流程</td>
<td>可控性取决于框架提供的扩展点</td>
<td>面向业务流程的多角色 Agent</td>
</tr>
<tr>
<td>OpenClaw</td>
<td>自托管 Gateway + Agent runtime</td>
<td>多渠道接入、会话路由、插件与设备节点整合强</td>
<td>更像入口与运行时基础设施，不是最轻量的 Python 编排库</td>
<td>本地优先、多渠道助手、长期在线 Agent 网关</td>
</tr>
<tr>
<td>Hermes Agent</td>
<td>自治运行时（Autonomous Runtime）</td>
<td>内建 learning loop、skills、memory、profiles 与 delegation</td>
<td>系统较重；要真正发挥优势需要接受其运行时哲学</td>
<td>长期运行、自我积累、强工具使用的个人/团队 Agent</td>
</tr>
<tr>
<td>OpenAI Responses API / Agents SDK</td>
<td>平台托管（Managed）</td>
<td>内建工具、Tracing、Handoff 与托管能力完善</td>
<td>更依赖平台抽象；深度定制时需额外设计 runtime</td>
<td>快速构建生产 Agent；希望复用官方工具栈</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">LangChain / LangGraph</span></div>
<p>LangChain 偏组件化抽象，LangGraph 偏图式控制流。前者适合把模型、检索器、工具、记忆等模块快速拼接起来；后者适合把复杂 Agent 流程显式建模为图（Graph），把循环、分支、检查点和人机介入点写成状态转移。对于生产系统，LangGraph 的意义尤其明显：它让“Agent 不是一段 prompt，而是一个状态机”这件事可以被直接编码。</p>
<p>在多智能体方面，LangGraph 当前更强调基于工具调用（tool-calling）的 supervisor 模式，而不是把所有 agent 都包装成自由对话体。原因很实际：当协作关系被写成显式图结构后，上下文边界、路由条件和失败恢复都会更好控制。</p>
<div class="blog_h3"><span class="graybg">AutoGen</span></div>
<p>AutoGen 的长处在于把多智能体协作建模为显式消息系统。不同 agent 可以作为独立节点收发消息、触发工具、交换中间状态，因此很适合研究“团队式智能体”以及需要复杂路由的实验性系统。新版 AutoGen 也更强调事件驱动（Event-driven）与可扩展运行时，而不只是几个人工角色互相聊天。</p>
<p>它的代价也非常明确：自由度越高，系统边界越需要开发者自己画清楚。权限、可观测性、记忆持久化与异常恢复如果没有在框架外补齐，多智能体对话很容易演化成难以调试的消息洪流。</p>
<div class="blog_h3"><span class="graybg">CrewAI</span></div>
<p>CrewAI 把多智能体系统表达为“角色（Role）+ 任务（Task）+ 工具（Tool）”的业务编排语言，适合把组织分工直接映射到系统设计中。它的上手成本较低，尤其适合内容生成、运营流程、分析流水线这类天然按岗位拆分的任务。</p>
<p>这类框架的优势是表达直观，代价是底层控制流往往被隐藏得更深。只要任务依赖、重试策略、共享状态和权限边界开始复杂化，就需要确认框架是否允许你把隐含运行时重新显式化。</p>
<div class="blog_h3"><span class="graybg">OpenAI Responses API / Agents SDK</span></div>
<p>从当前官方工具栈看，OpenAI 已经把 Responses API 作为构建 agent-like application 的统一接口，并以 Agents SDK 提供更轻量的编排、handoff、guardrail 与 tracing 能力。这一路线的核心价值是把常见基础设施平台化：函数调用、Web 搜索、Computer Use、Code Interpreter、Tracing 与多智能体 handoff 都可以沿着同一套接口组织。</p>
<p>托管式能力的优点是默认路径短、集成成本低、官方工具之间协同更顺滑；缺点是当业务开始需要深度定制的状态机、自定义持久化策略或异构执行环境时，应用仍然需要在 SDK 之上补一层自己的 runtime。换句话说，托管平台减少的是“起步成本”，而不是彻底消灭 Agent 工程本身。</p>
<div class="blog_h3"><span class="graybg">OpenClaw</span></div>
<p>OpenClaw 更适合理解为<span style="background-color: #c0c0c0;">本地优先的 Agent Gateway 与运行时基础设施</span>，而不是单纯的 prompt 编排库。它的核心不是“怎样写一条 Agent workflow”，而是“怎样把一个长期在线的 Agent 安全地接到 Discord、Slack、Telegram、WhatsApp、iMessage 这类渠道上，并用统一 Gateway 管理会话、路由、插件、节点设备和工具能力”。</p>
<p>这一路线的长处在于入口治理和系统完整性。若需求是“我需要一个可长期运行、跨多个聊天入口、会话持续存在、支持插件和设备节点的个人或团队助手”，OpenClaw 的抽象更贴近真实系统边界。它的重心不是研究型多智能体消息编排，而是把<span style="background-color: #c0c0c0;">渠道、会话、路由、插件和运行时</span>做成一个统一控制平面。</p>
<div class="blog_h3"><span class="graybg">Hermes Agent</span></div>
<p>Hermes Agent 则更像<span style="background-color: #c0c0c0;">学习型 Agent Runtime</span>。它不是简单包装单一模型 API 的聊天壳，而是试图把长期记忆（Memory）、技能（Skills）、子 Agent 委派（Delegation）、工具调用、批处理、浏览器自动化、语音模式和消息网关放进一个统一自治运行时里。其最鲜明的卖点，是官方强调的 built-in learning loop：Agent 会把经验沉淀成可复用技能，并跨会话持续积累。</p>
<p>因此，Hermes 更适合那类“希望 Agent 长期运行、逐步形成操作习惯、沉淀程序性知识并持续复用”的场景。它的重点不是先把多渠道入口打通，而是让 Agent 在运行中越来越像一个有历史、有技能库、有工作记忆的长期执行体。</p>
<p>OpenClaw 与 Hermes 对比：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">OpenClaw</td>
<td style="text-align: center;">Hermes Agent</td>
</tr>
</thead>
<tbody>
<tr>
<td>核心定位</td>
<td>自托管 Gateway / 渠道路由 / Agent 接入层</td>
<td>长期运行、自我积累的自治 Agent 运行时</td>
</tr>
<tr>
<td>更强的一侧</td>
<td>多渠道入口、会话与路由治理、插件与节点生态</td>
<td>记忆、技能沉淀、学习闭环、工具执行与多 profile 隔离</td>
</tr>
<tr>
<td>抽象重心</td>
<td>“把 Agent 接到哪里、如何长期在线、如何统一调度”</td>
<td>“让 Agent 如何长期工作、持续复用经验、并逐步变强”</td>
</tr>
<tr>
<td>更像什么</td>
<td>Agent 基础设施与控制平面</td>
<td>Agent 大脑与执行运行时</td>
</tr>
<tr>
<td>典型适用</td>
<td>个人 AI 助手、多聊天平台入口、设备联动</td>
<td>编码助手、研究助手、长期自动化执行体</td>
</tr>
</tbody>
</table>
<p>这两者并不是简单替代关系。一个偏“入口与治理”，一个偏“执行与成长”。如果系统目标是搭建长期在线、多入口、可治理的助手平台，OpenClaw 更贴近问题本身；如果目标是让 Agent 在长期运行中积累技能、形成风格、复用经验，Hermes 的抽象更自然。现实工程里，两类能力往往最终会逐步靠拢，但在当前阶段，它们仍代表了两条不同的主线。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-4">人工智能理论知识 - 智能体</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ai-knowledge-quick-ref-4/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>人工智能理论知识 - Transformers和大模型</title>
		<link>https://blog.gmem.cc/ai-knowledge-quick-ref-3</link>
		<comments>https://blog.gmem.cc/ai-knowledge-quick-ref-3#comments</comments>
		<pubDate>Wed, 15 Apr 2026 12:45:34 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=42151</guid>
		<description><![CDATA[<p>这一篇从常用算法进入机器学习基础概念、经典机器学习与神经网络，重点讨论“模型如何被构造、训练、评估与正则化”。前一篇给出了数学语言，这一篇开始进入真正的建模问题：数据怎样表示，损失怎样定义，优化怎样推进，不同模型家族各自擅长什么；再往后才会过渡到 Transformer 与大语言模型。 常用算法 基础数据结构和算法 这一节处理的核心问题是：当面对搜索、更新、统计、调度、最短路径、依赖分析或训练流水线等任务时，数据应该怎样组织，操作应该怎样执行，才能既正确又高效。数据结构（Data Structure）决定“数据在内存里如何表示”，算法（Algorithm）决定“在这种表示上如何完成查询、插入、删除、遍历、排序与优化”。很多系统性能问题，本质上不是算力不足，而是底层组织方式与操作方式不匹配。 可以把它理解成“仓库布局与搬运规则”的组合：同样一批货物，若排成连续货架、串成链式节点、组织成树状目录，或连接成路网，后续的查找、插入、合并与运输成本会完全不同。现代 AI 工程虽然把注意力集中在模型上，但数据加载器、特征流水线、参数缓存、向量检索、计算图调度、图学习和索引系统，最终都建立在这些基础结构之上。 结构 / 算法 核心能力 典型复杂度 常见场景 数组 / 动态数组 按下标随机访问；顺序扫描效率高 访问 ；中间插入/删除 <a class="read-more" href="https://blog.gmem.cc/ai-knowledge-quick-ref-3">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-3">人工智能理论知识 - Transformers和大模型</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>这一篇从常用算法进入机器学习基础概念、经典机器学习与神经网络，重点讨论“模型如何被构造、训练、评估与正则化”。前一篇给出了数学语言，这一篇开始进入真正的建模问题：数据怎样表示，损失怎样定义，优化怎样推进，不同模型家族各自擅长什么；再往后才会过渡到 Transformer 与大语言模型。</p>
<div class="blog_h1"><span class="graybg">常用算法</span></div>
<div class="blog_h2"><span class="graybg">基础数据结构和算法</span></div>
<p>这一节处理的核心问题是：当面对搜索、更新、统计、调度、最短路径、依赖分析或训练流水线等任务时，数据应该怎样组织，操作应该怎样执行，才能既正确又高效。数据结构（Data Structure）决定“数据在内存里如何表示”，算法（Algorithm）决定“在这种表示上如何完成查询、插入、删除、遍历、排序与优化”。很多系统性能问题，本质上不是算力不足，而是底层组织方式与操作方式不匹配。</p>
<p>可以把它理解成“仓库布局与搬运规则”的组合：同样一批货物，若排成连续货架、串成链式节点、组织成树状目录，或连接成路网，后续的查找、插入、合并与运输成本会完全不同。现代 AI 工程虽然把注意力集中在模型上，但数据加载器、特征流水线、参数缓存、向量检索、计算图调度、图学习和索引系统，最终都建立在这些基础结构之上。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">结构 / 算法</td>
<td style="text-align: center;">核心能力</td>
<td style="text-align: center;">典型复杂度</td>
<td style="text-align: center;">常见场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>数组 / 动态数组</td>
<td>按下标随机访问；顺序扫描效率高</td>
<td>访问 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>；中间插入/删除 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span></td>
<td>张量、批数据、embedding、排序、滑动窗口</td>
</tr>
<tr>
<td>链表</td>
<td>已知位置后插入/删除代价低</td>
<td>局部插删 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>；查找 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span></td>
<td>LRU、任务拼接、频繁重排的序列</td>
</tr>
<tr>
<td>栈（Stack）</td>
<td>后进先出（LIFO）</td>
<td>push / pop / top 均为 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span></td>
<td>递归展开、表达式解析、单调栈</td>
</tr>
<tr>
<td>队列（Queue）</td>
<td>先进先出（FIFO）</td>
<td>enqueue / dequeue 均为 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span></td>
<td>BFS、任务队列、流式缓冲</td>
</tr>
<tr>
<td>哈希表（Hash Table）</td>
<td>按键快速索引</td>
<td>平均查找/插入/删除 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span></td>
<td>字典、词表、缓存、去重</td>
</tr>
<tr>
<td>Bloom Filter</td>
<td>近似集合成员查询</td>
<td>插入/查询均为 <span displaypfx="inline-" class="mathjax-container">\(O(k)\)</span></td>
<td>缓存预检查、去重预过滤、存储层键存在性判断</td>
</tr>
<tr>
<td>树（Tree）</td>
<td>表达层次关系与有序结构</td>
<td>平衡查找常为 <span displaypfx="inline-" class="mathjax-container">\(O(\log n)\)</span></td>
<td>索引、优先队列、前缀匹配、规则分裂</td>
</tr>
<tr>
<td>图（Graph）</td>
<td>表达任意对象之间的关系</td>
<td>遍历通常为 <span displaypfx="inline-" class="mathjax-container">\(O(|V|+|E|)\)</span></td>
<td>社交网络、知识图谱、路线规划、依赖分析</td>
</tr>
</tbody>
</table>
<p>复杂度表只给出渐近上界，不能直接替代工程判断。真实系统还要同时考虑缓存友好性（Cache Locality）、常数项、并发开销、内存占用和实现复杂度。例如链表在理论上支持常数时间插入，但它对 CPU 缓存并不友好；数组在理论上中间插入较慢，但顺序扫描极快，因此在现代硬件上经常更有优势。</p>
<div class="blog_h3"><span class="graybg">数组与动态数组</span></div>
<p>数组（Array）处理的核心问题是：当元素类型一致、数量可以按顺序编号时，如何支持最低成本的随机访问与批量扫描。它的关键性质是<span style="background-color: #c0c0c0;">连续内存（Contiguous Memory）</span>。若每个元素大小为 <span displaypfx="inline-" class="mathjax-container">\(s\)</span>，首地址为 <span displaypfx="inline-" class="mathjax-container">\(\text{base}\)</span>，则第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个元素地址为</p>
<span displaypfx="" class="mathjax-container">\[\text{addr}(a_i)=\text{base}+i\cdot s\]</span>
<p>这个式子说明数组访问为何是 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>：位置可以直接计算，不需要沿指针逐步跳转。矩阵、张量、mini-batch、时间序列缓存、本地特征块和 embedding 表中的一行，本质上都依赖这种“地址可算”的结构。</p>
<p>数组的代价也非常明确：若在中间插入或删除元素，后面的元素必须整体搬移，因此复杂度通常是 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span>。这意味着数组适合“读多写少、顺序稳定”的任务，不适合“在任意位置频繁插入”的任务。</p>
<p>动态数组（Dynamic Array）是在数组上的工程扩展：容量不足时申请更大的连续空间，把原有元素整体拷贝过去，再继续追加。一次扩容代价很高，但若容量按倍数增长，则追加操作的均摊（Amortized）复杂度仍可视为 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。Python 的 list、C++ 的 vector、Java 的 ArrayList 都遵循这一思想。</p>
<p>直觉上，数组像按编号排好的货架：拿第 137 件货非常快，但若要把一件货塞进中间，后面整排货物都要整体后移。</p>
<div class="blog_h3"><span class="graybg">链表</span></div>
<p>链表（Linked List）处理的是另一类问题：当序列顺序经常变化时，能否避免数组那样的大规模搬移。链表不要求连续内存，而是让每个节点（Node）保存数据和指向下一个节点的指针（Pointer）；双向链表（Doubly Linked List）还会额外保存前驱指针。</p>
<p>若已经拿到某个节点的位置，那么在其前后插入或删除节点只需要调整局部指针，代价通常是 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。但链表无法像数组那样通过下标直接定位第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个元素，查找往往必须从头逐个走过去，因此通常是 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span>。</p>
<p>链表适合做“结构改动频繁、定位方式不是按下标而是按已有节点句柄”的任务。例如 LRU 缓存中，经常需要把刚访问的元素移到头部；若配合哈希表记录节点位置，链表就能高效完成重排。</p>
<p>链表像一串用绳子串起来的标签。改顺序很方便，但想直接摸到第 500 个标签，就只能沿着绳子一个个数过去。</p>
<div class="blog_h3"><span class="graybg">栈（Stack）</span></div>
<p>栈（Stack）定义的是一种<span style="background-color: #c0c0c0;">后进先出（Last In First Out, LIFO）</span>的访问约束。它处理的问题不是“怎样存更多数据”，而是“怎样强制最近进入的状态最先退出”。典型操作包括入栈 push、出栈 pop 与查看栈顶 top，它们都发生在同一端，因此实现代价通常是 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。</p>
<p>函数调用栈、递归回溯、表达式求值、括号匹配、深度优先搜索中的显式状态保存，都依赖这种结构。其本质是把“尚未处理完的上下文”按嵌套顺序压起来，等内部任务结束后再按相反顺序恢复。</p>
<p>单调栈（Monotonic Stack）是栈在算法中的重要变体。它通过维护一个单调递增或单调递减的栈，把“下一个更大元素”“柱状图最大矩形”等问题从 <span displaypfx="inline-" class="mathjax-container">\(O(n^2)\)</span> 降到 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span>。原因在于每个元素最多入栈和出栈各一次。</p>
<div class="blog_h3"><span class="graybg">队列（Queue）</span></div>
<p>队列（Queue）定义的是<span style="background-color: #c0c0c0;">先进先出（First In First Out, FIFO）</span>的访问约束。进入得早的元素先被处理，后来进入的元素排在尾部等待。它适合表达“任务排队、波前扩张、按到达顺序消费”的过程。</p>
<p>广度优先搜索（Breadth-First Search, BFS）之所以使用队列，正是因为 BFS 要按距离层层扩展：先处理距离起点为 1 的节点，再处理距离为 2 的节点。这个“分层推进”机制与 FIFO 完全一致。</p>
<p>循环队列（Circular Queue）通过把底层数组首尾相连，可以避免频繁搬移；双端队列（Deque）则允许两端都做插入和删除，因此能够支持滑动窗口最值、0-1 BFS 等更复杂的算法模式。</p>
<div class="blog_h3"><span class="graybg">哈希表（Hash Table）</span></div>
<p>哈希表（Hash Table）处理的核心问题是：当数据按“键（Key）”组织，而不是按位置组织时，如何快速找到对应的值（Value）。其思想是先通过哈希函数（Hash Function）把键映射成一个整数，再把这个整数映射到桶（Bucket）或槽位（Slot）上。</p>
<p>若哈希函数分布均匀，且装载因子（Load Factor）控制合理，则查找、插入和删除的平均复杂度都可接近 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。这正是词表映射、去重、缓存索引、参数名字典和特征 ID 映射大量采用哈希表的原因。</p>
<p>哈希表的难点在冲突（Collision）处理。多个键可能映射到同一位置，常见解决方案包括链地址法（Separate Chaining）和开放定址法（Open Addressing）。因此“哈希表平均 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>”并不意味着永远常数时间，它依赖于哈希函数质量、负载控制和冲突处理策略。</p>
<div class="blog_h4"><span class="graybg">Bloom Filter（布隆过滤器）</span></div>
<p>Bloom Filter 本质上属于<span style="background-color: #c0c0c0;">概率型数据结构（Probabilistic Data Structure）</span>，更准确地说，是一种近似集合成员查询结构（Approximate Membership Query, AMQ）。它解决的问题并不是“把元素精确存下来”，而是“用极小内存快速判断某元素是否可能出现过”。因此它通常作为哈希表、数据库索引或缓存系统之前的一层预过滤结构。</p>
<p>Bloom Filter 由一个长度为 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 的比特数组 <span displaypfx="inline-" class="mathjax-container">\(B\in\{0,1\}^m\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个哈希函数 <span displaypfx="inline-" class="mathjax-container">\(h_1,\dots,h_k\)</span> 构成，其中每个哈希函数都把元素 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 映射到区间 <span displaypfx="inline-" class="mathjax-container">\(\{0,1,\dots,m-1\}\)</span> 中的一个位置。插入元素 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时，执行</p>
<span displaypfx="" class="mathjax-container">\[B[h_1(x)]=B[h_2(x)]=\cdots=B[h_k(x)]=1\]</span>
<p>查询元素 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时，检查 <span displaypfx="inline-" class="mathjax-container">\(B[h_1(x)],\dots,B[h_k(x)]\)</span>。只要其中至少有一个位置为 0，就可以断定 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 一定不在集合中；若这些位置全部为 1，则只能说明 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 可能在集合中。</p>
<p>这一定义直接带来 Bloom Filter 最重要的判定性质：<span style="background-color: #c0c0c0;">它允许假阳性（False Positive），但不允许假阴性（False Negative）</span>。原因在于，不同元素可能把同一批 bit 位置反复置为 1，于是一个从未插入过的元素也可能“碰巧”命中全 1；但只要某个位置仍为 0，就说明没有任何已插入元素覆盖过这条哈希路径，因此该元素一定不存在。</p>
<p>设一共插入了 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个元素，则某个 bit 在所有插入结束后仍为 0 的概率近似为 <span displaypfx="inline-" class="mathjax-container">\(\left(1-\frac{1}{m}\right)^{kn}\approx e^{-kn/m}\)</span>。于是查询一个未出现元素时， <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个位置恰好都为 1 的假阳性概率近似为</p>
<span displaypfx="" class="mathjax-container">\[p\approx \left(1-e^{-kn/m}\right)^k\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 是 bit 数组长度， <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是已插入元素数， <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 是哈希函数数目， <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是假阳性概率。这个公式揭示了 Bloom Filter 的基本权衡： <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 越大，冲突越少； <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 越大，数组越接近被“染满”； <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 太小会降低区分能力，太大则会过度占满 bit 位。固定 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 时，常见的近似最优选择是</p>
<span displaypfx="" class="mathjax-container">\[k\approx \frac{m}{n}\ln 2\]</span>
<p>直觉上，Bloom Filter 像一排共享的指示灯。每来一个元素，就按亮若干盏灯；查询时，只要对应灯中有一盏没亮，就可以确认它从未出现过。若全部亮着，也只能说明“这些灯曾被某些元素点亮过”，却不能保证就是当前这个元素点亮的。</p>
<p>Bloom Filter 最适合用于“先快速排除绝大多数不存在项，再把少量可疑项交给精确结构复核”的场景。例如缓存系统可先判断某个 key 是否可能在缓存中，若 Bloom Filter 直接给出“不在”，就可以避免无意义回源；LSM-Tree 存储系统可用它判断某个键是否可能存在于某个 SSTable；爬虫去重、黑名单预过滤、向量检索候选预筛都大量使用这一思想。</p>
<p>Bloom Filter 的边界也很明确。第一，它不保存原始元素，因此不能枚举集合内容，也不能像哈希表那样返回关联值。第二，标准 Bloom Filter 不支持安全删除，因为把某个 bit 清零可能误伤其他元素留下的痕迹；若确实需要删除，通常要改用计数 Bloom Filter（Counting Bloom Filter）。第三，当假阳性代价非常高、系统需要完全精确的成员判断时，应优先使用哈希表、B 树或其他精确索引结构。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/bloom.jpg"><img class="alignnone size-full wp-image-41339" src="https://blog.gmem.cc/wp-content/uploads/2026/03/bloom.jpg" alt="bloom" width="1024" height="559" /></a></p>
<div class="blog_h3"><span class="graybg">树（Tree）</span></div>
<p>树（Tree）处理的是“层次结构”和“递归划分”问题。树中的节点之间具有父子关系，除了根节点（Root）外，每个节点都有唯一父节点。它天然适合表达目录层级、决策分裂、区间划分、优先级组织与前缀共享。</p>
<p>树之所以重要，在于它把原本线性的搜索空间组织成递归结构，使很多操作能通过“向左还是向右”“进入哪个子树”逐步缩小问题规模。若每次都能把候选空间缩小到原来的一半，复杂度就会从线性级下降到对数级。</p>
<div class="blog_h4"><span class="graybg">二叉树与遍历</span></div>
<p>二叉树（Binary Tree）规定每个节点至多有两个孩子。前序遍历（Preorder）、中序遍历（Inorder）、后序遍历（Postorder）和层序遍历（Level-order）分别对应不同的信息读取顺序：前序适合序列化结构，中序适合读取二叉搜索树中的有序键，后序适合先处理子问题再合并，层序适合按深度观察整体形状。</p>
<div class="blog_h4"><span class="graybg">二叉搜索树与平衡树</span></div>
<p>二叉搜索树（Binary Search Tree, BST）在每个节点上保持“左子树键值更小、右子树键值更大”的顺序约束，因此查找、插入和删除都可以沿着比较路径进行。若树高度为 <span displaypfx="inline-" class="mathjax-container">\(h\)</span>，这些操作的复杂度一般与 <span displaypfx="inline-" class="mathjax-container">\(O(h)\)</span> 成正比。</p>
<p>问题在于普通 BST 在极端情况下会退化成链表，此时 <span displaypfx="inline-" class="mathjax-container">\(h=n\)</span>。平衡树（Balanced Tree）如 AVL 树、红黑树（Red-Black Tree）通过旋转（Rotation）维护高度受控，使 <span displaypfx="inline-" class="mathjax-container">\(h=O(\log n)\)</span>，从而把查找、插入和删除稳定在对数复杂度。数据库索引和有序映射容器大量依赖这一思想。</p>
<div class="blog_h4"><span class="graybg">堆与优先队列</span></div>
<p>堆（Heap）维护的是局部顺序，而不是整棵树的全局有序性：在最小堆（Min-Heap）中，每个父节点都不大于子节点，因此根节点始终是全局最小值；最大堆（Max-Heap）则相反。它通常用数组实现，父子下标关系可以直接计算。</p>
<p>堆最适合实现优先队列（Priority Queue）：每次都要快速取出当前最重要、最小或最大的元素时，插入和弹出都只需 <span displaypfx="inline-" class="mathjax-container">\(O(\log n)\)</span>。Dijkstra、A* 搜索、任务调度、Top-K 维护和流式中位数都大量依赖优先队列。</p>
<div class="blog_h4"><span class="graybg">Trie 与前缀结构</span></div>
<p>Trie 树（Prefix Tree）把字符串按前缀共享组织起来。若插入单词集合 <span displaypfx="inline-" class="mathjax-container">\(\{w_1,\dots,w_m\}\)</span>，公共前缀只存一次，因此“是否存在某个前缀”“以某前缀开头的词有多少”都可以沿字符路径直接完成。</p>
<p>Trie 特别适合词典匹配、自动补全、敏感词过滤和子词切分。它牺牲了一部分空间，换来按字符长度而非按词典规模进行搜索的能力。</p>
<div class="blog_h3"><span class="graybg">图（Graph）</span></div>
<p>图（Graph）处理的是最一般的关系结构。若顶点集合为 <span displaypfx="inline-" class="mathjax-container">\(V\)</span>，边集合为 <span displaypfx="inline-" class="mathjax-container">\(E\)</span>，则图可写成 <span displaypfx="inline-" class="mathjax-container">\(G=(V,E)\)</span>。树本质上是图的一个特殊子类，但图允许环、允许多条连接、允许方向和权重，因此能表达社交关系、知识链接、网页跳转、道路网络、依赖图与神经网络计算图。</p>
<p>图的常见表示方式有邻接矩阵（Adjacency Matrix）和邻接表（Adjacency List）。前者适合稠密图，能 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span> 判断两点是否相连；后者适合稀疏图，空间复杂度更低，遍历邻居更高效。</p>
<div class="blog_h4"><span class="graybg">BFS 与 DFS</span></div>
<p>广度优先搜索（BFS）与深度优先搜索（DFS）是图遍历的两种基本组织方式。BFS 使用队列按层推进，适合无权最短路、层次扩展与最少步数问题；DFS 使用递归或显式栈沿一条路径尽量走深，适合回溯、环检测、拓扑排序、强连通分量与树形动态规划。</p>
<p>对邻接表表示的图，两者的时间复杂度通常都是 <span displaypfx="inline-" class="mathjax-container">\(O(|V|+|E|)\)</span>。区别不在渐近复杂度，而在访问顺序：BFS 保证按距离层层扩展，DFS 更擅长描述“先深入、后回退”的结构性问题。</p>
<div class="blog_h4"><span class="graybg">最短路径</span></div>
<p>最短路径（Shortest Path）问题处理的是：从起点到终点，总代价最小的路径是什么。若图无权，BFS 就能得到边数最少的路径；若边权非负，常用 Dijkstra 算法。它每次从优先队列中取出当前距离估计最小的顶点，并尝试松弛（Relax）相邻边。</p>
<p>Dijkstra 的核心更新为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}[v]=\min\big(\mathrm{dist}[v],\mathrm{dist}[u]+w(u,v)\big)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}[u]\)</span> 是当前已知的从源点到 <span displaypfx="inline-" class="mathjax-container">\(u\)</span> 的最短距离估计， <span displaypfx="inline-" class="mathjax-container">\(w(u,v)\)</span> 是边权。这个公式表达的是最短路的本质：若“先到 <span displaypfx="inline-" class="mathjax-container">\(u\)</span>，再走到 <span displaypfx="inline-" class="mathjax-container">\(v\)</span>”更便宜，就更新对 <span displaypfx="inline-" class="mathjax-container">\(v\)</span> 的距离认知。</p>
<div class="blog_h4"><span class="graybg">拓扑排序</span></div>
<p>拓扑排序（Topological Sort）处理的是有向无环图（Directed Acyclic Graph, DAG）中的依赖顺序。若边 <span displaypfx="inline-" class="mathjax-container">\(u\to v\)</span> 表示“<span displaypfx="inline-" class="mathjax-container">\(u\)</span> 必须先于 <span displaypfx="inline-" class="mathjax-container">\(v\)</span>”，那么拓扑序就是一种满足全部先后约束的线性排列。</p>
<p>课程先修关系、编译依赖、工作流调度、神经网络计算图执行次序，本质上都属于这一问题。拓扑排序的价值不只是“排出一个顺序”，而是把依赖图转成一条能够实际执行的流水线。</p>
<div class="blog_h4"><span class="graybg">最小生成树（Minimum Spanning Tree, MST）</span></div>
<p>最小生成树（Minimum Spanning Tree, MST）处理的是这样的问题：给定一个连通无向带权图 <span displaypfx="inline-" class="mathjax-container">\(G=(V,E)\)</span>，需要从边集合 <span displaypfx="inline-" class="mathjax-container">\(E\)</span> 中选出一部分边，把所有顶点连成一个整体，同时不产生环，并使总权重最小。若把所有生成树的集合记为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{T}(G)\)</span>，则标准形式可以写成</p>
<span displaypfx="" class="mathjax-container">\[T^*=\arg\min_{T\in \mathcal{T}(G)}\sum_{e\in T} w(e)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(T^*\)</span> 是最优生成树；<span displaypfx="inline-" class="mathjax-container">\(w(e)\)</span> 是边 <span displaypfx="inline-" class="mathjax-container">\(e\)</span> 的权重；<span displaypfx="inline-" class="mathjax-container">\(\sum_{e\in T} w(e)\)</span> 表示树中全部边的总代价。这里的“生成树”有三个同时成立的约束：第一， <span displaypfx="inline-" class="mathjax-container">\(T\subseteq E\)</span>；第二，图在边集 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 下必须连通；第三， <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 不能含环，因此边数必然满足 <span displaypfx="inline-" class="mathjax-container">\(|T|=|V|-1\)</span>。</p>
<p>这个定义明确了 MST 不是“找一棵看上去便宜的树”，而是在<span style="background-color: #c0c0c0;">所有能够覆盖全部顶点的无环连通方案</span>中做全局最小化。只强调“连通”会多出冗余边，只强调“边权小”又可能导致图不连通；MST 同时满足这两个条件。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mst-graph.png"><img class="alignnone size-full wp-image-41335" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mst-graph.png" alt="mst-graph" width="6242" height="2905" /></a></p>
<p>直觉上，可以把 MST 理解成“以最低总造价把一组城市接通，但不修多余的回路”。如果形成了环，说明这条网络中至少有一条边是重复支出；如果某些城市没有接入，说明方案根本不可用。MST 就是在“全覆盖”和“最低成本”之间取得最紧的平衡。</p>
<p>MST 成立的核心理论基础是<span style="background-color: #c0c0c0;">割性质（Cut Property）</span>：把顶点集切成两个不相交部分后，跨越这个切分的最小权边，一定存在于某棵最小生成树中。这个性质的含义是：局部最便宜的“安全边（Safe Edge）”可以被逐步加入，而不会破坏全局最优性。Prim 与 Kruskal 虽然组织方式不同，但本质上都在不断选择这样的安全边。</p>
<p>Prim 算法的思路是“从一个起点向外生长一棵树”。设当前已经纳入树中的顶点集合为 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，则 Prim 每一步都在所有满足 <span displaypfx="inline-" class="mathjax-container">\(u\in S,\ v\notin S\)</span> 的边中，选择权重最小的一条，把新顶点接入当前树。这个过程像不断把新城市接入已经建好的主干网，因此特别适合用优先队列维护“当前边界上最便宜的边”。若图用邻接表存储并配合二叉堆实现优先队列，时间复杂度通常为 <span displaypfx="inline-" class="mathjax-container">\(O(|E|\log |V|)\)</span>。</p>
<p>Kruskal 算法的思路是“按全图范围从便宜到昂贵依次选边”。它先对所有边按权重升序排序，然后从小到大扫描：若当前边连接的是两个不同连通块，就把它加入结果；若会在当前结构中形成环，就跳过。为了高效判断“两个端点是否已经连通”，Kruskal 通常配合并查集（Disjoint Set Union, DSU）。排序代价主导总复杂度，因此复杂度通常写成 <span displaypfx="inline-" class="mathjax-container">\(O(|E|\log |E|)\)</span>，与 <span displaypfx="inline-" class="mathjax-container">\(O(|E|\log |V|)\)</span> 在数量级上接近。</p>
<p>两种算法解决的是同一个优化问题，但适合的工程语境不同。Prim 更像“从局部网络不断扩张”，适合稠密图或从某个核心节点逐步向外建设的场景；Kruskal 更像“全局看所有候选边，再逐一合并连通块”，在边集天然可排序、图较稀疏时实现尤其直接。</p>
<p>一个最小例子可以把公式和过程连起来。设四个顶点 <span displaypfx="inline-" class="mathjax-container">\(A,B,C,D\)</span>，边权为： <span displaypfx="inline-" class="mathjax-container">\(w(A,B)=1\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(B,C)=2\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(A,C)=4\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(B,D)=3\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(C,D)=5\)</span>。Kruskal 会先按边权排序： <span displaypfx="inline-" class="mathjax-container">\((A,B),(B,C),(B,D),(A,C),(C,D)\)</span>。前 3 条边分别把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 连起来，此时已经得到 <span displaypfx="inline-" class="mathjax-container">\(|V|-1=3\)</span> 条边，且图连通无环，于是生成树为</p>
<span displaypfx="" class="mathjax-container">\[T=\{(A,B),(B,C),(B,D)\}\]</span>
<p>其总代价为</p>
<span displaypfx="" class="mathjax-container">\[\sum_{e\in T}w(e)=1+2+3=6\]</span>
<p>若改选边集 <span displaypfx="inline-" class="mathjax-container">\(\{(A,B),(A,C),(B,D)\}\)</span>，总代价是 <span displaypfx="inline-" class="mathjax-container">\(1+4+3=8\)</span>；若再加入 <span displaypfx="inline-" class="mathjax-container">\((B,C)\)</span>，虽然成本局部看不高，但边数会超过 <span displaypfx="inline-" class="mathjax-container">\(|V|-1\)</span> 并形成环，因此不再是树。这个例子把“最低成本”“连通”“无环”三项约束如何同时生效展示得很清楚。</p>
<p>MST 常见于网络布线、电力传输、骨架路网设计、图像分割、聚类和图压缩。层次聚类中的单链接（Single Linkage）就可以通过图的最小生成树来理解：先把点看成顶点，把样本间距离看成边权，再在 MST 上剪断最长的若干条边，就得到若干连通簇。这也是为什么 MST 不只是图论题型，而是很多数据分析与机器学习方法的底层结构。</p>
<p>MST 也有明确边界。它只适用于<span style="background-color: #c0c0c0;">无向、连通、带权</span>图上的“全连通最低总成本”问题；若任务要求的是“从源点到其余点的最短路”，应使用最短路径算法；若图有方向，目标就不再是普通 MST，而会进入最小树形图（Minimum Arborescence）等更复杂的问题。</p>
<div class="blog_h2"><span class="graybg">动态规划（Dynamic Programming, DP）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>动态规划（Dynamic Programming, DP）处理的是这样一类问题：目标是求一个全局最优值、最优路径，或所有路径的总和，但如果直接把所有可能性全部枚举出来，计算量会迅速爆炸。它常见于序列决策、路径规划、字符串匹配、图搜索，以及隐马尔可夫模型（HMM）、条件随机场（CRF）这类结构化预测模型。</p>
<p>这类问题通常有两个共同特征。第一，<span style="background-color: #c0c0c0;">重叠子问题（Overlapping Subproblems）</span>：同一个中间子问题会被反复计算。第二，<span style="background-color: #c0c0c0;">最优子结构（Optimal Substructure）</span>：大问题的最优解可以由小问题的最优解递推得到。例如，在长度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 的序列上，若每一步有 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|\)</span> 个可能状态，直接枚举所有状态路径往往需要考虑 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|^T\)</span> 条候选路径；当 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 稍大时，这种暴力方法几乎不可用。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>动态规划的核心在于：<span style="background-color: #c0c0c0;">先定义能够代表子问题的状态，再写出状态之间的递推关系，并把已经算过的结果缓存下来复用</span>。因此，它本质上是一种计算组织方式，而不是某个固定公式。</p>
<p>一个直观比喻是出差换乘。设想要从起点出发，经过很多站点，最终到达目的地。暴力法会把“到达每一站的所有走法”全部记下来；动态规划不会这样做。它只会为每个中间站保留一份最有价值的摘要，例如“到达这个站的最低成本”或“到达这个站的最大得分”。当继续前往下一站时，系统只需要查这份账本，而不必回头展开所有历史路径。</p>
<p>因此，动态规划通常包含四个步骤：定义状态、定义边界条件、写出转移方程、确定计算顺序。状态定义决定“中间结果要存什么”；边界条件决定“第一步从哪里开始”；转移方程决定“当前结果如何从更小问题得到”；计算顺序则保证所有依赖项在使用前已经计算完毕。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/dp.jpg"><img class="alignnone size-full wp-image-41269" src="https://blog.gmem.cc/wp-content/uploads/2026/03/dp.jpg" alt="dp" width="1408" height="768" /></a></p>
<div class="blog_h3"><span class="graybg">为什么局部次优前缀不会漏掉全局最优</span></div>
<p>动态规划能够丢弃大量“暂时看起来不够好”的前缀路径，前提是<span style="background-color: #c0c0c0;">状态（State）已经完整刻画了未来决策所需的全部信息</span>。一旦这个条件成立，到达同一状态的两条前缀路径，未来能够接上的可行后缀集合完全相同，因此只需要保留其中更优的那一条。</p>
<p>设两条前缀路径都到达同一状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span>，其当前累计代价分别为 <span displaypfx="inline-" class="mathjax-container">\(f_1(s)\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(f_2(s)\)</span>，且 <span displaypfx="inline-" class="mathjax-container">\(f_1(s)\le f_2(s)\)</span>。若从状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 出发，后续任意可行决策产生的附加代价记为 <span displaypfx="inline-" class="mathjax-container">\(g(s)\)</span>，并且这个附加代价只由当前状态决定，而不再依赖此前的完整历史，则有</p>
<span displaypfx="" class="mathjax-container">\[f_1(s)+g(s)\le f_2(s)+g(s)\]</span>
<p>这个不等式表明：在同一状态上，较差的前缀会被较优前缀完全支配（Dominated）。无论后面接哪一段后缀路径，较差前缀都不可能反超。因此 Bellman 最优性原理允许动态规划只保留“到达该状态的最优值”，而不必保留全部历史路径。</p>
<p>所谓“一个当前次优的路径，后来却通向全局最优”，本质上对应另一种情形：这条路径与当前更优路径虽然看起来到达了同一个位置，但它们对未来的可行动作并不相同。此时它们实际上并不属于同一个状态，而是状态定义缺失了关键信息。</p>
<p>一个典型例子是带资源约束的路径规划。若状态只写成当前位置 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span>，那么两条到达同一格子的路径会被合并；但若其中一条还保留一次传送机会，另一条已经把传送用掉，则它们未来的决策空间显然不同。正确的状态应扩展为 <span displaypfx="inline-" class="mathjax-container">\((i,j,\mathrm{used})\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\((i,j,\mathrm{fuel})\)</span> 这类更完整的形式。只有在状态把“剩余资源、上一步动作、已使用预算、是否持仓”等会影响未来的因素都编码进去后，动态规划的剪枝才是安全的。</p>
<p>因此，动态规划处理“局部次优可能导向全局最优”的方式，不是保留所有看起来有潜力的路径，而是通过<span style="background-color: #c0c0c0;">正确设计状态，使真正会影响未来的差异体现在不同状态上</span>。同一状态内部只保留最优前缀；不同状态之间分别递推。动态规划的正确性，最终依赖的正是这一点：未来只依赖当前状态，而不依赖通向当前状态的完整历史。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若记 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 为阶段或时间步， <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 为当前状态，一个非常典型的动态规划写法是：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[t,s]=\max_{s'\in \mathrm{Prev}(s)}\left(\mathrm{DP}[t-1,s']+\mathrm{score}(s',s,t)\right)\]</span>
<p>这条式子表达的是：要得到“第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步处于状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 时的最优值”，不必重新枚举所有完整路径，而是只需查看所有能够转移到 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 的前驱状态 <span displaypfx="inline-" class="mathjax-container">\(s'\)</span>，并在它们已有的最优值基础上，加上这一步的局部得分，再从中取最大。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[t,s]\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步、状态为 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 的最优子问题值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Prev}(s)\)</span>：所有可以转移到状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 的前驱状态集合。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{score}(s',s,t)\)</span>：从 <span displaypfx="inline-" class="mathjax-container">\(s'\)</span> 转移到 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 时，在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步新增的局部得分或代价。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\max\)</span>：表示当前任务要找“最好的一条路径”。若任务目标是最小代价，则可改为 <span displaypfx="inline-" class="mathjax-container">\(\min\)</span>；若任务目标是把所有路径概率加总，则可改为 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span>。</li>
</ul>
<p>边界条件通常写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[1,s]=\mathrm{init}(s)+\mathrm{local}(s,1)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{init}(s)\)</span> 表示序列从状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 开始的初始代价或初始分数， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{local}(s,1)\)</span> 表示第一步在该状态产生的局部贡献。没有这个起点，后续递推就无从展开。</p>
<p>动态规划真正带来的收益来自复杂度压缩。以一阶序列模型为例，若每一步有 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|\)</span> 个候选状态、总长度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span>，暴力枚举往往需要考虑 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|^T\)</span> 条完整路径；而若采用“时间步 + 当前状态”的动态规划状态定义，则通常只需在每个时间步枚举所有前驱状态，计算复杂度可以降为 <span displaypfx="inline-" class="mathjax-container">\(O(T|\mathcal{S}|^2)\)</span>。这种从指数级到多项式级的下降，正是动态规划在序列模型中不可替代的原因。</p>
<p>若任务不仅要求最优值，还要求恢复最优路径，则通常还会额外保存“当前最优值来自哪个前驱状态”的回溯信息（Backpointer）。这意味着动态规划不仅能回答“最优值是多少”，还能回答“这条最优路径具体怎么走”。</p>
<p>更重要的是，动态规划并不只对应一种运算。对于最优路径问题，递推中的核心运算往往是 <span displaypfx="inline-" class="mathjax-container">\(\max\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\min\)</span>；对于总概率、配分函数这类问题，核心运算则是 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span>。这也是为什么 HMM 的维特比算法、前向算法，以及 CRF 的前向后向算法，虽然目标不同，但都属于动态规划。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在 HMM 中，动态规划最典型地体现在两类问题上。第一类是维特比算法（Viterbi Algorithm）：它要求“给定观测序列后，哪一条隐藏状态路径最可能”，因此递推中的核心运算是 <span displaypfx="inline-" class="mathjax-container">\(\max\)</span>。第二类是前向算法（Forward Algorithm）：它要求“所有隐藏状态路径合起来，总概率是多少”，因此递推中的核心运算是 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span>。两者使用的是同一张状态网格，只是“每一步如何聚合前驱信息”不同。</p>
<p>在 CRF 中，动态规划同样是核心计算工具。训练时，需要对所有可能标签路径做归一化，这对应配分函数（Partition Function）的计算；解码时，需要找得分最高的那条标签路径，这对应最优路径搜索。在线性链 CRF 中，这两件事都可以通过“时间步 + 当前标签”的动态规划状态来高效完成，否则若直接枚举所有标签序列，计算量会随序列长度呈指数增长。</p>
<p>因此，在机器学习语境里，动态规划可以概括为：<span style="background-color: #c0c0c0;">把原本必须整体枚举的结构化问题，改写成一系列局部状态上的递推计算，并通过缓存中间结果把重复计算消掉</span>。一旦看到“序列路径很多、局部决策可递推、同类子问题会重复出现”这三个信号，通常就应该优先考虑动态规划。</p>
<div class="blog_h4"><span class="graybg">例子 1：编辑距离（Edit Distance）</span></div>
<p>设字符串 <span displaypfx="inline-" class="mathjax-container">\(A=a_1a_2\dots a_m\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(B=b_1b_2\dots b_n\)</span>。编辑距离要回答的问题是：至少经过多少次插入、删除、替换，才能把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>。若直接枚举所有编辑序列，可能性会指数增长；但若定义 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[i,j]\)</span> 表示“把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的前 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个字符变成 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 的前 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个字符所需的最小编辑次数”，问题就能递推解决。</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[i,j]=\min\begin{cases}\mathrm{DP}[i-1,j]+1\\ \mathrm{DP}[i,j-1]+1\\ \mathrm{DP}[i-1,j-1]+\mathbf{1}(a_i\ne b_j)\end{cases}\]</span>
<p>这里三项分别对应：删除 <span displaypfx="inline-" class="mathjax-container">\(a_i\)</span>、插入 <span displaypfx="inline-" class="mathjax-container">\(b_j\)</span>、或把 <span displaypfx="inline-" class="mathjax-container">\(a_i\)</span> 替换成 <span displaypfx="inline-" class="mathjax-container">\(b_j\)</span>（若本来相同，则替换代价为 0）。边界条件是 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[0,j]=j\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[i,0]=i\)</span>，因为空串变成长度为 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的串需要做 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 次插入，反之需要做 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 次删除。这个例子非常典型地体现了动态规划：状态是前缀长度，转移是三种编辑操作，目标是求最小总代价。</p>
<div class="blog_h4"><span class="graybg">例子 2：网格最短路径（Grid Shortest Path）</span></div>
<p>设一个 <span displaypfx="inline-" class="mathjax-container">\(m\times n\)</span> 网格，每个格子 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 都有进入代价 <span displaypfx="inline-" class="mathjax-container">\(w_{i,j}\)</span>，只能向右或向下移动。问题是：从左上角走到右下角的最小总代价是多少。若定义 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[i,j]\)</span> 表示“到达格子 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 的最小总代价”，则递推很直接：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[i,j]=w_{i,j}+\min\big(\mathrm{DP}[i-1,j],\mathrm{DP}[i,j-1]\big)\]</span>
<p>因为到达 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 只有两种可能：从上方 <span displaypfx="inline-" class="mathjax-container">\((i-1,j)\)</span> 走下来，或从左侧 <span displaypfx="inline-" class="mathjax-container">\((i,j-1)\)</span> 走过来。边界条件是第一行与第一列只能沿单一路径累计。这个例子说明，动态规划并不局限于字符串或序列模型；只要问题具有“局部来源有限、全局目标可递推”的结构，就可以用同样的思路求解。</p>
<div class="blog_h2"><span class="graybg">贪心算法（Greedy Algorithm）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>贪心算法（Greedy Algorithm）处理的是这样一类问题：希望快速构造一个全局可行解，并且每一步都只做当前看来最优的局部选择，而不回头修改已经作出的决定。它广泛出现在排序、调度、压缩、近似优化，以及许多机器学习训练与推断流程中。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>贪心的核心假设是：<span style="background-color: #c0c0c0;">当前最好的局部选择，能够导向全局最优，或至少导向足够好的近似解</span>。它像走山路时每一步都先选眼前最高、最稳的落脚点，而不是先把整座山的所有路径都完全规划出来。贪心的优势是快、简单、容易实现；风险是局部最优未必等于全局最优。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若记第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步可选动作集合为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{A}_t\)</span>，一个抽象的贪心选择可写为：</p>
<span displaypfx="" class="mathjax-container">\[a_t^*=\arg\max_{a\in\mathcal{A}_t}\ \mathrm{score}(a\mid \text{current state})\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{score}(a\mid \text{current state})\)</span> 是当前状态下动作 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 的局部收益；<span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 表示从所有可选动作里挑出得分最高的那个。贪心算法关心的是“眼下哪一步最好”，而不是“未来所有步骤联合起来后哪条完整路径最好”。</p>
<p>因此，贪心方法是否正确，取决于问题本身是否满足贪心选择性质（Greedy-choice Property）。如果这个性质成立，局部最优就能拼成全局最优；如果不成立，贪心通常只能作为启发式方法或近似算法。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>决策树训练就是一个典型例子。每个节点都不会提前规划整棵树的全局最优结构，而是只在当前节点上选择信息增益、基尼下降或误差下降最大的切分。这个过程本质上就是贪心：<span style="background-color: #c0c0c0;">每一步都先把当前最值得切的地方切开</span>。它训练快、解释性强，但也正因为是局部选择，单棵树通常不是全局最优树结构。</p>
<div class="blog_h2"><span class="graybg">分治算法（Divide and Conquer）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>分治算法（Divide and Conquer）处理的是“大问题可以被拆成若干个同结构小问题”的场景。它广泛出现在排序、搜索、矩阵运算、索引构建，以及大规模数据处理与并行计算中。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>分治的思想可以概括为三步：<span style="background-color: #c0c0c0;">分解（Divide）— 递归求解（Conquer）— 合并（Combine）</span>。它像整理一大堆文档时，先按主题拆成若干小堆，再分别处理，最后再合并成有序结果。与动态规划不同，分治更强调“子问题相互独立”，而不是“子问题结果需要反复复用”。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>分治算法的时间复杂度常写成递推式：</p>
<span displaypfx="" class="mathjax-container">\[T(n)=aT\left(\frac{n}{b}\right)+f(n)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是问题规模；<span displaypfx="inline-" class="mathjax-container">\(a\)</span> 表示被拆成多少个子问题；<span displaypfx="inline-" class="mathjax-container">\(n/b\)</span> 是每个子问题的规模；<span displaypfx="inline-" class="mathjax-container">\(f(n)\)</span> 是“分解 + 合并”本身的额外代价。这个式子不告诉我们具体怎么做，但它准确描述了分治算法的结构骨架。</p>
<p>例如，归并排序（Merge Sort）把长度为 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 的数组分成两个规模约为 <span displaypfx="inline-" class="mathjax-container">\(n/2\)</span> 的子数组，递归排好序后再线性合并，因此它的复杂度递推就是 <span displaypfx="inline-" class="mathjax-container">\(T(n)=2T(n/2)+O(n)\)</span>。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在机器学习工程中，分治思想常见于大规模近邻索引构建。例如构建 kd-tree 时，算法会按某个维度把样本集递归切成两半，再在左右子集上继续构树。这样得到的层次化空间划分，能显著加速后续的近邻搜索。它的本质并不是“学习一个模型”，而是通过递归拆分把原本需要全表扫描的搜索过程组织得更高效。</p>
<div class="blog_h2"><span class="graybg">图搜索与最短路径</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>许多软件与机器学习问题都可以抽象成图（Graph）：节点（Node）表示状态、样本、词、网页或知识实体，边（Edge）表示转移、相似性、依赖关系或可达关系。图搜索与最短路径算法要回答的问题是：如何从起点高效找到目标节点，或找到总代价最小的一条路径。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>图搜索的核心是“沿着边扩展状态空间，但尽量避免无意义的重复探索”。无权图最短路径常用广度优先搜索（BFS），因为它按层扩展，第一次到达目标通常就是步数最少的路径；带非负权图常用 Dijkstra，因为它总是优先扩展当前总代价最小的候选节点。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>带权最短路径算法里的基本更新步骤通常写成“松弛（Relaxation）”：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}(v)=\min\big(\mathrm{dist}(v),\ \mathrm{dist}(u)+w(u,v)\big)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}(u)\)</span> 是当前已知从起点到节点 <span displaypfx="inline-" class="mathjax-container">\(u\)</span> 的最小代价， <span displaypfx="inline-" class="mathjax-container">\(w(u,v)\)</span> 是边 <span displaypfx="inline-" class="mathjax-container">\(u\rightarrow v\)</span> 的权重， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}(u)+w(u,v)\)</span> 则是“先到 <span displaypfx="inline-" class="mathjax-container">\(u\)</span> 再走到 <span displaypfx="inline-" class="mathjax-container">\(v\)</span>”这条新候选路径的总代价。若它比当前记录的 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}(v)\)</span> 更小，就更新。</p>
<p>这个式子看起来和动态规划很像，原因是二者都在做“由已知子结果递推新结果”。区别在于：图搜索更强调如何选择下一个要扩展的节点，以及如何在一般图结构中避免重复访问。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在语音识别、机器翻译和图搜索推断中，解码过程经常会把候选状态组织成图或格（Lattice）。此时，寻找最优输出序列本质上就是图上的路径搜索问题。很多动态规划解码器也可以从“图上最优路径”的角度理解，因此图搜索是连接通用软件算法与结构化机器学习推断的重要桥梁。</p>
<div class="blog_h2"><span class="graybg">二分查找（Binary Search）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>二分查找（Binary Search）处理的是“搜索空间有序，或可行性判断具有单调性”的问题。它不仅用于有序数组查找，也广泛用于阈值搜索、参数调优、数值逼近和工程系统中的边界定位。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>二分查找的核心是：<span style="background-color: #c0c0c0;">每次利用单调性砍掉一半搜索空间</span>。它像猜数字游戏：如果知道答案一定在某个区间里，而且中点左侧和右侧满足不同性质，那么每问一次都能把候选范围减半。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若当前搜索区间为 <span displaypfx="inline-" class="mathjax-container">\([l,r]\)</span>，中点通常取：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{mid}=\left\lfloor\frac{l+r}{2}\right\rfloor\]</span>
<p>接着依据单调判定函数 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{check}(\mathrm{mid})\)</span> 缩小区间：</p>
<ul>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{check}(\mathrm{mid})\)</span> 为真，说明答案在左半边或恰好是中点，则令 <span displaypfx="inline-" class="mathjax-container">\(r=\mathrm{mid}\)</span>。</li>
<li>若为假，说明答案在右半边，则令 <span displaypfx="inline-" class="mathjax-container">\(l=\mathrm{mid}+1\)</span>。</li>
</ul>
<p>算法正确性的关键不在于公式本身，而在于维护区间不变式（Invariant）：在每一步更新后，真正的答案仍然留在当前区间中。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在机器学习里，二分查找常用于阈值定位。例如，当需要找到“使召回率至少达到某个目标值的最小分类阈值”时，只要阈值越大召回率越低这一单调关系成立，就可以在阈值区间上做二分查找，而不必逐点穷举。类似地，很多数值求根、超参数边界搜索、分位数定位问题也都可写成二分框架。</p>
<div class="blog_h2"><span class="graybg">随机采样（Random Sampling）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>随机采样（Random Sampling）处理的是这样一类问题：总体太大、精确计算太贵，或者目标本身就是概率性的，因此只能通过抽样近似整体行为。它是统计学习、蒙特卡洛估计、bootstrap、自助重采样、mini-batch 训练和负采样的共同基础。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>随机采样的核心是：<span style="background-color: #c0c0c0;">不必每次都看完整总体，而是通过足够有代表性的随机子样本估计总体性质</span>。它像民意调查：不可能每天逐个询问所有人，但若抽样方式合理，少量样本也能给出相对稳定的总体估计。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若目标是估计随机变量 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 下某个函数 <span displaypfx="inline-" class="mathjax-container">\(f(X)\)</span> 的期望 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[f(X)]\)</span>，最常见的蒙特卡洛估计写为：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\mu}=\frac{1}{n}\sum_{i=1}^{n} f(x_i),\qquad x_i\sim p(x)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x_i\sim p(x)\)</span> 表示样本 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 是按分布 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span> 随机抽到的； <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是样本数； <span displaypfx="inline-" class="mathjax-container">\(\hat{\mu}\)</span> 是用样本均值近似真实期望的估计量。样本越多，估计通常越稳定，但代价也越高。</p>
<p>这一思想在机器学习里非常普遍：SGD 不是每次都在全量数据上算梯度，而是用 mini-batch 的样本均值近似全数据梯度；negative sampling 不是对所有负类都求和，而是随机抽一小部分负样本近似完整目标。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>bootstrap 是一个很典型的例子。随机森林训练时，会对原始训练集做有放回采样，得到多份不同的 bootstrap 子集，再分别训练多棵树。这里真正起作用的不是“树”本身，而是随机采样制造了多个略有差异的数据视角，从而让集成后的模型更稳。</p>
<div class="blog_h1"><span class="graybg">机器学习基础概念</span></div>
<p>机器学习基础概念（Machine Learning Foundations）回答四类核心问题：数据从哪里来、模型在学什么、模型为什么能泛化、结果该如何评价。把这些问题分开看，会比死记算法名称更有效：学习范式决定监督信号来自哪里，假设空间与归纳偏置决定模型愿意相信什么，数据集工程决定模型实际看到了什么，模型评估决定这些学习结果是否真的能迁移到未见样本。</p>
<div class="blog_h2"><span class="graybg">假设/目标/代价/损失</span></div>
<p>这四个词描述的是同一条“训练=优化”的概念链，但位于不同层级。把层级理清后，公式与实现会自然对齐：模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 先给出预测，再用损失函数把预测变成数值误差，最后把误差在数据集上汇总成代价函数，并加入正则/约束得到最终的目标函数。</p>
<div class="blog_h3"><span class="graybg">假设函数（Hypothesis Function）</span></div>
<p>假设函数（Hypothesis Function）也常被直接称为模型（Model）或预测函数（Predictor），记作 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span>。它回答的问题是：<span style="background-color: #c0c0c0;">给定输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，模型输出什么</span>。参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 决定这条映射的具体形状。</p>
<p>线性回归（Linear Regression）的假设函数是最经典的例子：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=f_\theta(x)=\mathbf{w}^\top x+b,\quad \theta=(\mathbf{w},b)\]</span>
<div class="blog_h3"><span class="graybg">目标函数（Objective Function）</span></div>
<p>目标函数（Objective Function）记作 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)\)</span>，是优化器真正要优化的函数。工程上最常见、也最清晰的写法是：<span style="background-color: #c0c0c0;">目标函数 = 代价函数 + 正则化项</span>（没有正则化时可视为正则项为 0，因此 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)=L(\theta)\)</span>）。</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=L(\theta)+\lambda\,\Omega(\theta)\]</span>
<p>在线性回归里，若用 L2 正则（Ridge / Weight Decay），常见目标函数可以写成：</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=L(\theta)+\lambda\|\mathbf{w}\|_2^2\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 展开后，就是：</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=\frac{1}{N}\sum_{i=1}^{N}(\mathbf{w}^\top x_i+b-y_i)^2+\lambda\|\mathbf{w}\|_2^2\]</span>
<div class="blog_h3"><span class="graybg">代价/成本函数（Cost Function）</span></div>
<p>代价函数/成本函数（Cost Function）记作 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span>，通常指把样本损失在训练集上做平均或求和后的整体量，也就是经验风险（Empirical Risk）。不少教材会把它直接称为训练损失（training loss），并且在不引起歧义时把它与目标函数混用。</p>
<p>在线性回归里，常用“均方误差的平均”作为代价函数：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\ell_i(\theta)\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\ell_i(\theta)\)</span> 取为平方误差后，等价写法是：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}(\mathbf{w}^\top x_i+b-y_i)^2\]</span>
<div class="blog_h3"><span class="graybg">损失函数（Loss Function）</span></div>
<p>损失函数（Loss Function）记作 <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span>，通常定义在单个样本上，把“预测与目标的差距”映射为一个标量。它回答的问题是：<span style="background-color: #c0c0c0;">这一条样本我错了多少</span>。</p>
<p>在线性回归里，最常见的单样本损失是平方误差：</p>
<span displaypfx="" class="mathjax-container">\[\ell_i(\theta)=\ell(\hat y_i,y_i)=(\hat y_i-y_i)^2,\quad \hat y_i=f_\theta(x_i)\]</span>
<div class="blog_h2"><span class="graybg">假设空间、容量与归纳偏置</span></div>
<p>同一份训练数据，之所以会被不同模型学出完全不同的规律，根源在于每个模型都自带一套“允许学什么、不允许学什么”的结构约束。假设空间（Hypothesis Space）、模型容量（Model Capacity）与归纳偏置（Inductive Bias）共同描述的，就是这套约束。</p>
<div class="blog_h3"><span class="graybg">假设空间</span></div>
<p>假设空间（Hypothesis Space）是模型可表达函数的集合，常记为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{H}=\{f_\theta:\theta\in\Theta\}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 是由参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 决定的预测函数， <span displaypfx="inline-" class="mathjax-container">\(\Theta\)</span> 是参数可取的范围。这个定义的关键不在于“参数有多少”，而在于<span style="background-color: #c0c0c0;">模型最终允许出现哪些映射形状</span>。例如，一元线性回归的假设空间只包含直线；二次多项式回归的假设空间包含抛物线；深度神经网络的假设空间则更大，能表示复杂得多的非线性函数。</p>
<p>因此，训练并不是在所有可能函数中盲目搜索，而是在某个特定假设空间里找一个最合适的函数。假设空间太小，真实规律可能根本装不进去；假设空间太大，模型又容易把偶然噪声也解释成模式。</p>
<div class="blog_h3"><span class="graybg">模型容量</span></div>
<p>模型容量（Model Capacity）描述的是假设空间的表达能力有多强，也就是模型能拟合多复杂规律。容量高，不代表一定更好；它只表示模型“有能力”表示复杂函数。是否真的学得好，还取决于数据量、正则化、优化过程和任务本身。</p>
<p>容量可以从多个角度理解。参数更多通常意味着容量更高，但这不是唯一标准；树的深度、核方法的核函数形式、特征维度、网络层数、隐藏维度、注意力头数，都会改变容量。工程上常用一个朴素判断：<span style="background-color: #c0c0c0;">如果模型连训练集主要结构都拟合不了，容量偏低；如果训练集几乎完美、验证集却明显变差，容量往往偏高或约束不足</span>。</p>
<p>容量与复杂度控制始终是一组平衡。表格数据上的浅层树模型可能已经足够；图像、语音、自然语言这类高度复杂任务，则通常需要更高容量的模型族。容量本身不是缺点，关键在于它是否与数据规模和任务难度匹配。</p>
<div class="blog_h3"><span class="graybg">归纳偏置</span></div>
<p>归纳偏置（Inductive Bias）是模型在有限样本下从已见数据推广到未见数据时，默认采用的结构性偏好。只靠训练集上有限个点，无法唯一确定整个输入空间上的函数；模型之所以还能做出泛化判断，是因为它隐含地偏好某些解释，而排斥另一些解释。</p>
<p>把学习目标写成经验风险最小化时，这一点会更清楚：</p>
<span displaypfx="" class="mathjax-container">\[\hat f=\arg\min_{f\in\mathcal{H}}\hat R_n(f),\qquad \hat R_n(f)=\frac{1}{n}\sum_{i=1}^{n}\ell(f(x_i),y_i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\hat R_n(f)\)</span> 是经验风险（Empirical Risk），表示函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在训练集上的平均损失； <span displaypfx="inline-" class="mathjax-container">\(\hat f\)</span> 是最终选出的模型。关键约束正是 <span displaypfx="inline-" class="mathjax-container">\(f\in\mathcal{H}\)</span>：优化并不是在所有可能函数中找最优解，而是在一个被模型结构预先限制过的空间里找解。这个限制本身就是归纳偏置。</p>
<p>归纳偏置的来源很多。线性模型偏好线性关系；KNN（K-Nearest Neighbors）偏好局部相似样本给出相似输出；卷积神经网络（CNN）偏好局部连接与平移等变（Translation Equivariance）；树模型偏好分段常数的轴对齐切分；Transformer 则偏好通过注意力在 token 之间建立可变依赖。正则化、数据增强、参数共享、预训练初始化、优化器的更新轨迹，也都会进一步塑造模型的归纳偏置。</p>
<div class="blog_h2"><span class="graybg">期望风险、经验风险与结构风险</span></div>
<p>在统计学习（Statistical Learning）里，训练目标可以从三个层次来理解：期望风险（Expected Risk）描述模型在真实数据分布上的平均误差；经验风险（Empirical Risk）描述模型在有限训练集上的平均误差；结构风险（Structural Risk）则在经验风险之外，把模型复杂度一并纳入考虑。三者回答的是同一个问题的不同版本：<span style="background-color: #c0c0c0;">模型到底应该怎样才算“学得好”</span>。</p>
<div class="blog_h3"><span class="graybg">期望风险</span></div>
<p>期望风险也常被称为真实风险（True Risk）或总体风险（Population Risk）。若真实数据来自未知分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>，模型为 <span displaypfx="inline-" class="mathjax-container">\(f\)</span>，单样本损失为 <span displaypfx="inline-" class="mathjax-container">\(\ell(f(x),y)\)</span>，则期望风险定义为：</p>
<span displaypfx="" class="mathjax-container">\[R(f)=\mathbb{E}_{(x,y)\sim P}\big[\ell(f(x),y)\big]\]</span>
<p>这条式子的含义很直接：把模型放到所有可能出现的真实样本上，计算平均损失。理论上，这才是机器学习真正想最小化的对象，因为泛化能力最终取决于模型在未知数据上的表现，而不是只取决于训练集上的表现。</p>
<p>困难在于，真实分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span> 并不可见。训练时手里只有有限样本，而没有“全体可能数据”的上帝视角。因此，期望风险通常不能被直接计算，只能被估计。</p>
<div class="blog_h3"><span class="graybg">经验风险</span></div>
<p>经验风险是用训练集对期望风险做出的现实近似。设训练集为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{D}=\{(x_i,y_i)\}_{i=1}^{n}\)</span>，则经验风险定义为：</p>
<span displaypfx="" class="mathjax-container">\[\hat R_n(f)=\frac{1}{n}\sum_{i=1}^{n}\ell\big(f(x_i),y_i\big)\]</span>
<p>它就是模型在当前这批已观测样本上的平均损失。工程上常见的 training loss，本质上反映的正是经验风险，或它的 mini-batch 近似。经验风险最小化（Empirical Risk Minimization, ERM）的训练逻辑也很朴素：既然期望风险不可直接计算，就先把训练集上的平均损失压低。</p>
<p>ERM 的关键前提是：训练集足够代表真实分布。当样本数量增加且采样足够合理时，经验风险通常会更接近期望风险；但在有限样本条件下，两者并不相等。二者之间的差距，本质上就是泛化误差（Generalization Gap）的来源。若模型只是把训练集中的偶然模式、局部噪声和标注误差也一并记住，那么 <span displaypfx="inline-" class="mathjax-container">\(\hat R_n(f)\)</span> 可以很低，而 <span displaypfx="inline-" class="mathjax-container">\(R(f)\)</span> 仍然很高，这正是过拟合（Overfitting）的典型形式。</p>
<div class="blog_h3"><span class="graybg">结构风险</span></div>
<p>结构风险（Structural Risk）是在经验风险之外，再把模型复杂度或假设空间规模纳入考虑的目标。它对应的思想是结构风险最小化（Structural Risk Minimization, SRM）：模型不仅要在训练集上拟合得好，还要避免复杂到足以随意记忆有限样本。</p>
<p>在统计学习理论的严格表述中，SRM 常写成在一族嵌套假设空间之间做选择；在工程实践里，它更常以“经验风险 + 复杂度惩罚”的形式出现，例如：</p>
<span displaypfx="" class="mathjax-container">\[J(f)=\hat R_n(f)+\lambda\,\Omega(f)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)\)</span> 是复杂度项（Complexity Penalty）， <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 控制数据拟合与复杂度约束之间的权衡。L2 正则化（L2 Regularization）、L1 正则化（L1 Regularization）、Weight Decay、早停（Early Stopping）以及对模型深度、宽度和树复杂度的限制，都可以看作结构风险最小化思想的具体实现。</p>
<p>因此，结构风险并不是对经验风险的否定，而是对 ERM 的补充：在有限样本条件下，<span style="background-color: #c0c0c0;">仅仅把训练误差压到最低，并不能保证模型在真实分布上表现最好</span>。模型必须同时控制复杂度，才能让“训练集上学到的规律”更有机会迁移到未见样本。</p>
<div class="blog_h3"><span class="graybg">为什么它重要</span></div>
<p>这三者共同构成了机器学习里最基本的张力。期望风险是理论上真正想优化的目标，但它不可直接见；经验风险是训练时可观测、可优化的替代量；结构风险则提醒我们，有限样本下不能把“训练集表现更好”直接等同于“真实世界表现更好”。换句话说，监督学习不仅是在找一个拟合训练集的函数，更是在有限数据和有限模型约束下，寻找一个最可能泛化的解释。</p>
<p>从这里继续往下，就会自然出现另一个问题：如果高维真实数据本身就带有强结构约束，那么经验风险为何常能在有限样本下逼近期望风险，模型又为何能够泛化到未见样本？流形假设（Manifold Hypothesis）正是对这个问题的一条几何回答：真实数据并不会任意填满整个高维空间，而是集中在某个低维、连续、受约束的结构附近。</p>
<div class="blog_h2"><span class="graybg">流形假设</span></div>
<p>流形假设（Manifold Hypothesis）给出了现代机器学习里一条极其重要的几何直觉：现实世界中有意义的数据，虽然表面上嵌在极高维空间里，但真正有效的变化自由度通常远低于表观维度。也就是说，高维观测往往不是填满整个空间，而是集中分布在一个低维流形（Low-dimensional Manifold）附近。</p>
<p>图像是最容易理解的例子。一个 <span displaypfx="inline-" class="mathjax-container">\(1024\times 1024\)</span> 的 RGB 图像在像素空间中维度极高，但自然图像并不会均匀占据这个巨大空间：物体形状、光照条件、视角变化、相机成像规律与纹理结构都受到强约束。因此，“像真实猫照片”的图像实际上只落在高维像素空间中的极小区域里。文本也类似。一个长度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 的 token 序列组合数极其巨大，但真正同时符合语法、语义和任务约束的文本，只占离散组合空间中很小的一部分。模型之所以能够泛化，一个重要原因正是：它并不需要学会覆盖整个高维空间，而只需要学会沿着这些低维结构建模。</p>
<div class="blog_h3"><span class="graybg">什么是流形</span></div>
<p>流形（Manifold）首先是一个几何对象。它的关键性质不是“弯不弯”，而是<span style="background-color: #c0c0c0;">局部上看起来像普通的低维欧几里得空间，整体上却可以弯曲、卷曲并嵌入到更高维空间中</span>。更正式地说，若一个集合对其上每一点，都存在一个足够小的邻域，可以用 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^d\)</span> 中的局部坐标平滑描述，那么这个集合就可以看作一个 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维流形。</p>
<p>地球表面是最直观的例子。站在操场或街道上时，局部地面几乎是平的，可以用二维坐标定位；但从整体看，地球表面显然不是二维平面，而是嵌入三维空间中的弯曲曲面。因此，地球表面就是一个嵌在三维空间里的二维流形。对只能沿地面运动的观察者而言，真正相关的不是三维空间全部坐标，而是地表上那两个局部自由度。</p>
<p>机器学习教材里常见的瑞士卷（Swiss Roll）把这个概念进一步可视化。可以先想象一张二维纸，再把它卷进三维空间。卷起来之后，样本点在外部看来落在三维空间里，但沿着纸面定位某一点时，真正需要的仍然只是二维坐标。外在维度变高了，内在结构却没有变。这正是流形概念在机器学习里最重要的几何直觉：<span style="background-color: #c0c0c0;">数据的表观维度可以很高，但它的内在维度却可能很低</span>。</p>
<p>这类卷曲结构还带来另一个机器学习里非常关键的后果：<span style="background-color: #c0c0c0;">嵌入空间中的欧氏距离（Euclidean Distance）与流形上的测地距离（Geodesic Distance）并不一定一致</span>。两点在外部空间里看起来可能很近，因为一条直线可以直接“穿过空气”连接它们；但若真实数据只能沿流形本身变化，那么真正相关的距离应当是沿曲面或曲线走过去的那条路径长度。流形学习（Manifold Learning）之所以强调邻域图、测地近似和局部结构，正是因为外部直线距离经常不能反映数据在内在结构上的真实远近关系。</p>
<p>放到高维数据上，这个判断尤其关键。一张 <span displaypfx="inline-" class="mathjax-container">\(1000\times1000\)</span> 的灰度图像在像素空间里有一百万维，但“真实人脸图像”显然不会填满整个一百万维空间。姿态、光照、表情、年龄、拍摄距离等因素彼此耦合，使真实样本只落在高维像素空间中一个极薄、极小、受连续约束的区域附近。文本也是同样的逻辑：虽然 token 组合空间极其巨大，但真正同时满足语法、语义、上下文与任务约束的句子，只会沿着某种低维结构变化，而不会任意填满整个离散组合空间。</p>
<p>因此，在机器学习语境中谈流形，真正想表达的并不是完整搬运微分几何的全部形式系统，而是一个更直接的判断：<span style="background-color: #c0c0c0;">有意义的数据并不会随机散落在高维空间中，而是集中在某个低维、连续、受约束的结构附近</span>。后面关于自由度、主成分、隐空间、低维近似以及 LoRA 任务子空间的讨论，都是围绕这个判断展开的不同形式化视角。</p>
<div class="blog_h3"><span class="graybg">自由度</span></div>
<p>沿着前面“表观维度高、内在结构低”的判断继续往下走，就会自然落到自由度（Degrees of Freedom）这个概念上。表观维度说的是“数据在形式上有多少个坐标轴”；自由度说的是“这些数据实际上有多少种彼此独立的有效变化方式”。二者并不相同。一个对象可以嵌在极高维空间里，但真正能变化的自由度却很少。</p>
<p>例如，一张脸部图片在像素空间里有数百万维，但很多像素并不能独立随意变化：头部转向、光照强弱、表情变化、年龄纹理、拍摄距离这些因素彼此耦合，共同决定了大部分像素的联动变化。因此，“一张脸”看起来是高维数组，真正支配它变化的自由度却远小于像素总数。文本也一样。句子表面上由许多 token 组成，但语法结构、主题、语气、说话者意图与上下文约束，使它不可能在每个位置上完全独立自由地变化。</p>
<p>这也是流形假设真正重要的地方：它不是抽象地说“数据在低维流形上”，而是在说<span style="background-color: #c0c0c0;">有效自由度远少于表观维度</span>。一旦把这层理解清楚，后面关于主成分、隐空间、隐主题、低秩近似乃至 LoRA 的很多思想都会变得顺理成章，因为它们都在试图用更少的自由度，去抓住决定数据或参数变化的核心结构。</p>
<div class="blog_h3"><span class="graybg">主成分、隐空间与隐主题</span></div>
<p>一旦接受了“高维数据实际靠近低维结构”这一点，后面许多术语就会自然连起来。主成分（Principal Components）强调几何视角：在一组高维数据里，哪些方向承载了最主要的变化。隐空间（Latent Space）强调表示视角：把原始高维观测压缩到一个更低维、但仍保留关键信息的内部空间。隐主题（Latent Topics）则更偏语义视角：在文本与文档分解里，低维方向常常可以被解释为若干潜在语义因素，例如“体育”“金融”“法律”这类人类能命名的主题轴。</p>
<p>这三个词并不完全同义，但常常指向同一个底层事实：<span style="background-color: #c0c0c0;">高维观测可以通过少数主导方向或潜在因子来近似描述</span>。主成分更强调方差最大的坐标轴；隐空间更强调模型内部那间低维“房间”；隐主题则是在某些任务里，对这些低维方向做出的语义解释。它们分别对应几何、表示与语义三种语言，但共享同一条低维结构主线。</p>
<div class="blog_h3"><span class="graybg">PCA 与低维近似</span></div>
<p>PCA（Principal Component Analysis）是这条思路最经典、也最直接的算法形式。它在无监督条件下寻找方差最大的几个方向，并把数据投影到这些方向张成的低维子空间中。若数据矩阵为 <span displaypfx="inline-" class="mathjax-container">\(X\)</span>，PCA 本质上是在找一个低维线性子空间，使投影后的重建误差尽量小。在线性代数上，这与奇异值分解（SVD）直接对应：保留最大的前 <span displaypfx="inline-" class="mathjax-container">\(r\)</span> 个奇异值及其奇异向量，就得到最佳的 rank-<span displaypfx="inline-" class="mathjax-container">\(r\)</span> 近似。</p>
<p>这类思想并不只存在于经典降维里。自动编码器（Autoencoder）通过瓶颈层学习隐空间；潜在语义分析（Latent Semantic Analysis, LSA）和主题模型在文档-词矩阵里抽取潜在主题；词向量、句向量与深度表示模型把高维离散符号压缩到稠密向量空间。它们的目标函数和可解释性不同，但都默认：原始高维观测背后存在一个更低维、更有结构的变化空间。</p>
<div class="blog_h3"><span class="graybg">LoRA 与任务相关子空间</span></div>
<p>LoRA（Low-Rank Adaptation）通过把参数更新 <span displaypfx="inline-" class="mathjax-container">\(\Delta W\)</span> 限制在一个低秩子空间中，实现对大模型的参数高效微调（PEFT）。它的核心做法不是直接改写原始权重矩阵的全部自由度，而是把更新写成两个低秩矩阵的乘积：</p>
<span displaypfx="" class="mathjax-container">\[\Delta W = BA,\quad B\in\mathbb{R}^{d_{\text{out}}\times r},\ A\in\mathbb{R}^{r\times d_{\text{in}}},\ r\ll \min(d_{\text{in}},d_{\text{out}})\]</span>
<p>LoRA 应放在“低维结构”这条主线上理解，但不能把它与 PCA 直接等同。它的核心不是对输入数据做主成分分析，而是对模型参数更新施加低秩约束。</p>
<p>这个约束的含义是：模型不能在完整高维参数空间中任意改动，而只能在一个 rank-<span displaypfx="inline-" class="mathjax-container">\(r\)</span> 的低维更新子空间里移动。从思想上看，它确实与“只保留主导方向”高度相似；若事后对某个全量更新矩阵做 SVD，最佳低秩近似也会只保留最主要的奇异方向。但 LoRA 学到的并不是传统 PCA 意义上“数据方差最大”的主成分，而是<span style="background-color: #c0c0c0;">对当前任务损失下降最有用的低维更新方向</span>。前者是无监督的统计主轴，后者是由反向传播和任务目标共同决定的优化子空间。</p>
<p>因此，把 LoRA 理解为“逼迫模型只在少数主导方向上修改参数”是成立的；但这些方向更准确地说是任务相关的低维适配方向，而不是直接等同于原始数据的 PCA 主成分。也正因为如此，LoRA 与内在维度（Intrinsic Dimension）讨论天然相连：如果一个下游任务真正需要修改的有效自由度本来就不高，那么让模型只在一个低秩子空间里更新，不仅不会显著损失性能，反而会自动抑制大量无意义的噪声方向。</p>
<div class="blog_h2"><span class="graybg">学习范式</span></div>
<p>这里先按监督信号来源划分学习范式。监督学习（Supervised Learning）、无监督学习（Unsupervised Learning）、自监督学习（Self-supervised Learning）和强化学习（Reinforcement Learning）回答的是同一个问题：模型训练时的监督信号究竟来自哪里。它们属于同一分类标准，因此可以并列讨论。</p>
<div class="blog_h3"><span class="graybg">监督学习</span></div>
<p>监督学习（Supervised Learning）使用带标签的数据对 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 训练模型：输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是特征（Feature），输出 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是目标或标签（Label）。模型学习的不是“记住答案”，而是学习一个映射 <span displaypfx="inline-" class="mathjax-container">\(f_\theta:x\to y\)</span>，使它对新样本也能给出合理预测。</p>
<p>但“学一个映射”还不够，训练时还必须回答另一个更具体的问题：<span style="background-color: #c0c0c0;">怎样才算模型学得好</span>。监督学习里最常见的回答，是在训练集上逐个比较预测与真实标签的差距，再把这些差距汇总成一个总体目标；这就导向经验风险最小化（Empirical Risk Minimization, ERM）。</p>
<p>经验风险最小化的典型目标写成：</p>
<span displaypfx="" class="mathjax-container">\[\frac{1}{N}\sum_{i=1}^{N}\ell\big(f_\theta(x_i),y_i\big)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 是样本数， <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 是模型预测， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实标签， <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span> 是损失函数（Loss Function）。这条式子的含义不是抽象求和，而是：<span style="background-color: #c0c0c0;">逐个样本计算“预测错了多少”，再取平均，把平均错误压到尽可能小</span>。</p>
<p>例：垃圾邮件分类里， <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 可以是邮件文本特征， <span displaypfx="inline-" class="mathjax-container">\(y\in\{0,1\}\)</span> 表示“正常/垃圾”；房价预测里， <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 可以是面积、地段、楼龄， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是价格。前者是分类（Classification），后者是回归（Regression），但“有标签地学映射、再用损失函数衡量误差”这一训练逻辑完全一致。</p>
<div class="blog_h4"><span class="graybg">分类任务</span></div>
<p>分类任务（Classification）要预测的是<span style="background-color: #c0c0c0;">离散类别</span>，也就是样本属于哪一类。输出可以是一个类别 id，也可以是一组类别概率。例如二分类里常见输出 <span displaypfx="inline-" class="mathjax-container">\(P(y=1|x)\)</span>，表示“给定特征 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时，样本属于正类的概率”。</p>
<p>垃圾邮件识别、肿瘤良恶性判断、情感分析、图像里的猫狗识别都属于分类任务。它更像“做选择题”：模型最终要在有限候选里做判断。训练时常配合交叉熵（Cross-Entropy）这类损失，因为模型不仅要选对类别，还要给正确类别足够高的置信度。</p>
<div class="blog_h4"><span class="graybg">回归任务</span></div>
<p>回归任务（Regression）要预测的是<span style="background-color: #c0c0c0;">连续数值</span>，也就是标签不是几个固定类别，而是在某个数值区间内连续变化。输出通常直接是一个实数，或一个多维连续向量。</p>
<p>房价预测、销量预测、温度预测、广告点击率中的停留时长估计都属于回归任务。它更像“做填空题”：模型不能只说“高”或“低”，而必须给出具体数值。训练时常配合均方误差（MSE）或平均绝对误差（MAE），因为关心的是预测值与真实值到底差了多少。</p>
<p>分类与回归都属于监督学习，因为它们都有标签；真正的区别在于<span style="background-color: #c0c0c0;">标签空间的形状</span>：分类的标签空间是离散集合，回归的标签空间是连续区间。这个区别会直接决定模型输出层形式、损失函数选择以及评估指标。</p>
<div class="blog_h3"><span class="graybg">弱监督学习</span></div>
<p>弱监督学习（Weakly Supervised Learning）仍然使用标签信号训练模型，但这些标签并不像标准监督学习那样完整、精确且逐样本对齐。它处理的核心情形是：<span style="background-color: #c0c0c0;">标签存在，但监督结构不够理想</span>，例如标签只存在于更粗粒度层面、标签本身含噪，或只有部分样本带标签。</p>
<p>从概念上看，弱监督并不是单一算法，而是一组监督不完备情形的总称。常见形式包括：标签不完整（incomplete supervision），即只有部分样本带标签；标签不精确（inexact supervision），即标签附着在聚合层级而不是实例层级；标签不准确（inaccurate supervision），即标签本身带噪或来自启发式规则、远程监督（Distant Supervision）与弱标注器。它与标准监督学习的边界在于：监督信号依然存在，但标签质量、粒度或覆盖度不足以直接当作“干净答案册”。</p>
<div class="blog_h4"><span class="graybg">多实例学习（Multi-Instance Learning, MIL）</span></div>
<p>多实例学习（Multi-Instance Learning, MIL）是弱监督学习中的一个经典范式。它的关键设定是：标签不附着在单个实例（instance）上，而是附着在一个由多个实例组成的包（bag）上。设训练集由若干包 <span displaypfx="inline-" class="mathjax-container">\(\{B_1,B_2,\dots,B_n\}\)</span> 构成，其中第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个包可写成</p>
<span displaypfx="" class="mathjax-container">\[B_i=\{x_{i1},x_{i2},\dots,x_{im_i}\},\qquad y_i\in\{0,1\}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x_{ij}\)</span> 是包内第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个实例， <span displaypfx="inline-" class="mathjax-container">\(m_i\)</span> 是该包包含的实例数， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是包级标签。训练时只知道整个包的标签，不知道每个实例自己的标签。</p>
<p>MIL 最经典的标准假设（Standard MI Assumption）是：正包中至少存在一个正实例，负包中所有实例都为负。写成逻辑形式就是：</p>
<span displaypfx="" class="mathjax-container">\[y_i=1 \iff \exists j,\ z_{ij}=1;\qquad y_i=0 \iff \forall j,\ z_{ij}=0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(z_{ij}\)</span> 表示实例级未知标签。这个设定特别适合两类问题：第一，真正决定结果的证据只稀疏地存在于少数关键实例中，其余实例更像背景或噪声；第二，获取实例级标签成本高，只能拿到更粗粒度的聚合标签。</p>
<p>因此，MIL 的关键不只是“把很多实例放在一起”，而是要学习一条从<span style="background-color: #c0c0c0;">实例集合到包级判断</span>的聚合规则。早期方法常用最大池化、均值池化或手工设计的聚合函数；深度学习阶段则更常引入可学习聚合，例如注意力式 MIL（Attention-based MIL），用可训练权重自动决定哪些实例对最终包标签贡献更大。这使 MIL 不只具有表达能力，也更容易给出“模型主要关注了哪些实例”的解释线索。</p>
<p>MIL 的适用性可以用一个抽象例子来理解。若一条完整客服对话被视作一个包，其中每轮发言是实例，而整体满意度评分是包级标签，那么模型面对的就是“整体有标签、逐轮无标签”的典型结构。此时，MIL 的任务不是给每轮发言都强行分配人工标签，而是在只有整体评分的前提下，学习哪些局部发言更可能决定整段会话的最终判断。</p>
<div class="blog_h3"><span class="graybg">无监督学习</span></div>
<p>无监督学习（Unsupervised Learning）只有输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，没有人工标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>。它的目标不是拟合“标准答案”，而是从数据中发现结构（Structure），例如聚类（Clustering）、降维（Dimensionality Reduction）、密度估计（Density Estimation）与异常检测（Anomaly Detection）。</p>
<p>一个直观类比是：监督学习像“拿着答案册做题”，无监督学习像“没有答案册，只能自己把一堆材料按相似性归类”。例如电商用户没有现成“用户类型”标签，但可以根据浏览、购买、停留时间等行为聚成“价格敏感型”“冲动购买型”“高价值复购型”等群体，用于运营分层。</p>
<div class="blog_h3"><span class="graybg">自监督学习</span></div>
<p>自监督学习（Self-supervised Learning）介于监督与无监督之间：原始数据没有人工标签，但任务标签可以由数据本身自动构造出来。核心思想是<span style="background-color: #c0c0c0;">从数据内部制造预测任务</span>，让模型在完成这些任务的过程中学到可迁移表示（Representation）。</p>
<p>语言模型的下一个 token 预测就是最典型的自监督任务：前文是输入，后一个 token 是由原始文本自动给出的“监督信号”。图像领域里，旋转预测、遮挡恢复、不同增强视角匹配也属于同一路线。</p>
<div class="blog_h4"><span class="graybg">掩码预测</span></div>
<p>掩码预测（Masked Prediction）把输入中的一部分信息故意遮住，再要求模型恢复。例如 BERT 会把句子中的部分 token 替换成特殊标记 <span displaypfx="inline-" class="mathjax-container">\([MASK]\)</span>，模型要根据上下文预测被遮住的词。</p>
<p>类比来看，这像完形填空：你不是死记整句，而是学会根据上下文推断缺失信息。它迫使模型同时利用左侧和右侧上下文，因此特别适合编码器（Encoder）型表示学习。</p>
<div class="blog_h3"><span class="graybg">强化学习</span></div>
<p>强化学习（Reinforcement Learning, RL）研究的是：智能体（Agent）在环境（Environment）中持续交互，根据奖励（Reward）学习策略（Policy），使长期累计回报（Cumulative Return）尽可能大。它关心的不是“单个输入该预测什么标签”，而是<span style="background-color: #c0c0c0;">一串连续动作最终能否带来更高的长期收益</span>。</p>
<div class="blog_h4"><span class="graybg">问题设定</span></div>
<p>强化学习的基本循环可以概括为：在时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span>，智能体观察状态 <span displaypfx="inline-" class="mathjax-container">\(s_t\)</span>，选择动作 <span displaypfx="inline-" class="mathjax-container">\(a_t\)</span>，环境转移到新状态 <span displaypfx="inline-" class="mathjax-container">\(s_{t+1}\)</span>，并返回奖励 <span displaypfx="inline-" class="mathjax-container">\(r_{t+1}\)</span>。策略记为 <span displaypfx="inline-" class="mathjax-container">\(\pi(a\mid s)\)</span>，它回答的是“在当前状态下，动作应该怎样选”。</p>
<p>很多任务里，奖励并不会在正确动作发生的那一刻立刻显现。下棋时，一步好棋可能要十几步后才体现价值；推荐系统里，一次推荐是否合理，也要看后续点击、停留和转化。因此强化学习优化的不是单步得分，而是长期回报：</p>
<span displaypfx="" class="mathjax-container">\[J(\pi)=\mathbb{E}_{\pi}\!\left[\sum_{t=0}^{\infty}\gamma^t r_{t+1}\right]\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\gamma\in[0,1)\)</span> 是折扣因子（Discount Factor）。<span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 越接近 1，策略越重视长期收益；越小，策略越偏向短期收益。这个目标函数的含义很直接：在所有可能的策略中，找到那个平均下来总分最高的行为规则。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/rl.png"><img class="alignnone size-full wp-image-40981" src="https://blog.gmem.cc/wp-content/uploads/2026/03/rl.png" alt="rl" width="100%" /></a></p>
<div class="blog_h4"><span class="graybg">和监督学习的差异</span></div>
<p>监督学习通常基于带标签样本 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 训练静态映射 <span displaypfx="inline-" class="mathjax-container">\(x\mapsto y\)</span>；强化学习面对的是动态环境中的序贯决策。监督学习收到的是“正确答案应当是什么”的指导性反馈，强化学习收到的是“这一步或这条轨迹好不好”的评估性反馈。前者的误差归因通常较直接，后者则需要把最终回报回溯到一串历史动作，这就是时间信用分配（Temporal Credit Assignment）的难点来源。</p>
<p>因此，强化学习的训练数据也不是静态不变的。当前策略会决定智能体之后访问哪些状态，于是数据分布本身会随着策略更新而改变。这一点使强化学习同时面对建模问题、探索问题和训练稳定性问题。</p>
<div class="blog_h4"><span class="graybg">马尔可夫决策过程（MDP）</span></div>
<p>强化学习的标准数学框架是马尔可夫决策过程（Markov Decision Process, MDP）。一个 MDP 通常写成五元组 <span displaypfx="inline-" class="mathjax-container">\((\mathcal{S},\mathcal{A},P,R,\gamma)\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{S}\)</span> 是状态空间， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{A}\)</span> 是动作空间， <span displaypfx="inline-" class="mathjax-container">\(P(s'|s,a)\)</span> 是状态转移概率， <span displaypfx="inline-" class="mathjax-container">\(R(s,a)\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(R(s,a,s')\)</span> 是奖励函数， <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 是折扣因子。</p>
<p>“马尔可夫”这个词的核心含义是：<span style="background-color: #c0c0c0;">如果当前状态已经把决策所需的信息概括完整，那么未来只取决于当前状态和当前动作</span>。它并不要求系统真的没有历史，而是要求当前状态已经足够代表历史中与决策相关的部分。</p>
<div class="blog_h4"><span class="graybg">价值函数与 Bellman 方程</span></div>
<p>强化学习里最重要的两个量是状态价值函数（State-value Function）和动作价值函数（Action-value Function）：</p>
<span displaypfx="" class="mathjax-container">\[V^\pi(s)=\mathbb{E}_\pi[G_t\mid s_t=s],\qquad Q^\pi(s,a)=\mathbb{E}_\pi[G_t\mid s_t=s,\ a_t=a]\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(V^\pi(s)\)</span> 描述“在状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 下，按策略 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span> 继续行动，长期回报大约是多少”；<span displaypfx="inline-" class="mathjax-container">\(Q^\pi(s,a)\)</span> 则更进一步，描述“在状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 下先做动作 <span displaypfx="inline-" class="mathjax-container">\(a\)</span>，再按策略 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span> 继续行动，长期回报大约是多少”。</p>
<p>Bellman 方程（Bellman Equation）把“长期回报”写成“即时奖励 + 下一步价值”的递归形式。对固定策略 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span>，状态价值满足：</p>
<span displaypfx="" class="mathjax-container">\[V^\pi(s)=\mathbb{E}_\pi\big[r_{t+1}+\gamma V^\pi(s_{t+1})\mid s_t=s\big]\]</span>
<p>这条式子的直觉非常重要：一个状态值多少钱，不需要把未来整条轨迹一口气全部展开，只要看“这一步先拿到多少，再加上下一状态值多少钱”。这就是动态规划思想在强化学习中的核心落点。</p>
<p>若目标是最优控制，则最优动作价值函数满足 Bellman 最优方程：</p>
<span displaypfx="" class="mathjax-container">\[Q^*(s,a)=\mathbb{E}\big[r_{t+1}+\gamma\max_{a'}Q^*(s_{t+1},a')\mid s_t=s,\ a_t=a\big]\]</span>
<p>它表达的是：当前动作的最优价值，等于这一步的即时奖励，加上下一状态里最佳后续选择的折扣价值。价值型方法、时序差分学习（Temporal-Difference Learning, TD）以及 Q-Learning，都是围绕这个递归结构展开的。</p>
<div class="blog_h4"><span class="graybg">价值方法：Q-Learning 与 DQN</span></div>
<p>价值型方法（Value-Based Methods）先估计“某个状态或状态-动作对值多少钱”，再根据价值做决策。最经典的做法是 Q-Learning。它不直接记住“这一步该做什么”，而是维护一个动作价值估计 <span displaypfx="inline-" class="mathjax-container">\(Q(s,a)\)</span>，让模型逐步学会哪些动作长期更划算。</p>
<p>Q-Learning 的标准更新写成：</p>
<span displaypfx="" class="mathjax-container">\[Q(s,a)\leftarrow Q(s,a)+\alpha\Big(r+\gamma\max_{a'}Q(s',a')-Q(s,a)\Big)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(r+\gamma\max_{a'}Q(s',a')\)</span> 是新的目标值（TD target），它把“这一步拿到的即时奖励”和“下一步开始最好还能拿到多少”拼接在一起；括号里的整体差值是 TD 误差（TD error），表示旧估计和新观察之间的偏差。Q-Learning 每见到一次新的转移样本，就用它修正一次账本。</p>
<p>当状态空间很小，例如离散网格迷宫，价值可以直接存成 Q 表；当状态变成图像、传感器序列或高维特征时，就需要用神经网络近似 <span displaypfx="inline-" class="mathjax-container">\(Q(s,a)\)</span>，这就是 DQN（Deep Q-Network）。它没有改变基本思想，只是把“查表记账”升级成“让网络来估值”。</p>
<div class="blog_h4"><span class="graybg">策略方法：Policy Gradient、Actor-Critic 与 PPO</span></div>
<p>策略型方法（Policy-Based Methods）不先学一个价值表，而是直接参数化策略 <span displaypfx="inline-" class="mathjax-container">\(\pi_\theta(a\mid s)\)</span>。给定状态，模型直接输出动作概率分布或连续控制参数，再通过优化把高回报动作的概率提高、低回报动作的概率压低。它尤其适合连续动作空间，因为这类问题往往很难穷举所有动作再逐个估值。</p>
<p>策略梯度（Policy Gradient）的基本形式是：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_\theta J(\theta)=\mathbb{E}\big[\nabla_\theta \log \pi_\theta(a_t\mid s_t)\,G_t\big]\]</span>
<p>它的含义可以概括成一句话：最终效果好的动作，以后更常做；最终效果差的动作，以后更少做。问题在于，直接用 <span displaypfx="inline-" class="mathjax-container">\(G_t\)</span> 更新通常方差很大，训练容易抖动。</p>
<p>于是就出现了 Actor-Critic。Actor 负责输出策略，也就是“怎么行动”；Critic 负责评估当前状态或动作值，也就是“这一步大概值多少”。Critic 提供更稳定的基线或优势估计，Actor 再沿着这个更平滑的信号更新策略。这样做的结果是：策略更新方向仍然由回报决定，但梯度噪声显著更可控。</p>
<p>PPO（Proximal Policy Optimization）可以看作一种工程上非常成功的 Actor-Critic 变体。它的核心思想不是改变优化方向，而是给策略更新加护栏，限制新旧策略之间的偏移幅度，避免一步改得过猛导致训练失稳。因此，PPO 在机器人控制、游戏智能体和大模型对齐里都很常见。</p>
<div class="blog_h4"><span class="graybg">探索与利用</span></div>
<p>强化学习始终要面对探索与利用（Exploration vs. Exploitation）的权衡。利用意味着优先选择当前已知回报较高的动作；探索意味着尝试那些暂时不确定、但可能更优的动作。只利用，策略可能很快卡在局部最优；只探索，又会浪费大量样本在明显不好的选择上。</p>
<p>这个矛盾在强化学习里是结构性的，因为策略会影响后续看到的数据。常见做法包括 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>-greedy、熵正则化（Entropy Regularization）、Boltzmann exploration、上置信界（UCB）等。它们形式不同，但共同目标一致：既让模型敢于试错，又不至于长期停留在无意义的随机行动里。</p>
<div class="blog_h4"><span class="graybg">Model-Based 与 Model-Free</span></div>
<p>另一条常见划分标准是 model-based 与 model-free。二者的区别不在于是否使用神经网络，而在于<span style="background-color: #c0c0c0;">是否显式学习或利用环境动力学</span>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">核心思路</td>
<td style="text-align: center;">优势</td>
<td style="text-align: center;">代价</td>
</tr>
</thead>
<tbody>
<tr>
<td>Model-Based RL</td>
<td>显式学习或已知状态转移与奖励模型，再据此规划或生成模拟轨迹</td>
<td>样本效率通常更高；能做前瞻规划</td>
<td>环境模型一旦学偏，规划也会被带偏；实现更复杂</td>
</tr>
<tr>
<td>Model-Free RL</td>
<td>不显式建模环境，直接从交互样本学习价值函数或策略</td>
<td>实现相对直接；适合复杂高维环境</td>
<td>样本效率通常较低；对真实交互成本更敏感</td>
</tr>
</tbody>
</table>
<p>价值型方法如 Q-Learning、DQN，策略型方法如 REINFORCE、PPO，多数都属于 model-free 路线；若先学习一个世界模型（World Model）或已知环境转移，再基于模型做搜索和规划，则属于 model-based 路线。实际系统也常把两者混合使用。</p>
<div class="blog_h4"><span class="graybg">和大模型对齐的衔接</span></div>
<p>大模型时代出现的 RLHF、PPO-based alignment、GRPO 等方法，属于强化学习思想在语言模型上的应用层。它们沿用的仍然是策略、奖励、回报、优势函数和策略优化这些通用概念，只是把环境替换成“基于 prompt、回答和偏好反馈构成的交互过程”。因此，理解通用强化学习基础之后，再进入后文的强化学习对齐，会更容易看清哪些是 RL 本体，哪些是大模型场景下的特化设计。</p>
<div class="blog_h2"><span class="graybg">统计学习</span></div>
<p>统计学习（Statistical Learning）强调从<span style="background-color: #c0c0c0;">数据由某种概率机制生成</span>这一视角理解机器学习。前面的概率论与统计已经介绍了概率、似然、MLE、MAP、边缘化与随机过程；这里不重复纯数学定义，而是把这些概念收束成机器学习里的几条主线：模型究竟在建模什么分布，隐藏变量怎样进入问题，推断为什么会变难，以及“相关”与“因果”为什么不是同一件事。</p>
<div class="blog_h3"><span class="graybg">从分布到学习问题</span></div>
<p>从统计学习视角看，很多模型的区别首先不在神经网络层数或树的深度，而在于它们选择建模哪一种概率对象。判别式模型（Discriminative Model）直接学习 <span displaypfx="inline-" class="mathjax-container">\(p(y\mid x)\)</span> 或决策边界，关心“给定输入后标签怎么判”；生成式模型（Generative Model）则更关心 <span displaypfx="inline-" class="mathjax-container">\(p(x,y)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(p(x\mid y)\)</span> 或带隐藏变量的联合分布，关心“数据是如何被生成出来的”。</p>
<p>这种划分并不等于“一个更先进、一个更落后”。判别式方法通常更直接服务于分类或回归目标；生成式方法则更容易表达不确定性、缺失变量和隐含结构。朴素贝叶斯、高斯混合模型（GMM）、隐马尔可夫模型（HMM）偏生成式；逻辑回归、支持向量机（SVM）、条件随机场（CRF）偏判别式。HMM 与 CRF 的细节放在后面的经典机器学习部分展开，这里只强调它们在统计建模立场上的差异。</p>
<div class="blog_h3"><span class="graybg">概率图模型</span></div>
<p>概率图模型（Probabilistic Graphical Model, PGM）用图结构表达随机变量之间的条件独立关系，并把高维联合分布拆成较小的局部因子。它的核心价值不是“把概率画成图”，而是把<span style="background-color: #c0c0c0;">哪些变量直接相互作用、哪些依赖可以被切断</span>写成显式结构，从而让建模、推断和解释都更清晰。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">对象</td>
<td style="text-align: center;">关注点</td>
<td style="text-align: center;">典型形式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>贝叶斯网络（Bayesian Network）</td>
<td>有向依赖与条件独立</td>
<td>有向无环图（DAG）</td>
<td>适合表达“父节点影响子节点”的生成结构</td>
</tr>
<tr>
<td>因子图（Factor Graph）</td>
<td>联合分布如何分解为若干局部因子</td>
<td>变量节点 + 因子节点二部图</td>
<td>更强调分解结构，常作为统一表达方式</td>
</tr>
<tr>
<td>HMM / CRF</td>
<td>序列中的局部依赖</td>
<td>链式图结构</td>
<td>HMM 偏生成式，CRF 偏判别式</td>
</tr>
</tbody>
</table>
<p>贝叶斯网络适合表达“一个变量如何通过若干中间变量影响另一个变量”的有向依赖；因子图则更偏向把复杂联合分布拆成局部势函数（Factor）相乘的形式。若问题具有明显序列结构，HMM 和 CRF 就是最典型的链式概率图模型实例。它们之所以重要，不只是因为历史地位高，而是因为许多现代模型虽然实现方式更复杂，仍然在利用“局部分解 + 全局归一化/推断”这条思想主线。</p>
<div class="blog_h3"><span class="graybg">潜变量、EM 与变分推断</span></div>
<p>统计学习里很多问题之所以变难，不是因为观测变量本身太复杂，而是因为模型里存在隐藏变量（Latent Variable）<span displaypfx="inline-" class="mathjax-container">\(z\)</span>。一旦联合分布写成 <span displaypfx="inline-" class="mathjax-container">\(p(x,z)\)</span>，训练或预测往往都要面对边缘化：</p>
<span displaypfx="" class="mathjax-container">\[p(x)=\sum_z p(x,z)\quad \text{或} \quad p(x)=\int p(x,z)\,dz\]</span>
<p>当这个求和或积分无法直接算清时，推断（Inference）就成为核心问题。EM（Expectation-Maximization）适合一类带潜变量的参数估计问题：E 步先根据当前参数估计隐藏变量的后验分布或其期望统计量，M 步再在这些期望量上更新参数。GMM 的训练、HMM 的 Baum-Welch 算法，都是这一路线的经典例子。</p>
<p>若后验分布本身也难以精确求解，就需要近似推断。变分推断（Variational Inference, VI）的思路是：不用直接算真实后验 <span displaypfx="inline-" class="mathjax-container">\(p(z\mid x)\)</span>，而是选一个可计算的近似分布 <span displaypfx="inline-" class="mathjax-container">\(q(z)\)</span> 去逼近它。于是问题从“直接求后验”转化为“在一个可处理的分布族里找最接近后验的那个近似”。这一思路后来也自然延伸到了现代深度生成模型，例如变分自编码器（VAE）的训练就建立在变分推断框架之上。</p>
<div class="blog_h3"><span class="graybg">因果图与 do-calculus</span></div>
<p>统计相关性回答的是“变量经常一起变化吗”，因果推断（Causal Inference）回答的则是“如果我主动干预一个变量，另一个变量会怎样变”。这两类问题在形式上很接近，但含义完全不同。观察性条件概率 <span displaypfx="inline-" class="mathjax-container">\(p(y\mid x)\)</span> 只说明在看到 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 通常是什么样；干预分布 <span displaypfx="inline-" class="mathjax-container">\(p(y\mid \mathrm{do}(x))\)</span> 讨论的是把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 强行设定为某个值后， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 会怎样变化。</p>
<p>因果图（Causal Graph）通常也写成有向无环图，但它表达的不再只是统计依赖，而是因果生成结构。混杂因素（Confounder）、中介变量（Mediator）和碰撞点（Collider）之所以重要，正是因为它们决定了哪些相关性可以被解释为因果效应，哪些只是共同原因或选择偏差造成的表象相关。do-calculus 则是一套把干预分布改写成可识别表达式的规则系统，用来判断在给定图结构下，目标因果效应是否能够从可观测分布中恢复出来。</p>
<p>在这份速查里，因果推断只保留这一层定位：它是统计学习向更强解释目标的延伸。普通监督学习通常停在“预测得准”，统计学习进一步讨论“不确定性和隐藏结构怎么处理”，而因果学习则继续追问“如果系统被主动改变，结果会不会跟着改变”。三者不是互斥关系，而是建模目标逐步增强的不同层级。</p>
<div class="blog_h2"><span class="graybg">表示学习与适配策略</span></div>
<p>与“学习范式”不同，下面这些概念不再按监督信号来源分类，而是分别回答另外几个问题：表示该怎样学、计算该怎样近似、已有知识该怎样迁移、在极少样本下又该怎样适配。因此它们更适合看成与学习范式并列的训练目标或训练策略。</p>
<div class="blog_h3"><span class="graybg">表示学习（Representation Learning）</span></div>
<p>表示学习（Representation Learning）讨论的是：如何把原始输入自动变换成更有用的特征表示，使后续任务更容易处理。它关心的不只是“最后预测对不对”，还关心模型内部是否学到了稳定、可迁移、对任务有判别力的中间表示。</p>
<p>传统特征工程（Feature Engineering）与表示学习处理的是同一个核心问题：如何把原始输入变成更适合下游任务的表示。但二者的方法论不同。特征工程主要依赖人工设计表示，例如词频、n-gram、统计量、规则特征与人工交叉特征；表示学习则强调由模型通过优化过程自动学出表示，例如 PCA、自编码器、词向量、上下文化表示以及深度网络中的隐藏状态。因此，传统手工特征本身通常不直接归入表示学习；只有当表示是通过训练自动获得时，它才更准确地属于表示学习范畴。</p>
<p>从 one-hot、BoW、词嵌入，到 BERT 的上下文化表示、Sentence-BERT 的句向量，主线始终一致：把原始符号或原始观测映射到更适合计算的表示空间。监督学习、自监督学习、对比学习都可以被用来学习表示；区别只在于监督信号来自哪里、训练目标如何设计。</p>
<div class="blog_h3"><span class="graybg">对比学习</span></div>
<p>对比学习（Contrastive Learning）通过“拉近正样本、推远负样本”学习表示。这里的关键不是类别标签本身，而是样本之间的相对关系：哪些应该相似，哪些必须区分。因此它特别适合表示学习、检索、多模态对齐和度量学习（Metric Learning）。</p>
<p>它的真正价值不只在于“学会匹配”，更在于学会<span style="background-color: #c0c0c0;">区分性特征（Discriminative Features）</span>。如果训练信号只告诉模型“这两个文本有关”，模型很容易停留在泛泛的共性描述上；而当训练持续提供“相似对”和“不相似对”，模型就被迫回答更尖锐的问题：究竟是什么让这两个文本属于同一语义区域，又是什么让它们必须分开。对比学习因此天然擅长抑制“表面上正确但没有区分度”的表示，转而强化真正决定语义边界的特征。</p>
<p>例如在商品评论表示学习里，句子“物流很快，包装也完整”和“物流很快，但东西是坏的”都包含“物流很快”这类高频表述。若模型只抓住表面词汇重叠，就可能把二者编码得非常接近；但在对比学习里，前者可能与“发货速度快、体验不错”构成正样本，后者则会与“收到商品后无法使用”“质量有问题”这类负面评价更接近。模型因此会逐步学会：真正决定语义边界的，不是共享的套话，而是“包装完整”“东西是坏的”这类改变整体语义走向的区分性片段。</p>
<p>从几何角度看，对比学习学到的是一个更有结构的向量空间。语义接近的文本会在局部形成簇（Cluster），语义无关或语义相反的文本则被推向更远位置。情感分析、语义检索、重复问句检测、意图聚类之所以能直接建立在 embedding 之上，本质上就是因为模型已经把“哪些内容应当靠近、哪些内容应当远离”编码进了空间结构，而不只是输出一个任务特定的分类分数。</p>
<p>在 NLP 中，这条路线并不是突然出现的。Word2Vec 已经体现了早期的对比式思想：真实共现词是正样本，随机采样词是负样本，模型通过区分“真实上下文”和“噪声配对”学习词向量。后来的句向量和文档向量模型，则把这种思想从词级扩展到句子级和文档级：正样本可以是复述句、问答配对、查询与相关文档，负样本则是不相关句子或困难反例（Hard Negatives）。</p>
<p>一个常见形式是 InfoNCE 损失：</p>
<span displaypfx="" class="mathjax-container">\[-\log \frac{\exp(\mathrm{sim}(z_i,z_i^+)/\tau)}{\sum_{j}\exp(\mathrm{sim}(z_i,z_j)/\tau)}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(z_i\)</span> 是当前样本表示， <span displaypfx="inline-" class="mathjax-container">\(z_i^+\)</span> 是与它匹配的正样本表示， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{sim}(\cdot,\cdot)\)</span> 是相似度函数（常用余弦相似度）， <span displaypfx="inline-" class="mathjax-container">\(\tau\)</span> 是温度参数（Temperature），控制分布尖锐程度。这个目标的含义是：在一堆候选中，让正确配对拿到最高分，同时把不相关样本推远。</p>
<p>对比学习在句向量任务中的意义尤其大。交叉编码器（Cross-Encoder）把两个句子拼接后联合编码，能够做非常细的交互判断，但它直接输出的是“这一对句子有多像”，而不是可复用的独立句向量；一旦候选集合很大，计算量会迅速爆炸。双编码器（Bi-Encoder）路线则把两个文本分别编码成独立向量，再用余弦相似度或点积比较。SBERT 正是这一路线的经典代表：它通过孪生网络（Siamese Network）与对比式微调，把原本不适合作为通用句向量的 BERT 表示空间，改造成适合检索、聚类与语义匹配的 embedding 空间。</p>
<p>工程上，负样本既可以来自同一 batch 中的其他样本（In-batch Negatives），也可以来自专门构造的困难负样本（Hard Negatives）。所谓困难负样本，指的不是“完全无关”的反例，而是<span style="background-color: #c0c0c0;">在表面上很像、但语义上不应被判为同一项</span>的样本。例如检索里，与查询主题相近但并不真正回答问题的文档；句向量训练里，措辞高度相似却语义立场不同的句子；推荐里，风格相近但用户最终没有点击或转化的候选。它们之所以“困难”，正是因为模型若只依赖浅层词汇重叠、模板结构或主题相近性，很容易把这类负样本误判成正样本。</p>
<p>困难负样本的价值在于：它迫使模型放弃过于粗糙的匹配捷径，转而学习更细粒度的区分信号。随机负样本通常太容易分开，训练后期提供的梯度会迅速变弱；而困难负样本更接近真实决策边界，能持续推动表示空间学习“看起来相似但本质不同”的区别。不过它也有代价：若负样本挖掘质量不高，容易把本来就相关的样本错当成负例，形成假负样本（False Negatives），反而会伤害表示质量。因此，现代检索和 embedding 训练里，Hard Negatives 往往与 in-batch negatives、教师模型挖掘（teacher mining）或 reranker 筛选结合使用，而不是完全依赖人工拍脑袋构造。</p>
<p>CLIP、Sentence-BERT、现代检索 embedding、推荐召回模型，乃至许多 query-document dual encoder，本质上都在利用这种“正样本拉近、负样本推远”的训练逻辑。区别主要不在原理，而在样本如何构造、负样本如何选择，以及表示对象是词、句子、文档还是跨模态对。</p>
<div class="blog_h4"><span class="graybg">负采样</span></div>
<p>负采样（Negative Sampling）是与对比学习和词向量训练密切相关的一类近似策略。它的核心动机是：当候选空间极大时，没有必要每次都与所有候选比较；只保留 1 个正样本和少量负样本，就能得到足够强的判别信号。换句话说，它把原本代价高昂的“大规模归一化选择问题”，近似成若干个“真配对还是噪声配对”的二分类判断。</p>
<p>在 Word2Vec 的 Skip-gram 中，若直接对全词表做 softmax，分母需要对 <span displaypfx="inline-" class="mathjax-container">\(|{\cal V}|\)</span> 个词求和，计算代价很高。负采样则对每个正样本对 <span displaypfx="inline-" class="mathjax-container">\((w,c)\)</span> 只保留少量噪声词 <span displaypfx="inline-" class="mathjax-container">\(w_i\)</span>，并最大化：</p>
<span displaypfx="" class="mathjax-container">\[\log\sigma(v_w^\top v_c)+\sum_{i=1}^{k}\log\sigma(-v_w^\top v_{w_i})\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 是 sigmoid 函数，第一项鼓励真实配对的内积更大，第二项鼓励噪声配对的内积更小。这样一来，计算量就从与词表大小同阶，降到与 <span displaypfx="inline-" class="mathjax-container">\(1+k\)</span> 个样本同阶。负样本也不一定完全随机：Word2Vec 常按词频的 <span displaypfx="inline-" class="mathjax-container">\(0.75\)</span> 次方采样；现代对比学习则常用 in-batch negatives 或 hard negative mining。推荐系统召回、知识图谱嵌入、句向量训练等任务里，这种思想到今天仍然非常常见。</p>
<div class="blog_h3"><span class="graybg">迁移学习</span></div>
<p>迁移学习（Transfer Learning）讨论的是：先在数据更丰富、任务更通用的源任务上学到参数或表示，再把这些知识迁移到目标任务。它不是按监督信号划分出来的独立“学习方式”，而是一种<span style="background-color: #c0c0c0;">跨任务复用知识的训练策略</span>。现代大模型先预训练、再微调，本质上就是迁移学习。</p>
<p>BERT 就是这一思路的典型例子。它通常先在大规模通用文本上做语言建模预训练，例如维基百科（Wikipedia）这类覆盖面很广的语料；模型先学到词法、句法、语义关系以及上下文表示能力。随后再把这一预训练模型迁移到具体任务上，例如情感分类、自然语言推断（NLI）、命名实体识别（NER）或文本匹配，只需接上任务头并用该任务的数据继续微调，就能把通用语言知识转化为面向目标任务的能力。</p>
<p>它与对比学习不在同一层面。对比学习回答的是“预训练阶段该用什么目标来学表示”；迁移学习回答的是“学到的表示如何迁到新任务”。两者经常配合出现：例如先在海量无标签图像上用对比学习预训练视觉编码器，再把该编码器迁移到医学影像分类、工业缺陷检测或小样本识别任务上。</p>
<div class="blog_h3"><span class="graybg">少样本学习</span></div>
<p>少样本学习（Few-shot Learning）处理的是“每个任务只有极少标注样本”时如何仍然快速泛化。它通常建立在迁移学习或预训练模型之上：模型先学到一套通用表示，再在很少示例下快速适配新任务。困难不在于单个任务本身，而在于模型必须把以往经验迁移到新任务上。直觉上，它更像“学会如何快速学习”，而不是“把一个任务彻底学透”。</p>
<div class="blog_h4"><span class="graybg">零样本（Zero-shot）</span></div>
<p>零样本（Zero-shot）指模型在目标任务上没有任何专门示例，也能凭借已有知识完成任务。大语言模型通过指令理解实现的很多能力都属于这一类。例：不给任何情感分类样例，只写“判断下面评论是正面还是负面”，模型仍可能完成分类。</p>
<div class="blog_h4"><span class="graybg">单样本（One-shot）</span></div>
<p>单样本（One-shot）指只给 1 个示例。这个示例的价值不是提供统计规律，而是告诉模型“输出格式、任务边界和你想要的判别标准”。例如先给一条“商品评论 → 正面”的例子，再让模型判断下一条评论。</p>
<div class="blog_h4"><span class="graybg">K 样本（K-shot）</span></div>
<p>K 样本（K-shot）指给每类或每任务提供 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个示例。随着 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 增大，模型更容易对任务意图和判别标准形成稳定估计。工程上，prompt 中的 few-shot 示例本质上就是在上下文窗口里做一种“临时任务适配”。</p>
<div class="blog_h4"><span class="graybg">元学习（Meta-learning / MAML）</span></div>
<p>元学习（Meta-learning）研究“让模型更快适应新任务”。MAML（Model-Agnostic Meta-Learning）的核心不是直接学一个最终答案，而是学一个好的初始化参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，使模型只需少量梯度更新就能适配新任务。</p>
<p>MAML 的外层目标可概括为：</p>
<span displaypfx="" class="mathjax-container">\[\min_\theta \sum_{\mathcal{T}} \mathcal{L}_{\mathcal{T}}\big(\theta-\alpha\nabla_\theta \mathcal{L}_{\mathcal{T}}(\theta)\big)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{T}\)</span> 表示一个任务， <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 是内层更新步长。读法是：先用当前参数在某任务上走一步，再看更新后的参数在该任务上的表现好不好；如果“一步后就变好”，说明初始化是好的。类比来看，MAML 训练的不是“会做每道题的学生”，而是“只要老师讲一遍就能迅速举一反三的学生”。</p>
<div class="blog_h4"><span class="graybg">原型网络（Prototypical Networks）</span></div>
<p>原型网络（Prototypical Networks）把每个类别表示成嵌入空间中的一个“类中心（Prototype）”。对类别 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，其原型定义为该类支持集（Support Set）样本嵌入的平均：</p>
<span displaypfx="" class="mathjax-container">\[c_k=\frac{1}{|S_k|}\sum_{(x_i,y_i)\in S_k,\ y_i=k} f_\theta(x_i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 是样本的向量表示， <span displaypfx="inline-" class="mathjax-container">\(S_k\)</span> 是类别 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的支持样本集合。分类时，把新样本映射到嵌入空间，看它离哪个原型最近。直觉上，这像“每一类先算一个代表点，新样本按离哪个代表点最近来归类”。在 few-shot 图像分类中，这种方法往往比直接训练复杂分类头更稳。</p>
<div class="blog_h2"><span class="graybg">表示聚合与池化</span></div>
<p>池化（Pooling）可以先按一句人话来理解：<span style="background-color: #c0c0c0;">把一组相邻或相关的特征，压缩成更短、更稳定、更容易继续处理的摘要</span>。它不是重新发明新特征，而是对已有特征做聚合（Aggregation）或下采样（Downsampling）。这里的下采样指：<span style="background-color: #c0c0c0;">沿某些维度减少位置数或采样点数，让表示尺寸变小、分辨率变粗</span>。例如把 <span displaypfx="inline-" class="mathjax-container">\(4\times 4\)</span> 的特征图压成 <span displaypfx="inline-" class="mathjax-container">\(2\times 2\)</span>，或把一长段序列压成更短的摘要向量，都属于下采样。</p>
<p>若把一组输入特征记为 <span displaypfx="inline-" class="mathjax-container">\(x_1,\dots,x_k\)</span>，则池化可以抽象写成</p>
<span displaypfx="" class="mathjax-container">\[y=\mathrm{Pool}(x_1,\dots,x_k)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Pool}\)</span> 可以是最大值（Max Pooling）、平均值（Average Pooling）、求和（Sum Pooling）或更复杂的加权聚合。它们做的事不同，但主线一致：<span style="background-color: #c0c0c0;">把“多个位置/多个元素的表示”变成“更少的表示”</span>。</p>
<p>池化之所以重要，是因为很多任务并不需要保留每个细节位置的完整分辨率。图像分类不一定关心边缘恰好落在第 17 个还是第 18 个像素；句子分类也不一定要求记住某个情绪词出现在第 6 个还是第 7 个 token。此时，把局部细节适度压缩，往往能提升稳定性、降低计算量，并让后续层更关注“有没有出现模式”，而不是“模式的坐标是否一模一样”。</p>
<div class="blog_h3"><span class="graybg">最常见的几种池化</span></div>
<p>最大池化（Max Pooling）保留一组特征里最强的那个响应。若某个窗口里有一个边缘、某个关键词或某个邻居信号特别强，最大池化会把它留下来。它更像在问：<span style="background-color: #c0c0c0;">这一小块区域里，最显著的模式有没有出现</span>。</p>
<p>平均池化（Average Pooling）对一组特征取平均，更强调整体趋势而不是最强局部点。它更像在问：这一块区域总体上激活强不强、语义平均水平如何。</p>
<p>求和池化（Sum Pooling）常见于图网络和集合建模，用于累积总量信息。若节点数量本身有意义，求和会把“有多少邻居/总共多强”也编码进去；平均池化则更强调归一化后的平均强度。</p>
<p>全局池化（Global Pooling）表示不再只看局部窗口，而是直接把整张特征图、整段序列或整个节点集合压成一个向量。例如全局平均池化（Global Average Pooling, GAP）会把一整个空间维度平均掉，得到“每个通道在全局上的平均响应”。自适应池化（Adaptive Pooling）则把输出尺寸预先固定，例如无论输入特征图多大，最终都压成 <span displaypfx="inline-" class="mathjax-container">\(1\times 1\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(7\times 7\)</span>。</p>
<div class="blog_h3"><span class="graybg">不同网络里的含义</span></div>
<p>在卷积神经网络（CNN）里，池化最经典的含义是<span style="background-color: #c0c0c0;">沿空间维度做局部下采样</span>。例如一张特征图经过 <span displaypfx="inline-" class="mathjax-container">\(2\times 2\)</span> 最大池化后，宽高会缩小，局部最强响应被保留下来。它的直接收益有三点：减小特征图尺寸、扩大后续层的感受野（Receptive Field）、并降低模型对小幅平移和局部扰动的敏感度。</p>
<p>在时序模型和文本模型里，池化更常表示<span style="background-color: #c0c0c0;">沿时间或序列长度维度做聚合</span>。例如把所有 token 表示做平均池化，得到整句向量；把一段音频帧表示做最大池化，得到“这一整段里最强的模式”。这里池化的重点不再是二维空间下采样，而是把变长序列压成固定长度表示，方便做分类、检索或相似度计算。</p>
<p>在图神经网络（GNN）里，池化有两层常见含义。第一层是邻域聚合（Neighborhood Aggregation）：一个节点把邻居表示做均值、求和或最大值，再更新自己；这可以理解为“节点级局部池化”。第二层是图级读出（Graph-level Readout）：把整张图的节点表示再做一次全局聚合，得到整个图的表示，用于图分类、图回归等任务。</p>
<p>在 Transformer 里，池化通常不再以“池化层”这一模块形式高频出现，但概念仍然存在。句子分类常取 <span displaypfx="inline-" class="mathjax-container">\([\mathrm{CLS}]\)</span> 位置表示，或对所有 token 做平均池化；Embedding 模型也常对最后一层隐藏状态做 mean pooling / max pooling 得到句向量。进一步看，注意力（Attention）本身也可以理解成一种<span style="background-color: #c0c0c0;">带内容依赖的加权聚合</span>：区别只在于普通池化的规则通常固定，而注意力的权重是由输入动态决定的。</p>
<div class="blog_h3"><span class="graybg">池化到底保留了什么、丢掉了什么</span></div>
<p>池化保留的是摘要信息，丢掉的是更精细的位置细节。最大池化更偏向“是否出现过显著模式”，平均池化更偏向“整体平均状态如何”，求和池化则更偏向“总量有多大”。因此，池化总带有一种 trade-off：表示更紧凑、更稳、更省算力，但精确定位能力会下降。</p>
<p>这也是为什么不同任务会选择不同聚合方式。图像分类往往欢迎一定程度的位置不敏感，因此池化很自然；语义检索希望一整句压成一个句向量，因此句级池化很常见；但像语义分割、目标检测、序列标注这类任务，输出本身依赖逐位置判断，就不能过早把位置信息池掉，否则细粒度边界会被抹平。</p>
<div class="blog_h3"><span class="graybg">上采样、插值与转置卷积</span></div>
<p>若说下采样（Downsampling）是在压缩表示，那么上采样（Upsampling）就是反过来<span style="background-color: #c0c0c0;">把较粗的表示扩展回更细的空间分辨率</span>。它做的不是凭空创造新信息，而是把低分辨率特征重新铺回更高分辨率的位置网格中，方便恢复空间结构、边界或局部细节。</p>
<p>插值（Interpolation）是最直接的上采样方式。最邻近插值（Nearest Neighbor Interpolation）相当于把原像素或原特征直接复制到更密的网格里，速度快、实现简单，但边界可能显得生硬；双线性插值（Bilinear Interpolation）则按周围位置做平滑加权，结果更连续，但也更容易把边界抹平。它们的共同特点是：上采样规则是固定的，不需要学习参数。</p>
<p>转置卷积（Transposed Convolution）则是<span style="background-color: #c0c0c0;">可学习的上采样</span>。它不是“把普通卷积矩阵简单转置后就 magically 还原图像”，而是指：若把普通卷积看作一个线性算子，转置卷积对应的是这个线性算子的转置形式，因此能够把较小的特征图映射到较大的特征图。因为它带有可学习卷积核，所以模型可以学习“该怎样把粗特征展开成更适合任务的细特征”。</p>
<p>在视觉任务里，这三者的分工很常见。语义分割、图像生成、超分辨率、U-Net 解码器、扩散模型解码器都需要上采样：有时先用插值把尺寸放大，再接普通卷积细化；有时直接用转置卷积一步完成“放大 + 学习性重组”。前者通常更稳、更少棋盘格伪影（Checkerboard Artifacts），后者表达力更强，但更依赖实现细节与核大小、步幅（Stride）设置。</p>
<div class="blog_h2"><span class="graybg">泛化、过拟合与偏差—方差</span></div>
<p>机器学习更关心模型离开训练集之后能否维持稳定表现。泛化（Generalization）、过拟合（Overfitting）、欠拟合（Underfitting）与偏差—方差权衡（Bias–Variance Tradeoff）描述的，就是这件事。</p>
<div class="blog_h3"><span class="graybg">泛化</span></div>
<p>泛化（Generalization）指模型在未见样本上的表现。若训练数据与未来输入都来自同一数据分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>，则模型的总体风险可写成：</p>
<span displaypfx="" class="mathjax-container">\[R(f)=\mathbb{E}_{(x,y)\sim P}\big[\ell(f(x),y)\big]\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(R(f)\)</span> 是真实风险（Population Risk），表示模型 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在整个真实分布上的平均损失； <span displaypfx="inline-" class="mathjax-container">\(\ell(f(x),y)\)</span> 是单样本损失；期望 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}\)</span> 表示对所有可能样本做平均。训练时真正能看到的只有有限样本，因此优化的通常是经验风险 <span displaypfx="inline-" class="mathjax-container">\(\hat R_n(f)\)</span>，而不是这个理想化的总体风险。</p>
<p>训练误差与测试误差之间的差距，常称为泛化间隙（Generalization Gap）。间隙小说明模型在未见数据上比较稳定；间隙大则说明模型过度依赖训练样本中的偶然细节。</p>
<div class="blog_h3"><span class="graybg">内插与外推</span></div>
<p>内插（Interpolation）与外推（Extrapolation）描述的是：模型面对未见样本时，究竟是在<span style="background-color: #c0c0c0;">已观测范围之内补全规律</span>，还是在<span style="background-color: #c0c0c0;">已观测范围之外延伸规律</span>。二者都属于预测，但难度和风险完全不同。</p>
<p>以一维回归为例，若训练样本的输入主要落在区间 <span displaypfx="inline-" class="mathjax-container">\([a,b]\)</span> 内，那么对 <span displaypfx="inline-" class="mathjax-container">\(x\in[a,b]\)</span> 附近新样本做预测，更接近内插；对明显落在这个范围之外的 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 做预测，则更接近外推。内插依赖的是“训练数据已经覆盖了这片区域”；外推依赖的是“模型学到的规律在未见区域仍然继续成立”。后者显然要求更强。</p>
<p>在机器学习里，大多数标准泛化讨论其实都更接近内插。只要训练集与测试集近似满足独立同分布（IID）假设，测试样本通常仍然落在训练分布支持集（Support）附近，模型主要是在已知数据流形附近做平滑补全。这也是为什么现代高容量模型即使参数极多，只要训练分布覆盖充分，仍然能在测试集上表现得相当稳定。</p>
<p>外推对应的则是更困难的分布外泛化（Out-of-Distribution Generalization, OOD Generalization）。此时，测试输入不再只是训练分布中的轻微变化，而是进入了训练时很少见、甚至从未见过的区域。自动驾驶模型若只在晴天高速公路上训练，却要在暴雪、泥地和夜间乡道上决策；医学模型若主要见过成人数据，却被要求用于儿童病例；金融模型若只在平稳市场阶段训练，却要应对极端波动期，这些都属于外推问题。</p>
<p>外推之所以困难，根源在于经验风险最小化并不自动保证“规律可被安全延伸到训练分布之外”。模型完全可能在训练分布内部拟合得很好，却在一旦离开这片区域后迅速失效。因此，外推能力往往比普通测试集精度更能检验模型究竟学到了稳定结构，还是只学会了训练分布内部的高质量插值。</p>
<p>大语言模型里有一种非常典型的外推形式：长度外推（Length Extrapolation）。若模型训练时主要见到 <span displaypfx="inline-" class="mathjax-container">\(4\mathrm{K}\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(8\mathrm{K}\)</span> 上下文，却在推理时被要求处理 <span displaypfx="inline-" class="mathjax-container">\(128\mathrm{K}\)</span> 甚至更长序列，那么它面对的就不再只是“在熟悉长度范围内继续理解”，而是在训练长度之外延伸位置建模规律。RoPE 缩放、NTK-aware 调整、YaRN 等方法，本质上都是在尽量让模型把短上下文中学到的位置规律外推到更长序列上。</p>
<p>因此，可以把两者压缩成一句话：内插更像在已知区域里补全空白，外推更像拿着已总结出的规律去穿越未知边界。前者是标准机器学习评测里的常态，后者则更接近真实系统进入新环境时会遭遇的硬问题。</p>
<div class="blog_h3"><span class="graybg">过拟合</span></div>
<p>过拟合（Overfitting）指模型在训练集上持续吸收局部细节、噪声与偶然模式，但这些信息不能稳定迁移到未见样本上。它的典型外观是：训练集表现继续改善，而验证集或测试集表现停止改善、开始恶化，二者之间的泛化间隙（Generalization Gap）不断扩大。</p>
<p>过拟合描述的是一种训练现象，本身并不自动等于“模型已经不可用”。更准确的分析方式，是区分<span style="background-color: #c0c0c0;">模型到底过拟合了什么</span>。分类任务里最常见的两类退化并不完全相同：一类是决策边界本身开始贴合训练集偶然性；另一类是分类边界大体没变，但模型对既有判断越来越极端，概率校准逐步恶化。</p>
<div class="blog_h4"><span class="graybg">两种常见退化</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">退化对象</td>
<td style="text-align: center;">训练期常见信号</td>
<td style="text-align: center;">主要后果</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策边界过拟合</td>
<td>模型学到只在训练集上成立的判别规则</td>
<td>训练 F1 / Accuracy 持续上升，验证 F1 / Accuracy 停滞或下降；训练 loss 很低而验证指标回落</td>
<td>真正损害分类泛化，换一批数据就更容易判错</td>
</tr>
<tr>
<td>置信度过拟合</td>
<td>模型对已有判断越来越极端，logit 持续膨胀</td>
<td>验证 F1 基本稳定，但验证 loss、Brier Score、ECE 等校准指标恶化；预测概率更频繁地逼近 0 或 1</td>
<td>硬分类结果可能不变，但概率值本身变得不可信</td>
</tr>
</tbody>
</table>
<p>前一类退化直接伤害模型的判别泛化，后一类退化主要伤害概率校准（Calibration）。因此，若系统只需要固定阈值之后的硬标签，置信度过拟合通常是次一级问题；若系统依赖概率值本身做风险分层、阈值调度、排序融合或人工兜底，置信度过拟合就会立刻变成工程问题。</p>
<div class="blog_h4"><span class="graybg">训练期间的识别信号</span></div>
<p>识别过拟合时，不能只盯着单一 loss，而要同时观察<span style="background-color: #c0c0c0;">训练集指标、验证集指标、概率分布形状与误差结构</span>。最典型的信号包括：</p>
<ul>
<li>训练 loss 持续下降，但验证 loss 在某个阶段后停止下降，随后回升。</li>
<li>训练 Accuracy / F1 继续提高，而验证 Accuracy / F1 停滞甚至下滑。这通常提示决策边界开始贴近训练集特有模式。</li>
<li>验证 F1 基本稳定，但验证 loss 明显变差。这类“指标稳、loss 崩”的组合，更接近置信度过拟合而不是判别边界崩坏。</li>
<li>logit 绝对值持续增大，softmax 或 sigmoid 输出更集中到接近 0 和 1 的两端，说明模型在继续放大自信度。</li>
<li>训练后期错误样本逐渐集中在少量硬样本，而 easy case 的置信度仍在继续极化，说明模型已经不再学新的判别规律，而是在强化已有判断的幅度。</li>
<li>不同随机种子、不同验证切分下的波动变大，说明模型开始依赖训练样本中的偶然结构。</li>
</ul>
<div class="blog_h4"><span class="graybg">常见诱因</span></div>
<p>过拟合并不只由“参数太多”导致。更常见的诱因包括：样本量不足、类别长尾、标签噪声、训练集与线上分布不一致、数据泄露、训练时间过长、正则化过弱、batch 过小导致梯度噪声放大，以及高重复语料让模型过度记忆头部模式。对深度模型而言，容量大只是风险放大器，真正决定是否过拟合的，通常是<span style="background-color: #c0c0c0;">模型自由度与有效监督信号之间是否失衡</span>。</p>
<div class="blog_h3"><span class="graybg">欠拟合</span></div>
<p>欠拟合（Underfitting）指模型连训练数据中的主要结构都没有学出来，表现为训练集和验证集都做不好。它对应的是高偏差（High Bias）状态：<span style="background-color: #c0c0c0;">模型的平均预测长期偏离真实目标</span>，也就是模型拟合能力不够，或训练过程根本还没有进入足够低误差的区域。</p>
<p>与过拟合相比，欠拟合的特征通常不是“训练和验证拉开了差距”，而是<span style="background-color: #c0c0c0;">两边都差，而且差得很一致</span>。训练集上的 loss 仍然偏高，训练 Accuracy / F1 也上不去，说明模型尚未把任务主结构写进参数中。</p>
<div class="blog_h4"><span class="graybg">训练期间的识别信号</span></div>
<ul>
<li>训练 loss 和验证 loss 都较高，而且两者相差不大。</li>
<li>训练 Accuracy / F1 与验证 Accuracy / F1 同时偏低，没有形成明显的泛化间隙。</li>
<li>训练到后期时，两条曲线仍然一起缓慢下降，说明模型可能还没收敛；若提前停止训练，问题更接近“没训练够”。</li>
<li>即使继续训练较长时间，训练指标仍然明显低于任务应有上限，说明容量、特征或优化配置本身不足。</li>
<li>错误并不只集中在边界样本或少数难例，而是连大量 easy case 都无法稳定学会。</li>
</ul>
<div class="blog_h4"><span class="graybg">常见诱因</span></div>
<p>欠拟合常见于模型容量过小、特征表达弱、模型结构与任务不匹配、正则化过强、学习率设置不当、训练轮数不够、输入信息被过度截断，或任务本身需要非线性组合而模型只允许非常受限的线性表达。工程上，欠拟合也经常伪装成“模型很稳但一直不强”：曲线不震荡、训练不发散，却始终到不了可接受的性能区间。</p>
<div class="blog_h4"><span class="graybg">与过拟合的区分</span></div>
<p>一个实用判断准则是先看训练集是否已经被充分学会。若训练集指标本身就很差，优先考虑欠拟合；若训练集指标很好而验证集开始回落，优先考虑过拟合；若验证集分类指标基本不动，但验证 loss 与校准指标变差，则更接近置信度过拟合。把这三种状态区分清楚，后续的调参与正则化方向才不会混淆。</p>
<div class="blog_h3"><span class="graybg">坍缩（Collapse）</span></div>
<p>坍缩（Collapse）描述的是训练过程中的一种<span style="background-color: #c0c0c0;">退化解（Degenerate Solution）</span>：模型表面上仍在输出结果，但内部表示、预测分布或优化轨迹已经失去有效多样性，学习过程塌到某种简单、无用或几乎无信息的模式上。它和过拟合不同。过拟合仍然在“认真地区分训练样本”，只是把训练集细节学得过头；坍缩则意味着模型逐渐失去区分能力，或者训练目标退化为某种几乎不再提供有效学习信号的状态。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">典型表现</td>
<td style="text-align: center;">例子</td>
</tr>
</thead>
<tbody>
<tr>
<td>Mode Collapse</td>
<td>不管输入如何变化，输出都向少数模式收缩，预测类别或生成模式高度单一</td>
<td>分类器几乎把所有样本都判成“满意”；GAN 只会生成少数几种图像</td>
</tr>
<tr>
<td>Representation Collapse</td>
<td>编码器输出趋于相同或近似相同的向量，样本间表示几何结构被压扁</td>
<td>所有 hidden state / embedding 都高度相似，下游分类器只能依赖噪声做区分</td>
</tr>
<tr>
<td>Objective / Loss Collapse</td>
<td>训练目标迅速退化到近乎恒定的无信息状态，loss 长时间停在极低、极高或近乎不变的单一水平，梯度也可能同步衰减</td>
<td>自监督目标被模型用常数解“钻空子”；梯度消失后 loss 几乎不再变化；某些错误实现让目标函数被提前满足</td>
</tr>
</tbody>
</table>
<p>其中前两类最常见也最容易直观理解。Mode Collapse 强调<span style="background-color: #c0c0c0;">输出空间的多样性消失</span>；Representation Collapse 强调<span style="background-color: #c0c0c0;">内部表示空间的多样性消失</span>。第三类常被笼统地称作 loss collapse，但更准确的理解是“优化目标退化”或“训练目标坍缩”：loss 本身只是一个观测信号，真正的问题在于模型已经进入某种几乎不再产生有效学习内容的状态。</p>
<p>判断坍缩时，关键不是只看某一个时刻的平均 loss，而是看<span style="background-color: #c0c0c0;">输出分布、表示分布与样本间差异是否还存在</span>。若某个 epoch 中同时出现极低 loss 样本和极高 loss 样本，说明模型仍在把 easy case 与 hard case 区分开，训练信号仍有明显异质性；这更像正常训练中的难度分层，而不是已经坍缩。真正的坍缩通常会伴随更一致的退化迹象，例如预测类别快速单一化、embedding 方差急剧缩小、梯度长期接近 0，或者 loss 在大多数样本上收缩到近乎同一个无信息水平。</p>
<p>坍缩在不同任务中的诱因并不相同。对比学习（Contrastive Learning）里若缺少 stop-gradient、predictor、负样本或方差保持机制，表示空间很容易整体塌平；生成模型里，判别器与生成器失衡会诱发 mode collapse；分类任务里，极端类别不平衡、错误的损失实现、过强正则化或训练数据本身标签塌缩，都可能把模型推向低信息输出。工程上监控坍缩，通常需要同时看 loss、预测类别分布、embedding 方差、梯度范数与验证集指标，而不能只盯着单一数值曲线。</p>
<div class="blog_h3"><span class="graybg">偏差与方差</span></div>
<p>偏差（Bias）与方差（Variance）是分析泛化误差来源的经典视角。对平方损失（Squared Loss），常见分解写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}\big[(Y-\hat f(X))^2\big]=\mathrm{Bias}^2+\mathrm{Variance}+\sigma^2\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[(Y-\hat f(X))^2]\)</span> 是模型的平均平方误差； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Bias}^2\)</span> 表示模型平均预测与真实函数之间的系统性偏离； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Variance}\)</span> 表示模型对训练样本波动的敏感度； <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 是数据本身不可约的噪声（Irreducible Noise），即使模型和训练过程都完美，也无法完全消除。</p>
<p>更直白地说，<span style="background-color: #c0c0c0;">偏差看的是“模型平均预测离真实值有多远”，代表模型的拟合能力；方差看的是“换一份训练集后模型预测会抖动多大”，代表模型的稳定性</span>。偏差高通常对应欠拟合，因为模型学不到足够有效的数据规律；方差高通常对应过拟合，因为模型过度依赖某一份训练集的细节，离开训练集后泛化能力变差。</p>
<p>工程上，降低偏差常靠更强模型、更好特征和更充分训练；降低方差常靠更多数据、正则化、数据增强、早停（Early Stopping）和集成学习（Ensemble Learning）。很多建模决策，本质上都是在“预测还不够准”和“预测太不稳定”之间做权衡。下表用几类典型树模型把这种权衡具体化。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">偏差与方差的典型状态</td>
<td style="text-align: center;">为什么会这样</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策树</td>
<td>高偏差 + 高方差风险并存</td>
<td>没有集成机制时，树过浅会欠拟合、偏差高；树过深又会对训练集细节极敏感、方差高，因此属于“双风险”模型</td>
</tr>
<tr>
<td>随机森林</td>
<td>低偏差 + 低方差</td>
<td>单棵深树先把偏差压低，再通过 Bagging 平均掉树与树之间的高方差</td>
</tr>
<tr>
<td>GBDT / XGBoost</td>
<td>低偏差 + 低方差</td>
<td>串行累加很多棵浅树持续降低偏差，而单棵浅树本身方差较低，再配合学习率、正则化与早停控制整体方差</td>
</tr>
</tbody>
</table>
<p>因此，偏差与方差不是抽象口号，而是在解释不同模型为什么会“学不动”或“学过头”。看树模型尤其直观：单树的问题是两头都可能出错；随机森林主要靠并行平均压方差；Boosting 家族主要靠串行纠错压偏差，再用浅树和正则化把方差稳住。</p>
<div class="blog_h2"><span class="graybg">经验风险最小化与正则化</span></div>
<p>大部分监督学习训练都可以概括成同一条主线：先定义单样本损失，再在训练集上取平均形成经验风险，最后通过优化算法把它压低。正则化（Regularization）是在这条主线之上加入额外约束，用来控制复杂度并改善泛化。</p>
<div class="blog_h3"><span class="graybg">经验风险最小化</span></div>
<p>经验风险最小化（Empirical Risk Minimization, ERM）是统计学习的基本训练原则。设训练集为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{D}=\{(x_i,y_i)\}_{i=1}^{n}\)</span>，则经验风险定义为：</p>
<span displaypfx="" class="mathjax-container">\[\hat R_n(f)=\frac{1}{n}\sum_{i=1}^{n}\ell(f(x_i),y_i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是样本数， <span displaypfx="inline-" class="mathjax-container">\(f(x_i)\)</span> 是模型对第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的预测， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实标签， <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span> 是损失函数。经验风险最小化就是在假设空间 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span> 中寻找让这条平均损失最小的函数：</p>
<span displaypfx="" class="mathjax-container">\[\hat f_{\mathrm{ERM}}=\arg\min_{f\in\mathcal{H}}\hat R_n(f)\]</span>
<p>这条原则覆盖范围极广。线性回归最小化的是平方误差经验风险，逻辑回归和多分类神经网络最小化的是交叉熵经验风险，序列标注模型最小化的是序列级条件对数似然。算法形式不同，骨架是一致的。</p>
<div class="blog_h3"><span class="graybg">正则化</span></div>
<p>正则化（Regularization）是在经验风险之外，再加入一个偏好“更简单、更平滑、更稳定”解的约束项。常见写法是：</p>
<span displaypfx="" class="mathjax-container">\[\hat f=\arg\min_{f\in\mathcal{H}}\hat R_n(f)+\lambda\,\Omega(f)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)\)</span> 是正则项（Regularizer），刻画模型复杂度； <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是正则化强度，决定“拟合训练集”和“控制复杂度”之间的权衡。 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越大，模型越保守；越小，模型越自由。</p>
<p>L2 正则（L2 Regularization）偏好较小权重，常写成 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)=\|\mathbf{w}\|_2^2\)</span>；L1 正则（L1 Regularization）偏好稀疏解，常写成 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)=\|\mathbf{w}\|_1\)</span>。更广义地看，早停、Dropout、数据增强、权重共享、标签平滑、参数冻结、低秩适配，都是在不同层面对模型自由度施加约束，因此都可以看作正则化思想的工程实现。</p>
<div class="blog_h2"><span class="graybg">IID 与分布偏移</span></div>
<p>机器学习中的很多训练与评估结论，都建立在一个默认前提上：训练样本与未来样本来自同一统计机制。这个前提通常写成 IID（Independent and Identically Distributed，独立同分布）假设。只要这个前提破坏，训练集表现与线上表现之间就可能出现明显断裂。</p>
<div class="blog_h3"><span class="graybg">IID 假设</span></div>
<p>设样本对 <span displaypfx="inline-" class="mathjax-container">\((x_i,y_i)\)</span> 来自某个联合分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>，IID 假设写成：</p>
<span displaypfx="" class="mathjax-container">\[(x_1,y_1),\dots,(x_n,y_n)\overset{\mathrm{iid}}{\sim}P(X,Y)\]</span>
<p>这里“独立（Independent）”表示一个样本是否出现，不影响另一个样本的生成；“同分布（Identically Distributed）”表示所有样本都来自同一个联合分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>。这个假设让训练集平均损失能够作为总体风险的近似，也让交叉验证、置信区间和很多泛化理论成立。</p>
<p>IID 是理想化近似，而不是自然界的铁律。时间序列、推荐系统、金融交易、医疗数据、A/B 实验日志、用户行为数据，常常都存在相关性、群组效应、时间漂移或采样偏差，因此不能机械套用 IID 设定。</p>
<div class="blog_h3"><span class="graybg">分布偏移</span></div>
<p>当训练分布与测试分布不一致时，就发生了分布偏移（Distribution Shift）：</p>
<span displaypfx="" class="mathjax-container">\[P_{\mathrm{train}}(X,Y)\neq P_{\mathrm{test}}(X,Y)\]</span>
<p>分布偏移有几种常见形式。协变量偏移（Covariate Shift）指 <span displaypfx="inline-" class="mathjax-container">\(P(X)\)</span> 变化，而 <span displaypfx="inline-" class="mathjax-container">\(P(Y|X)\)</span> 基本稳定；例如线上用户年龄结构变了，但“给定用户画像时是否点击”的规律没明显变。标签偏移（Label Shift）指 <span displaypfx="inline-" class="mathjax-container">\(P(Y)\)</span> 变化，例如欺诈率在促销期突然上升。概念漂移（Concept Drift / Concept Shift）指 <span displaypfx="inline-" class="mathjax-container">\(P(Y|X)\)</span> 本身发生变化，例如垃圾邮件发送策略升级后，原来有效的文本模式不再可靠。</p>
<p>因此，训练集、验证集与测试集的切分不能只追求随机均匀，还必须尽量模拟未来部署环境。若线上是时间推进场景，测试集就应按时间后移；若线上按用户或设备泛化，切分就应按实体隔离；若业务分布持续漂移，还需要做持续监控、重训和再校准。分布偏移不是评估里的边角问题，而是机器学习系统走向生产后的主要失效来源之一。</p>
<div class="blog_h3"><span class="graybg">OOD</span></div>
<p>OOD 是 Out-of-Distribution 的缩写，意为分布外（Out-of-Distribution）。它强调的是：<span style="background-color: #c0c0c0;">当前输入已经落到训练分布覆盖较弱、甚至根本没有覆盖的区域</span>。与一般意义上的“有点噪声”不同，OOD 更像是模型被带到了一个不熟悉的世界里。</p>
<p>例如，一个文本分类模型训练时主要见到的是规范书面语，部署后却大量遇到拼写错误、口语、英文混杂、模板化投诉和新产品名称；一个视觉模型训练时主要见到晴天白昼图像，线上却开始接收夜间、雨雪和红外图像。这类输入即使形式上仍然属于同一任务，也可能已经超出训练分布支持范围。OOD 检测与分布外泛化因此成为真实系统里的关键问题：模型不只要尽量判对，还要在“不熟悉”时知道自己不熟悉。</p>
<div class="blog_h3"><span class="graybg">数据漂移</span></div>
<p>数据漂移（Data Drift）强调的是<span style="background-color: #c0c0c0;">线上数据分布会随着时间持续变化</span>。它和 OOD 高度相关，但语境更偏工程系统：不是某一个样本偶然跑出了训练分布，而是整体数据来源、用户群体、业务流程或采集方式正在逐步改变。</p>
<p>若输入分布 <span displaypfx="inline-" class="mathjax-container">\(P(X)\)</span> 发生变化，常称为数据漂移或协变量漂移；若标签分布 <span displaypfx="inline-" class="mathjax-container">\(P(Y)\)</span> 变化，常表现为类别比例变化；若 <span displaypfx="inline-" class="mathjax-container">\(P(Y|X)\)</span> 也改变，则更接近概念漂移。现实系统里，这些变化往往同时发生。例如促销活动带来全新的用户结构，新功能改变用户行为路径，标注口径调整导致同类样本的标签规则也跟着变化。数据漂移的工程含义很直接：离线验证通过，并不意味着模型可以长期稳定在线上工作，监控、告警、回灌和重训机制必须跟上。</p>
<div class="blog_h3"><span class="graybg">鲁棒性</span></div>
<p>鲁棒性（Robustness）指模型在噪声、扰动、输入变形和分布变化下，性能是否仍能维持稳定。它关心的不只是“在标准测试集上最高能到多少分”，更关心<span style="background-color: #c0c0c0;">输入一旦变脏、变偏、变怪，模型会不会立刻失效</span>。</p>
<p>鲁棒性与 OOD、数据漂移不是同一概念，但三者紧密相关。OOD 和数据漂移描述的是输入环境发生了什么变化；鲁棒性描述的是模型面对这些变化时的承受能力。一个鲁棒性差的模型，可能在干净样本上分数很高，却会被轻微拼写错误、格式扰动、图像模糊、特征缺失或采样偏移迅速击穿。真实生产系统里，鲁棒性通常比单次 benchmark 分数更接近“模型能否长期可用”这个问题。</p>
<div class="blog_h2"><span class="graybg">数据集工程</span></div>
<p>数据集工程（Dataset Engineering）决定了模型看到什么、以什么尺度看到、又会被哪些偏差误导。很多所谓“模型问题”，根源其实是数据问题：标签噪声、分布漂移、类别极不均衡或特征泄漏，都会直接扭曲训练结果。</p>
<div class="blog_h3"><span class="graybg">黄金/白银数据集</span></div>
<p>数据集工程里常见一个实用分层：黄金数据集（Gold Dataset）与白银数据集（Silver Dataset）。黄金数据集通常指由高质量人工标注、规则严格审核或专家确认得到的小而精数据，标签噪声低，适合做最终评测、关键验证集或高价值监督信号；白银数据集则通常来自启发式规则、弱监督（Weak Supervision）、模型打标、日志回收或大规模自动清洗，规模更大、成本更低，但噪声也更高。实际工程中，常见策略不是二选一，而是用白银数据集提供覆盖面和规模，用黄金数据集提供校准、纠偏与最终可信评估。</p>
<div class="blog_h3"><span class="graybg">数据划分</span></div>
<p>数据划分的目标，是把<span style="background-color: #c0c0c0;">学参数、做模型选择、汇报最终结果</span>这三件事严格隔离开。若同一批数据既用来训练参数，又用来调超参数，最后还拿来汇报效果，评估结果通常会乐观得不真实，因为模型已经间接“看过”了答案。</p>
<p>从统计学习角度看，这三类数据分别承担三种不同职责：训练集负责让模型学习参数；验证集负责帮助人或训练流程做工程决策；测试集负责模拟真正的未知数据，给出最后一次、尽量无偏的泛化评估。三者分工清楚，模型评估才有可信度。</p>
<div class="blog_h4"><span class="graybg">训练集</span></div>
<p>训练集（Training Set）用于更新模型参数。监督学习中，训练集包含输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 与标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>；模型在这批样本上计算损失（Loss）、反向传播梯度（Gradient）并更新参数，因此训练集直接决定“模型学到了什么”。它回答的问题是：<span style="background-color: #c0c0c0;">在已观测样本上，模型有没有学会输入与输出之间的对应关系</span>。</p>
<p>训练集通常应占数据的大头，因为参数学习需要足够多的样本来稳定估计模式。实践中常见比例是 70% 到 80%，但这不是固定规则：若数据总量非常大，训练集比例可以更高；若数据本来就少，则往往需要把更多精力放在交叉验证（Cross Validation）而不是死守固定比例。</p>
<p>训练集上的误差通常是三者里最低的，这并不说明模型已经具有泛化能力。一个模型完全可能在训练集上表现极好，却只是记住了样本中的噪声与偶然性。训练集成绩更多反映“拟合能力”，而不是“真实上线表现”。</p>
<div class="blog_h4"><span class="graybg">验证集</span></div>
<p>验证集（Validation Set）用于模型选择（Model Selection）和超参数调优（Hyperparameter Tuning）。它不直接参与参数更新，但会影响训练流程中的关键决策，例如学习率（Learning Rate）、正则化强度、模型深度、树的数量、batch size、阈值选择，以及是否执行 Early Stopping。</p>
<p>验证集回答的问题不是“模型能不能学会”，而是：<span style="background-color: #c0c0c0;">在若干候选配置里，哪一个更可能在新数据上表现最好</span>。因此，验证集像训练过程中的“模拟考试”：它不是最终成绩单，但会决定你在训练期间如何改模型、如何调参数、何时停止训练。</p>
<p>验证集通常占总数据的 10% 到 15% 左右。若数据量很小，单独留出一份验证集的代价会较高，此时更常见的做法是使用 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 折交叉验证，让每个样本轮流充当验证数据，以减少一次随机划分带来的偶然性。交叉验证的细节放在后面的“模型评估”部分展开。</p>
<div class="blog_h4"><span class="graybg">测试集</span></div>
<p>测试集（Test Set）用于最终评估模型的泛化能力（Generalization）。它应尽量只在方案冻结之后使用：模型结构、超参数、训练策略、阈值和后处理规则都不再修改时，才在测试集上做一次最终评估。它回答的问题是：<span style="background-color: #c0c0c0;">如果把模型部署到真实世界，它在新样本上的表现大致会怎样</span>。</p>
<p>测试集通常占总数据的 10% 到 15%。它的重要性不在于比例有多大，而在于它必须保持“未参与决策”。如果开发过程中反复查看测试集结果，并据此继续改模型，那么测试集就已经被污染，不再是独立评测，而变成了另一个隐性的验证集。</p>
<p>因此，测试集更像真正的“高考卷”或“盲测集”：它的价值在于最后一次、尽量无偏的评估，而不是参与训练流程本身。</p>
<div class="blog_h4"><span class="graybg">如何划分数据集</span></div>
<p>最常见的简单划分是训练集 / 验证集 / 测试集 = 70% / 15% / 15%，或 80% / 10% / 10%。这种划分适合样本量较大、类别分布较稳定的任务，因为单次随机切分已经足以给出相对稳定的训练与评估结果。</p>
<p>当数据量较小、类别极不平衡、或者不同子群体差异明显时，划分策略就必须更谨慎。分类任务常采用分层抽样（Stratified Split），确保训练、验证、测试三部分的类别比例大致一致；时间序列任务则必须按时间顺序切分，避免未来信息泄漏到过去；用户级、设备级、病人级任务常需要按实体分组切分，防止同一实体的样本同时出现在训练集和测试集中，造成过于乐观的结果。</p>
<p>因此，“如何划分”本身就是建模的一部分。划分方式若与真实部署场景不一致，即使指标很好，也可能只是评估设定过于宽松，而不是真正泛化能力强。</p>
<div class="blog_h4"><span class="graybg">数据泄露</span></div>
<p>数据泄露（Data Leakage）指测试集或验证集中的信息以直接或间接方式进入训练过程，从而导致模型评估结果虚高。它的危险不在于“代码报错”，而在于模型会表现得看似极好，却无法在真实新数据上复现。</p>
<p>最常见的数据泄露有几类。第一类是<span style="background-color: #c0c0c0;">先对全量数据做预处理，再切分数据</span>，例如先用全量数据计算标准化均值和方差，再划分训练 / 测试集；这样测试集的信息已经进入了训练流程。第二类是<span style="background-color: #c0c0c0;">用测试集反复调参</span>，例如每改一次模型就看一次测试集成绩，直到测试集最好看为止。第三类是<span style="background-color: #c0c0c0;">特征中混入未来或标签信息</span>，例如用预测时不可能知道的字段做输入，或把目标变量的某种变形偷偷带进特征。</p>
<p>避免数据泄露的原则只有一句：<span style="background-color: #c0c0c0;">任何依赖数据分布统计量、特征构造规则、模型选择决策或阈值选择的步骤，都只能在训练集内部完成，再把同样的变换应用到验证集和测试集</span>。标准化、特征选择、缺失值填补、目标编码（Target Encoding）、降维（PCA）和重采样（Resampling）都要遵守这一原则。</p>
<p>因此，数据集划分不只是“把数据分三份”这么简单，而是整个实验设计（Experimental Design）的一部分。只有训练集、验证集、测试集的职责边界清晰，交叉验证使用得当，且数据泄露被严格控制，模型指标才具有解释价值和可复现实验意义。</p>
<div class="blog_h3"><span class="graybg">归一化与标准化</span></div>
<p>归一化（Normalization）与标准化（Standardization）都在解决“不同特征量纲和尺度差异过大”问题，但含义不同。最常见的最小-最大归一化把数据映射到固定区间：</p>
<span displaypfx="" class="mathjax-container">\[x'=\frac{x-x_{\min}}{x_{\max}-x_{\min}}\]</span>
<p>它把特征压到 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span>，适合像像素值、比例值这类天然有上下界的量。标准化则是减去均值、再除以标准差：</p>
<span displaypfx="" class="mathjax-container">\[z=\frac{x-\mu}{\sigma}\]</span>
<p>标准化后的特征均值为 0、标准差为 1，更适合线性模型、距离模型和很多神经网络优化过程。类比来看，归一化像“把不同长度的尺子都缩到同一长度区间”；标准化像“先平移到共同中心，再按波动尺度统一单位”。</p>
<div class="blog_h3"><span class="graybg">特征工程</span></div>
<p>特征工程（Feature Engineering）是把原始数据加工成更利于模型学习的表示。它不是“多造点列”这么简单，而是在把领域知识编码进输入空间。例：时间戳可以拆成小时、星期、是否节假日；用户行为日志可以构造近 7 天点击次数、转化率、时间衰减统计；文本可以做 TF-IDF、n-gram 或实体抽取。</p>
<p>类比来看，特征工程像做菜前的备料：同样的原料，如果已经切片、去骨、配好比例，后续烹饪会顺畅得多。经典机器学习对特征工程高度依赖；深度学习则把一部分特征学习自动化了，但在表格数据、推荐、广告和风控里，特征工程仍然决定上限。</p>
<div class="blog_h3"><span class="graybg">类别不平衡处理</span></div>
<p>类别不平衡（Class Imbalance）指某些类别样本远多于另一些类别。欺诈检测、故障检测、医学筛查里最典型：正类往往极少。如果不处理，模型可能通过“永远预测多数类”获得看似不错的 Accuracy，却在关键少数类上彻底失效。</p>
<p>常见处理方法包括：重采样（过采样少数类、欠采样多数类）、类别加权（Class Weighting）、阈值调整（Threshold Tuning）和使用更合适的指标（如 Precision、Recall、PR-AUC）。例如在信用卡欺诈场景中，正类只占 0.1%，此时“全判正常”会有 99.9% Accuracy，但业务价值几乎为 0。</p>
<div class="blog_h2"><span class="graybg">超参数</span></div>
<p>超参数（Hyperparameters）是训练开始前由人或外部搜索过程设定的配置变量。它们与模型参数（Parameters）不同：模型参数如线性回归的权重、神经网络的矩阵和偏置，是通过训练数据学出来的；超参数则决定<span style="background-color: #c0c0c0;">模型该以什么结构、什么训练节奏、什么正则化强度去学习</span>。学习率、batch size、树深、dropout、LoRA rank 都属于超参数，而不是训练过程中直接被梯度更新出来的参数。</p>
<div class="blog_h3"><span class="graybg">什么是超参数</span></div>
<p>从作用层面看，超参数大致分成三类。第一类决定模型结构，例如树的最大深度、神经网络层数、隐藏维度、注意力头数；第二类决定优化过程，例如学习率、batch size、训练轮数、warmup 步数；第三类决定复杂度控制，例如正则化强度、dropout、weight decay、早停耐心值。它们共同定义了“模型允许学成什么样、以及训练过程会沿哪条轨迹逼近这个结果”。</p>
<p>因此，超参数优化并不是在调模型已经学到的权重，而是在搜索<span style="background-color: #c0c0c0;">哪一套训练配置更可能在验证集上泛化得最好</span>。这也是为什么超参数搜索天然依赖验证集，而不能依赖测试集。</p>
<div class="blog_h3"><span class="graybg">通用超参数</span></div>
<p>有些超参数跨很多模型家族都反复出现，它们更像训练流程级控制杆，而不是某个算法私有旋钮。最常见的一组可概括如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">超参数</td>
<td style="text-align: center;">主要控制什么</td>
<td style="text-align: center;">常见影响</td>
</tr>
</thead>
<tbody>
<tr>
<td>学习率（Learning Rate）</td>
<td>每步更新幅度</td>
<td>过大易震荡或发散，过小则收敛过慢或停在高误差区</td>
</tr>
<tr>
<td>batch size</td>
<td>每次梯度估计使用多少样本</td>
<td>影响吞吐、显存占用、梯度噪声和有效学习率范围</td>
</tr>
<tr>
<td>训练轮数 / 训练步数（Epochs / Steps）</td>
<td>训练总时长</td>
<td>过少易欠拟合，过多则更易过拟合</td>
</tr>
<tr>
<td>正则化强度（Regularization Strength）</td>
<td>复杂度惩罚有多强</td>
<td>过强会欠拟合，过弱则更易记忆训练集细节</td>
</tr>
<tr>
<td>weight decay</td>
<td>参数收缩强度</td>
<td>常用于控制神经网络权重规模与泛化</td>
</tr>
<tr>
<td>dropout</td>
<td>随机屏蔽单元的比例</td>
<td>抑制共适应，但过强会削弱表示能力</td>
</tr>
<tr>
<td>学习率调度（Scheduler）</td>
<td>训练过程中学习率如何变化</td>
<td>直接影响早期稳定性与后期收敛质量</td>
</tr>
<tr>
<td>warmup</td>
<td>前期学习率爬升过程</td>
<td>对 Transformer 和大 batch 训练尤为重要</td>
</tr>
<tr>
<td>早停耐心值（Early Stopping Patience）</td>
<td>验证集多久不提升才停止</td>
<td>影响训练预算与过拟合控制</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">模型特定超参数</span></div>
<p>另一类超参数只在特定模型家族里出现。它们往往直接对应某个算法的结构假设，因此不能简单迁移到别的模型上。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型家族</td>
<td style="text-align: center;">典型超参数</td>
<td style="text-align: center;">控制什么</td>
</tr>
</thead>
<tbody>
<tr>
<td>KNN</td>
<td><span displaypfx="inline-" class="mathjax-container">\(k\)</span>、距离度量</td>
<td>邻域大小与“相似”的定义</td>
</tr>
<tr>
<td>SVM</td>
<td><span displaypfx="inline-" class="mathjax-container">\(C\)</span>、kernel、<span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span></td>
<td>间隔惩罚与核函数形状</td>
</tr>
<tr>
<td>决策树 / 随机森林</td>
<td>max depth、min samples leaf、树数</td>
<td>树的复杂度与集成规模</td>
</tr>
<tr>
<td>Boosting / XGBoost / LightGBM</td>
<td>learning rate、树数、max depth、采样比例</td>
<td>弱学习器叠加节奏与复杂度</td>
</tr>
<tr>
<td>CNN</td>
<td>卷积核大小、通道数、stride、pooling 配置</td>
<td>局部感受野与空间降采样方式</td>
</tr>
<tr>
<td>RNN / LSTM</td>
<td>隐藏维度、层数、截断长度</td>
<td>时序记忆容量与反向传播范围</td>
</tr>
<tr>
<td>Transformer</td>
<td>层数、隐藏维度、头数、最大上下文长度</td>
<td>表示容量、并行结构与长程建模能力</td>
</tr>
<tr>
<td>PEFT / LoRA</td>
<td>rank、alpha、target modules、adapter dropout</td>
<td>低秩适配容量与写入位置</td>
</tr>
</tbody>
</table>
<p>这也是为什么“超参数”不能被理解成一张固定清单。不同模型真正敏感的旋钮并不相同。对树模型，max depth 和叶节点约束常是核心；对 Transformer，学习率、warmup、weight decay、batch 与上下文长度往往更关键；对 LoRA，rank 与挂载模块会直接决定可写入容量。</p>
<div class="blog_h3"><span class="graybg">超参数搜索</span></div>
<p>超参数搜索（Hyperparameter Search）指用验证集表现，在若干候选配置中选择更优组合。它本质上不是训练参数，而是在搜索<span style="background-color: #c0c0c0;">哪套训练配方更值得被固定下来</span>。搜索空间越大，找到更优组合的机会通常越高，但实验成本、验证集过拟合风险和复现难度也会同步上升。</p>
<div class="blog_h4"><span class="graybg">贪婪串行登山</span></div>
<p>贪婪串行登山（Greedy Sequential Hill Climbing）是一种非常实用的超参数搜索策略。它的核心规则是：<span style="background-color: #c0c0c0;">每次只调整一个超参数，在当前其余超参数固定不变的条件下，选择验证集上更优的方向走一步；确定后先固定该值，再去调下一个超参数</span>。在离散候选集上，它可以看作一种坐标式局部搜索。</p>
<p>例如先固定 dropout 和 batch size，只比较若干学习率；一旦找到当前最优学习率，就暂时锁定它，再去比较 dropout；然后再固定前两者去比较 batch size。这样做的优点是实验次数通常近似线性增长，适合“训练一次代价不低、超参数数量又不算很多”的场景。</p>
<div class="blog_h4"><span class="graybg">棘轮式锁定</span></div>
<p>贪婪串行登山常伴随一种棘轮式锁定（Ratchet-style Fixing）：某一轮一旦选定一个更优取值，就先不回头重开这个维度。这样做能显著缩小后续搜索空间，但代价也很明确：较早做出的局部最优决策，会限制后面组合空间的探索。</p>
<p>它最容易出问题的地方，是参数交互（Hyperparameter Interaction）。若两个超参数彼此强相关，例如学习率和 batch size、学习率和 warmup、LoRA rank 和 target modules，那么“在当前默认值下看起来更优”的选择，未必能和后续维度组成真正最优的整体组合。棘轮式锁定会把这类交互提前屏蔽掉。</p>
<div class="blog_h4"><span class="graybg">优点与局限</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>优点</td>
<td>简单、可解释、实验次数少，适合作为快速锁定大方向的工程基线</td>
</tr>
<tr>
<td>局限 1</td>
<td>容易停在局部最优，因为它不会接受“短期下降、长期更优”的探索路径</td>
</tr>
<tr>
<td>局限 2</td>
<td>默认把超参数近似看成可分离维度，但现实中经常存在强交互</td>
</tr>
<tr>
<td>局限 3</td>
<td>若反复依赖同一验证集做很多轮决策，更容易把验证集偶然性误判成真实提升</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">与其他搜索策略对比</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">策略</td>
<td style="text-align: center;">实验成本</td>
<td style="text-align: center;">能否捕捉参数交互</td>
<td style="text-align: center;">典型特点</td>
</tr>
</thead>
<tbody>
<tr>
<td>贪婪串行登山</td>
<td>较低</td>
<td>较弱</td>
<td>工程上快速、便宜、可解释，但更局部</td>
</tr>
<tr>
<td>网格搜索（Grid Search）</td>
<td>高，常随维度指数增长</td>
<td>强</td>
<td>穷举规则清楚，但高维时代很快失去性价比</td>
</tr>
<tr>
<td>随机搜索（Random Search）</td>
<td>可控</td>
<td>中等</td>
<td>在高维空间常比网格搜索更高效，是强基线</td>
</tr>
<tr>
<td>贝叶斯优化（Bayesian Optimization）</td>
<td>中等到较高</td>
<td>较强</td>
<td>利用历史试验结果自适应建议下一个点，适合昂贵实验</td>
</tr>
</tbody>
</table>
<p>因此，何时使用哪种策略，取决于训练代价与搜索空间形状。若一次训练就要数十分钟甚至数小时，且可调超参数并不多，贪婪串行登山往往已经足够作为第一轮工程方案；若参数交互明显、预算允许，随机搜索或贝叶斯优化通常更稳。无论使用哪一种方法，最关键的前提都不变：<span style="background-color: #c0c0c0;">搜索必须由验证集驱动，而测试集必须保持未参与决策</span>。</p>
<div class="blog_h2"><span class="graybg">模型评估</span></div>
<p>模型评估（Model Evaluation）回答的不是“模型能不能在训练集上做对”，而是“模型在新数据上是否可靠，以及错误代价如何”。不同任务对应的指标重点不同：分类关心类别区分，回归关心数值偏差，排序关心相对顺序。</p>
<div class="blog_h3"><span class="graybg">交叉验证</span></div>
<p>交叉验证（Cross Validation）在数据较少时特别重要。最常见的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 折交叉验证把数据分成 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 份：每次用其中 1 份做验证、其余 <span displaypfx="inline-" class="mathjax-container">\(K-1\)</span> 份训练，循环 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 次，最后对 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个验证结果取平均。</p>
<p>它的作用像“轮流把不同一份数据拿出来当模拟考试卷”，从而降低一次随机划分带来的偶然性。对小数据集而言，单次划分可能刚好“运气好或坏”；交叉验证则给出更稳定的泛化估计。</p>
<div class="blog_h3"><span class="graybg">校准</span></div>
<p>校准（Calibration）讨论的是：<span style="background-color: #c0c0c0;">模型给出的概率值，是否真的能当概率解释</span>。分类模型不只输出“判成哪一类”，还常输出一个置信分数，例如 <span displaypfx="inline-" class="mathjax-container">\(0.9\)</span>。若一个模型在所有“预测概率约为 0.9”的样本子集上，最终真的有约 90% 预测正确，那么它就是校准良好的；若它经常把只有 60% 把握的样本说成 90%，就属于过度自信（Overconfident）。</p>
<p>二分类中，若模型输出正类概率 <span displaypfx="inline-" class="mathjax-container">\(\hat p(x)\in[0,1]\)</span>，理想校准条件可写成：</p>
<span displaypfx="" class="mathjax-container">\[P(Y=1\mid \hat p(X)=p)=p\]</span>
<p>这里左边表示：在所有预测概率等于 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 的样本中，真实为正类的条件概率；右边的 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是模型自己报出的概率。两者相等时，概率输出就与真实频率一致。多分类场景下，常把模型最大类别概率当作置信度，并检验“报 80% 置信度的样本，是否真的大约 80% 正确”。</p>
<p>校准与准确率不是同一件事。一个模型可以分类很准，但概率不可靠；也可以概率尺度较准，但分类边界并不最优。前者常见于深层神经网络：argmax 分类结果不错，但 softmax 概率偏尖，置信度系统性偏高。涉及风险控制、医学筛查、自动驾驶、检索重排、多阶段决策时，概率是否可信往往和“分对多少”同样重要，因为阈值决策、人工复核和代价加权都依赖这个概率尺度。</p>
<p>校准的可视化工具通常是可靠性图（Reliability Diagram）。做法是把预测置信度分成若干区间，例如 <span displaypfx="inline-" class="mathjax-container">\([0.0,0.1),[0.1,0.2),\dots\)</span>，然后对每个区间分别计算平均置信度与真实准确率。若图上的点接近对角线 <span displaypfx="inline-" class="mathjax-container">\(y=x\)</span>，说明校准较好；若点普遍落在对角线下方，说明模型报得比实际更自信；若点在对角线上方，则说明模型偏保守。</p>
<p>常用数值指标是期望校准误差（Expected Calibration Error, ECE）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ECE}=\sum_{m=1}^{M}\frac{|B_m|}{n}\,\big|\mathrm{acc}(B_m)-\mathrm{conf}(B_m)\big|\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 是置信度分箱数， <span displaypfx="inline-" class="mathjax-container">\(B_m\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 个置信度区间中的样本集合， <span displaypfx="inline-" class="mathjax-container">\(|B_m|\)</span> 是该区间样本数， <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是总样本数， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{acc}(B_m)\)</span> 是该区间的实际准确率， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{conf}(B_m)\)</span> 是该区间的平均预测置信度。ECE 的含义很直接：把每个置信区间里“说得多准”和“实际多准”的差值取绝对值，再按样本占比加权平均。ECE 越小，表示整体校准越好。</p>
<p>另一类常见指标是 Brier Score。对二分类，它定义为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Brier}=\frac{1}{n}\sum_{i=1}^{n}(\hat p_i-y_i)^2\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\hat p_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的预测正类概率， <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{0,1\}\)</span> 是真实标签。它既惩罚分类错误，也惩罚概率刻度不准，因此兼顾区分能力与概率质量。与单纯 Accuracy 不同，Brier Score 会区分“错得有多离谱”：把一个负样本报成 <span displaypfx="inline-" class="mathjax-container">\(0.51\)</span> 和报成 <span displaypfx="inline-" class="mathjax-container">\(0.99\)</span>，代价并不相同。</p>
<p>工程上最常见的后处理方法是温度缩放（Temperature Scaling）。设原始 logits 为 <span displaypfx="inline-" class="mathjax-container">\(z_i\)</span>，则缩放后的 softmax 概率写成：</p>
<span displaypfx="" class="mathjax-container">\[p_i=\frac{\exp(z_i/T)}{\sum_j \exp(z_j/T)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T&gt;0\)</span> 是温度参数。 <span displaypfx="inline-" class="mathjax-container">\(T&gt;1\)</span> 会把分布拉平，降低过度自信； <span displaypfx="inline-" class="mathjax-container">\(T&lt;1\)</span> 会把分布压尖，提高置信度。温度参数通常在验证集上通过最小化负对数似然（Negative Log-Likelihood, NLL）来拟合，然后固定用于测试或部署阶段。它不会改变类别排序，因此常能在几乎不影响 Accuracy 的前提下改善概率校准。</p>
<div class="blog_h3"><span class="graybg">分类指标</span></div>
<div class="blog_h4"><span class="graybg">混淆矩阵（Confusion Matrix）</span></div>
<p>分类指标通常从混淆矩阵（Confusion Matrix）出发。设正类预测结果统计为真阳性 <span displaypfx="inline-" class="mathjax-container">\(TP\)</span>、假阳性 <span displaypfx="inline-" class="mathjax-container">\(FP\)</span>、真阴性 <span displaypfx="inline-" class="mathjax-container">\(TN\)</span>、假阴性 <span displaypfx="inline-" class="mathjax-container">\(FN\)</span>。不同指标本质上是在回答不同问题：是看“总共判对多少”，还是看“判成正类时有多准”，还是看“真实正类抓到了多少”。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/confusion-matrix.png"><img class="alignnone size-full wp-image-41785" src="https://blog.gmem.cc/wp-content/uploads/2026/03/confusion-matrix.png" alt="confusion-matrix" width="1024" height="776" /></a></p>
<div class="blog_h4"><span class="graybg">Accuracy</span></div>
<p>准确率（Accuracy）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Accuracy}=\frac{TP+TN}{TP+TN+FP+FN}\]</span>
<p>它衡量“总体上判对了多少比例”，适合类别相对平衡、不同错误代价接近的场景。但在类别极不平衡时会误导：例如癌症筛查里，99% 都是阴性时，模型全判阴性也可能有 99% Accuracy，却毫无检测价值。</p>
<div class="blog_h4"><span class="graybg">Precision</span></div>
<p>精确率（Precision）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Precision}=\frac{TP}{TP+FP}\]</span>
<p>它回答的是：“所有被模型判成正类的样本里，有多少真的为正。”当误报成本很高时，Precision 特别重要。例：垃圾邮件过滤里，如果把正常邮件误判成垃圾邮件代价很高，就要关心 Precision。</p>
<div class="blog_h4"><span class="graybg">Recall</span></div>
<p>召回率（Recall）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Recall}=\frac{TP}{TP+FN}\]</span>
<p>它回答的是：“所有真实正类里，有多少被模型找出来了。”当漏报成本很高时，Recall 更关键。例：医学筛查里漏掉患者可能比多做一次复检更危险，因此 Recall 往往比 Precision 更重要。</p>
<div class="blog_h4"><span class="graybg">F1 Score</span></div>
<p>F1 值（F1 Score）是 Precision 与 Recall 的调和平均：</p>
<span displaypfx="" class="mathjax-container">\[F_1=\frac{2\cdot \mathrm{Precision}\cdot \mathrm{Recall}}{\mathrm{Precision}+\mathrm{Recall}}\]</span>
<p>之所以用调和平均而不是普通平均，是因为它会惩罚“一高一低”的不平衡情况。若一个模型 Precision 极高但 Recall 很低，它并不能拿到高 F1。F1 适合正负样本不平衡、且希望兼顾漏报与误报的场景。</p>
<div class="blog_h4"><span class="graybg">AUC-ROC</span></div>
<p>AUC-ROC 衡量模型在不同分类阈值下区分正负样本的整体能力。ROC 曲线横轴是假阳性率（False Positive Rate），纵轴是真阳性率（True Positive Rate）。AUC 是曲线下面积，范围在 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span>；越接近 1，说明模型越能把正样本排在负样本前面。</p>
<p>它不依赖某一个固定阈值，因此适合比较“排序能力”。但在极端不平衡数据上，PR 曲线（Precision-Recall Curve）常更敏感，因为 ROC 容易被大量真阴性“冲淡”。</p>
<div class="blog_h3"><span class="graybg">回归指标</span></div>
<p>回归指标（Regression Metrics）衡量预测值与真实值之间的数值偏差。它们关注的不是“判对类别”，而是“偏差有多大、对大误差是否敏感、模型解释了多少波动”。以房价预测为例，预测 300 万和真实 320 万之间的差距，就是典型回归误差。</p>
<div class="blog_h4"><span class="graybg">MAE</span></div>
<p>平均绝对误差（Mean Absolute Error, MAE）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{MAE}=\frac{1}{N}\sum_{i=1}^{N}|\hat y_i-y_i|\]</span>
<p>它直接度量“平均差了多少个原始单位”，解释最直观。若房价单位是万元，MAE=12 就表示平均误差约 12 万元。由于使用绝对值，MAE 对离群点没有 MSE 那么敏感。</p>
<div class="blog_h4"><span class="graybg">MSE</span></div>
<p>均方误差（Mean Squared Error, MSE）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{MSE}=\frac{1}{N}\sum_{i=1}^{N}(\hat y_i-y_i)^2\]</span>
<p>平方会放大大误差，因此 MSE 对离群点更敏感。它常用于你希望“大错要被重罚”的场景。高斯噪声假设下，最小化 MSE 还对应最大似然估计，因此它不仅是工程指标，也是概率建模结果。</p>
<div class="blog_h4"><span class="graybg">RMSE</span></div>
<p>均方根误差（Root Mean Squared Error, RMSE）是</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{RMSE}=\sqrt{\mathrm{MSE}}\]</span>
<p>它保留了 MSE 对大误差更敏感的性质，同时把单位拉回原始量纲，因此更易解释。若房价 RMSE 为 20 万元，可以直接理解为“典型误差量级约 20 万元”。</p>
<div class="blog_h4"><span class="graybg">R²</span></div>
<p><span displaypfx="" class="mathjax-container">\[R^2\]</span>（决定系数，Coefficient of Determination）回答的是：<span style="background-color: #c0c0c0;">相比于最朴素的瞎猜基线，你的回归模型到底把预测提升了多少</span>。要理解它，只需要在脑子里放两条线：一条是“什么都不知道时只能猜平均值”的水平线，另一条是模型给出的预测曲线。</p>
<p>先看最朴素的基线。假设你要预测一批房子的价格，但你手里没有面积、地段、楼龄这些特征，别人却逼着你给出预测。此时最不容易挨打的办法，不是胡乱报一个数字，而是对所有房子都猜样本平均价 <span displaypfx="inline-" class="mathjax-container">\(\bar y\)</span>。在散点图上，这对应一条横向的水平线。</p>
<p>真实房价 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 会散落在这条平均线的上下。每个点到平均线的垂直距离 <span displaypfx="inline-" class="mathjax-container">\(y_i-\bar y\)</span>，就是“瞎蒙平均值”时犯下的误差。把这些误差平方后全部加起来，就得到</p>
<span displaypfx="" class="mathjax-container">\[\sum_i(y_i-\bar y)^2\]</span>
<p>这就是公式里的分母。它衡量的不是模型误差，而是这批数据本身原来就有多分散、多混乱。也可以把它理解为目标变量的<span style="background-color: #c0c0c0;">总波动</span>、总混沌程度，或者说“在完全不用特征时，世界原本有多少东西解释不了”。</p>
<p>现在再看你的模型。你训练出一个回归模型，它根据输入特征给出预测 <span displaypfx="inline-" class="mathjax-container">\(\hat y_i\)</span>。在图上，这不再是那条死板的水平线，而是一条试图穿过散点云中心的预测曲线。模型当然不可能完美，所以每个真实值 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 与预测值 <span displaypfx="inline-" class="mathjax-container">\(\hat y_i\)</span> 之间仍会有垂直误差，这个误差就是残差（Residual）。</p>
<p>把这些模型仍然没解释掉的误差平方后加起来，就得到</p>
<span displaypfx="" class="mathjax-container">\[\sum_i(\hat y_i-y_i)^2\]</span>
<p>这就是公式里的分子，也叫残差平方和（Residual Sum of Squares, RSS）。它代表模型已经尽力之后，世界上<span style="background-color: #c0c0c0;">依然残存的混沌</span>。分子越小，说明模型越贴近真实数据；分子越大，说明模型虽然复杂，但其实没把问题解释清楚。</p>
<p>于是</p>
<span displaypfx="" class="mathjax-container">\[R^2=1-\frac{\sum_i(\hat y_i-y_i)^2}{\sum_i(y_i-\bar y)^2}\]</span>
<p>这条式子就可以直接读成一句大白话：先看模型还剩下多少解释不了的波动，再除以最开始总共有多少波动，得到“模型搞不定的比例”；最后用 1 减掉它，剩下的就是<span style="background-color: #c0c0c0;">模型成功解释掉的波动比例</span>。</p>
<p>因此，若 <span displaypfx="inline-" class="mathjax-container">\(R^2=0.8\)</span>，意思不是“正确率 80%”，而是目标变量原本有 100 份波动，模型大约解释掉了其中 80 份，只剩 20 份还没解释；若 <span displaypfx="inline-" class="mathjax-container">\(R^2=0\)</span>，说明你的模型折腾半天，效果和“永远预测平均值”完全一样；若 <span displaypfx="inline-" class="mathjax-container">\(R^2&lt;0\)</span>，则表示模型比这个最朴素基线还差，常见原因是模型设错了、特征没信息，或实现上有 bug。</p>
<p>从老板视角看， <span displaypfx="inline-" class="mathjax-container">\(R^2\)</span> 的灵魂拷问其实只有一句：<span style="background-color: #c0c0c0;">相比直接拿平均值糊弄事，你这个复杂回归模型到底多解释了多少真实波动</span>。这也是为什么 <span displaypfx="inline-" class="mathjax-container">\(R^2\)</span> 特别适合回答“模型有没有真正利用特征学到东西”，但它不能替代 MAE、RMSE——因为 <span displaypfx="inline-" class="mathjax-container">\(R^2\)</span> 讲的是解释比例，而不是误差到底有多少个原始单位。</p>
<div class="blog_h2"><span class="graybg">优化算法</span></div>
<p>优化算法（Optimization Algorithms）解决的问题非常朴素：<span style="background-color: #c0c0c0;">模型参数该往哪个方向改，才能让损失函数持续下降</span>。只要训练目标能写成“最小化某个损失函数”，背后就需要一套更新参数的规则。线性回归、逻辑回归、神经网络、大语言模型训练，本质上都绕不开这个问题。</p>
<p>这里要先分清两件事。优化（Optimization）关心的是“训练损失能不能降下来”；泛化（Generalization）关心的是“模型在新样本上好不好”。一个优化器可能把训练集拟合得很好，但泛化依然一般；反过来，一个优化器如果连训练损失都压不下去，模型通常也谈不上有效。因此优化算法决定的是<span style="background-color: #c0c0c0;">你如何走向一个解</span>，而不是直接保证这个解一定最好。</p>
<div class="blog_h3"><span class="graybg">梯度下降</span></div>
<p>梯度下降（Gradient Descent）是最核心的一阶优化思想。设损失函数为 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span>，参数为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，则梯度 <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta L(\theta)\)</span> 给出“损失上升最快的方向”。既然梯度指向上坡，那么要让损失下降，就应沿着它的反方向更新参数：</p>
<span displaypfx="" class="mathjax-container">\[\theta_{t+1}=\theta_t-\eta\,\nabla_\theta L(\theta_t)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步的参数， <span displaypfx="inline-" class="mathjax-container">\(\eta\)</span> 是学习率（Learning Rate），决定每一步走多大； <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta L(\theta_t)\)</span> 是当前位置的斜率信息。学习率太大，容易一步跨过谷底甚至震荡发散；学习率太小，又会下降得极慢。它像蒙着雾下山：梯度告诉你脚下哪边更陡，学习率决定你每次迈多大步。</p>
<p>为什么很多模型不直接“解公式”，而要反复迭代？因为在深度学习里，参数维度极高，损失面又往往非凸（Non-convex），通常没有漂亮的闭式解（Closed-form Solution）。这时最现实的办法不是一次算出全局最优，而是利用局部斜率，一步一步把损失往下压。</p>
<p>当总损失是逐样本损失的平均时，梯度下降还可以写得更具体：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\ell(\theta;x_i)\]</span>
<span displaypfx="" class="mathjax-container">\[\nabla_\theta L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\nabla_\theta \ell(\theta;x_i)\]</span>
<p>这也回答了“为什么可以批量喂输入”：梯度对求和是线性的，<span style="background-color: #c0c0c0;">整体梯度等于逐样本梯度的平均</span>，所以可以并行算每个样本的梯度，再求平均后统一更新参数。</p>
<p>满足这种“逐样本求和/求平均”结构的损失函数其实非常多。典型例子包括：线性回归里的均方误差（MSE）、平均绝对误差（MAE），二分类里的 logistic loss / binary cross-entropy，多分类里的交叉熵（Cross-Entropy），语言模型训练里的负对数似然（Negative Log-Likelihood, NLL）。它们都可以写成“每个样本先各自算一份损失，再在整个数据集上取平均”的形式，因此天然适合 mini-batch 训练。</p>
<p>但并不是所有目标都能严格写成这种完全独立的逐样本平均。若损失显式依赖<span style="background-color: #c0c0c0;">样本之间的相对关系</span>，情况就会复杂一些。例如排序学习里的 pairwise/listwise loss、度量学习中的 triplet loss，以及对比学习里的 InfoNCE，都会让一个样本的损失依赖同一个 batch 中的其他样本。此时虽然仍然可以按 batch 计算梯度，但它已经不是“每个样本各算各的、最后简单平均”那么干净的分解了。</p>
<p>工程上还常见另一种情况：目标函数可以写成“逐样本平均损失 + 正则项（Regularization）”。例如</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\ell(\theta;x_i)+\lambda\Omega(\theta)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)\)</span> 可以是 <span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_2^2\)</span> 这类参数惩罚项。前半部分仍然按样本分解，后半部分则是直接作用于参数本身，而不对应某一个单独样本。很多现代训练目标，本质上都是这两部分的组合。</p>
<p>几个常用训练量需要先分清：</p>
<ul>
<li>批大小（Batch Size）：一次更新使用多少个样本。</li>
<li>步（Step / Iteration）：一次参数更新。</li>
<li>轮（Epoch）：完整遍历一次训练集。</li>
</ul>
<p>若训练集大小为 <span displaypfx="inline-" class="mathjax-container">\(N\)</span>，batch size 为 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>，则每个 epoch 的步数约为 <span displaypfx="inline-" class="mathjax-container">\(\lceil N/B\rceil\)</span>。工程里经常会说“训练了多少 step”，因为真正发生参数变化的是 step，而不是 epoch 这个更粗的计数单位。</p>
<div class="blog_h4"><span class="graybg">批量梯度下降（BGD）</span></div>
<p>批量梯度下降（Batch Gradient Descent, BGD）每次都用<span style="background-color: #c0c0c0;">整个训练集</span>来计算一次精确梯度，再更新参数。这里的 batch 指的不是现代深度学习里常说的一个 mini-batch，而是“整批训练数据”。它的优点是方向最稳定、梯度方差最小；缺点是每一步都很贵，数据一大就几乎不可用。它更适合小数据集、凸优化问题，或教材里说明“梯度下降原理”时使用。</p>
<div class="blog_h4"><span class="graybg">随机梯度下降（SGD）</span></div>
<p>随机梯度下降（Stochastic Gradient Descent, SGD）每次只用一个样本的梯度做更新。它的方向噪声很大，看起来像“跌跌撞撞地下山”，但每一步极便宜、更新极频繁，因此在数据流式到来或样本极多时很有价值。</p>
<ul>
<li>优点：更新快、内存开销小、天然适合在线学习（Online Learning）与流式数据（Streaming Data）。</li>
<li>优点：梯度噪声有时反而是好事，更容易离开鞍点（Saddle Point）和较差的局部极小。</li>
<li>缺点：轨迹抖动大，若学习率控制不好，训练容易不稳定。</li>
</ul>
<p>它像店长根据每一位新顾客的反馈立刻调价：反应很快，但也容易被个别顾客带偏。</p>
<p>适用场景：当你需要<span style="background-color: #c0c0c0;">用最新样本尽快产生参数更新</span>时，SGD（batch size=1）是最直接的选择，典型包括：</p>
<ul>
<li>在线学习（Online Learning）/流式数据（Streaming Data）：样本持续到来，要求增量更新，而不是离线反复扫全量数据。</li>
<li>分布漂移（Distribution Shift）与非平稳（Non-stationary）环境：用户偏好、市场、策略对抗等持续变化，需要快速跟踪新分布。</li>
<li>低延迟更新需求：例如广告/推荐/风控的在线校准，需要“见到一条新反馈就更新一点”。</li>
<li>资源受限或样本极大：内存无法容纳大 batch 或无法频繁计算全量梯度时，用单样本更新换取更低的每步计算与存储成本。</li>
</ul>
<p>在现代深度学习里，若使用 GPU/TPU 训练，大多数时候会用小批量梯度下降来兼顾吞吐与稳定性；纯 SGD 更常出现在在线/增量训练与部分强化学习（Reinforcement Learning, RL）设置中。</p>
<div class="blog_h4"><span class="graybg">小批量梯度下降（Mini-batch SGD）</span></div>
<p>小批量梯度下降（Mini-batch SGD）是在 BGD 与 SGD 之间折中：每次用一个 batch 的平均梯度更新。它既能利用 GPU 的并行算力，又保留一定梯度噪声，因此现代深度学习几乎都在这个范式下训练。</p>
<p>batch 太小，梯度估计噪声会很大；batch 太大，虽然每步更稳定，但显存压力更高、更新频率更低，有时还会让优化和泛化都变钝。因此 batch size 不是“越大越好”，而是吞吐、稳定性、显存和泛化之间的折中。</p>
<div class="blog_h3"><span class="graybg">动量法（Momentum）</span></div>
<p>单纯的梯度下降在“峡谷形”损失面里很容易左右来回震荡：沿陡峭方向上下摆动，沿真正有用的谷底方向前进却很慢。动量法（Momentum）的直觉是：不要只看当前这一脚的斜率，而要把过去几步的方向累积成一种“惯性”。</p>
<span displaypfx="" class="mathjax-container">\[v_{t+1}=\beta v_t+(1-\beta)g_t,\quad \theta_{t+1}=\theta_t-\eta v_{t+1}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 是当前梯度， <span displaypfx="inline-" class="mathjax-container">\(v_t\)</span> 是累计出来的速度， <span displaypfx="inline-" class="mathjax-container">\(\beta\in[0,1)\)</span> 控制“记住过去多少信息”。 <span displaypfx="inline-" class="mathjax-container">\(\beta\)</span> 越大，方向越平滑、惯性越强；越小，则越接近普通梯度下降。类比来看，它像推一个有重量的小球下山：不会因为脚下的微小凸凹就频繁改道，而会沿长期更一致的下降方向滚动。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/momentum.jpg"><img class="alignnone size-full wp-image-41017" src="https://blog.gmem.cc/wp-content/uploads/2026/03/momentum.jpg" alt="momentum" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">AdaGrad / RMSProp</span></div>
<p>AdaGrad 与 RMSProp 属于自适应学习率（Adaptive Learning Rate）方法。它们的核心思想是：<span style="background-color: #c0c0c0;">不同参数的梯度尺度不同，不应该所有维度都用同一个固定步长</span>。历史上梯度很大的维度，后续步子要缩小；梯度稀疏或很小的维度，则可以走得更积极。</p>
<p>AdaGrad 累积历史平方梯度：</p>
<span displaypfx="" class="mathjax-container">\[s_{t+1}=s_t+g_t\odot g_t,\quad \theta_{t+1}=\theta_t-\eta\,\frac{g_t}{\sqrt{s_{t+1}}+\epsilon}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(g_t\odot g_t\)</span> 表示逐元素平方， <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 是数值稳定项。这样一来，历史上梯度一直很大的维度会被自动缩小学习率。AdaGrad 在稀疏特征（Sparse Features）任务里很有效，例如早期的文本与推荐场景；但它的问题是 <span displaypfx="inline-" class="mathjax-container">\(s_t\)</span> 只增不减，训练到后期学习率可能衰减得过头，参数几乎不再动。</p>
<p>RMSProp 用指数滑动平均替代“无限累积”，缓解这个问题：</p>
<span displaypfx="" class="mathjax-container">\[s_{t+1}=\rho s_t+(1-\rho)(g_t\odot g_t),\quad \theta_{t+1}=\theta_t-\eta\,\frac{g_t}{\sqrt{s_{t+1}}+\epsilon}\]</span>
<p>这样历史信息会逐步“遗忘”，使学习率缩放更关注近期梯度尺度。可以把 AdaGrad 理解成“终身记账”，而 RMSProp 更像“滚动记账”。</p>
<div class="blog_h3"><span class="graybg">Adam</span></div>
<p>Adam（Adaptive Moment Estimation）把动量法（Momentum）的一阶矩估计与 RMSProp 的二阶矩估计结合起来，因此它同时解决优化中的两个核心痛点：<span style="background-color: #c0c0c0;">方向感（往哪走）</span>与<span style="background-color: #c0c0c0;">节奏感（每步走多大）</span>。直觉上，可以把它理解为“带惯性的方向盘 + 自动变速箱”：方向由平均梯度决定，步长由梯度的尺度自动缩放。</p>
<div class="blog_h4"><span class="graybg">一阶矩：方向感（Momentum）</span></div>
<span displaypfx="" class="mathjax-container">\[m_{t+1}=\beta_1 m_t+(1-\beta_1)g_t\]</span>
<ul>
<li><span style="background-color: #c0c0c0;">索引约定</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 次更新先在 <span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span> 处计算 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span>，再把它并入动量得到 <span displaypfx="inline-" class="mathjax-container">\(m_{t+1}\)</span>；下标 <span displaypfx="inline-" class="mathjax-container">\(t+1\)</span> 表示“更新后状态”，不是未来信息。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(m_{t+1}\)</span>：并入 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 后的一阶矩（动量）估计。</li>
<li>右边第一项 <span displaypfx="inline-" class="mathjax-container">\(\beta_1 m_t\)</span>：把历史动量按系数 <span displaypfx="inline-" class="mathjax-container">\(\beta_1\)</span> 保留下来，表示“历史方向对新方向的贡献”。</li>
<li>右边第二项 <span displaypfx="inline-" class="mathjax-container">\((1-\beta_1)g_t\)</span>：把当前梯度按系数 <span displaypfx="inline-" class="mathjax-container">\(1-\beta_1\)</span> 注入动量，表示“当前观测对新方向的贡献”。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(g_t\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步的梯度，通常由当前 mini-batch 估计得到（<span displaypfx="inline-" class="mathjax-container">\(g_t=\nabla_\theta L(\theta_t)\)</span>）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(m_t\)</span>：一阶矩估计的滑动平均（动量项），可理解为“平均梯度方向”。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\beta_1\in[0,1)\)</span>：动量衰减系数，越大表示记忆越长、方向越平滑。</li>
</ul>
<p>两项系数满足 <span displaypfx="inline-" class="mathjax-container">\(\beta_1+(1-\beta_1)=1\)</span>，因此这是指数滑动平均（Exponential Moving Average, EMA，越近发生的事情，参考价值越大；越久远的事情，参考价值越小）。当 <span displaypfx="inline-" class="mathjax-container">\(m_0=0\)</span> 时，可把它展开为：</p>
<span displaypfx="" class="mathjax-container">\[m_{t+1}=(1-\beta_1)\sum_{k=0}^{t}\beta_1^{t-k}g_k\]</span>
<p>上式说明：越久远的梯度 <span displaypfx="inline-" class="mathjax-container">\(g_k\)</span> 权重按 <span displaypfx="inline-" class="mathjax-container">\(\beta_1^{t-k}\)</span> 指数衰减；经验上可把有效记忆长度理解为 <span displaypfx="inline-" class="mathjax-container">\(O\!\left(\frac{1}{1-\beta_1}\right)\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(\beta_1\)</span> 越大，平均窗口越长，方向越平滑但响应越慢；<span displaypfx="inline-" class="mathjax-container">\(\beta_1\)</span> 越小，平均窗口越短，方向更敏捷但更易受噪声影响。</p>
<div class="blog_h4"><span class="graybg">二阶矩：节奏感（RMSProp）</span></div>
<p>RMSProp（Root Mean Square Propagation）用梯度平方的指数滑动平均（Exponential Moving Average, EMA）来估计每个参数维度的“尺度”，再用该尺度对当前梯度做逐元素归一化，从而把更新步伐控制在更稳定的量级。</p>
<span displaypfx="" class="mathjax-container">\[v_{t+1}=\beta_2 v_t+(1-\beta_2)(g_t\odot g_t),\quad \tilde g_t=\frac{g_t}{\sqrt{v_{t+1}}+\epsilon}\]</span>
<ul>
<li><span style="background-color: #c0c0c0;">索引约定</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 次更新先在 <span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span> 处计算 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span>，再用它更新得到 <span displaypfx="inline-" class="mathjax-container">\(v_{t+1}\)</span>；用 <span displaypfx="inline-" class="mathjax-container">\(v_{t+1}\)</span> 缩放 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 表示“用本次更新后的尺度估计做归一化”，不涉及未来信息。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(v_t\)</span>：上一轮更新结束后的二阶原点矩估计（EMA 状态，“更新前的尺度缓存”）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(v_{t+1}\)</span>：梯度的二阶原点矩（Second Raw Moment）估计，逐（梯度）维近似 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\)</span>，刻画“历史梯度大小”的典型尺度。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(g_t\odot g_t\)</span>：当前梯度的逐元素平方（<span displaypfx="inline-" class="mathjax-container">\(\odot\)</span> 为 Hadamard 乘积），只保留幅度信息。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sqrt{v_{t+1}}\)</span>：均方根（Root Mean Square, RMS）尺度；平方使量纲变为 <span displaypfx="inline-" class="mathjax-container">\(g^2\)</span>，开方把量纲还原到 <span displaypfx="inline-" class="mathjax-container">\(g\)</span>，从而可与 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 相除。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\tilde g_t\)</span>：按 RMS 尺度归一化后的梯度；分式表示逐元素相除（每个参数维度各自缩放）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\beta_2\in[0,1)\)</span>：衰减系数，控制尺度估计的记忆长度；越大表示对历史尺度更“长记忆”。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>：数值稳定项，避免分母为 0 或过小。</li>
</ul>
<p>该缩放把“方向”和“尺度”解耦：方向仍由 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 给出；步长由 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{v_{t+1}}\)</span> 自适应调节。若某维长期梯度偏大，则 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{v_{t+1}}\)</span> 变大、该维更新被压小；若某维长期梯度偏小，则分母较小、该维相对步子更大。</p>
<p>若把 RMSProp 作为独立优化器使用，参数更新可写为 <span displaypfx="inline-" class="mathjax-container">\(\theta_{t+1}=\theta_t-\eta\,\tilde g_t\)</span>。在 Adam 中，同样的 RMS 缩放作用在一阶矩估计上：用 <span displaypfx="inline-" class="mathjax-container">\(\hat m_{t+1}\)</span> 替代 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 作为分子，并在下一节通过偏置修正得到 <span displaypfx="inline-" class="mathjax-container">\(\hat v_{t+1}\)</span> 作为分母。</p>
<div class="blog_h4"><span class="graybg">偏置修正：冷启动校正（Bias Correction）</span></div>
<p>因为 <span displaypfx="inline-" class="mathjax-container">\(m_0=v_0=0\)</span>，训练初期的滑动平均会系统性偏小（“没热起来”）。Adam 用偏置修正把它拉回合理尺度：</p>
<span displaypfx="" class="mathjax-container">\[\hat m_{t+1}=\frac{m_{t+1}}{1-\beta_1^{t+1}},\quad \hat v_{t+1}=\frac{v_{t+1}}{1-\beta_2^{t+1}}\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\hat m_{t+1},\hat v_{t+1}\)</span>：偏置修正后的估计，用来抵消初始化为 0 导致的早期偏小。</li>
<li>分母 <span displaypfx="inline-" class="mathjax-container">\(1-\beta^{t+1}\)</span>：校正“冷启动偏小”的缩放因子。由于初始化为 0，指数滑动平均在经历 <span displaypfx="inline-" class="mathjax-container">\(t+1\)</span> 次更新时只积累了约 <span displaypfx="inline-" class="mathjax-container">\(1-\beta^{t+1}\)</span> 的有效权重，因此会偏小；除以它相当于把估计值按同样比例放大回去。</li>
</ul>
<p>更严格地说：若假设 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g_t]=\mu\)</span> 近似稳定，则由递推可得 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[m_{t+1}]=(1-\beta_1^{t+1})\mu\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(\hat m_{t+1}=\frac{m_{t+1}}{1-\beta_1^{t+1}}\)</span> 是把它改成近似无偏估计。同理可得 <span displaypfx="inline-" class="mathjax-container">\(\hat v_{t+1}\)</span> 的修正。</p>
<div class="blog_h4"><span class="graybg">参数更新：方向 ÷ 尺度</span></div>
<span displaypfx="" class="mathjax-container">\[\theta_{t+1}=\theta_t-\eta\,\frac{\hat m_{t+1}}{\sqrt{\hat v_{t+1}}+\epsilon}\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步参数（权重向量/张量）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\eta\)</span>：学习率（全局步长系数）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sqrt{\hat v_{t+1}}\)</span>：逐元素开方；整项 <span displaypfx="inline-" class="mathjax-container">\(\frac{\hat m_{t+1}}{\sqrt{\hat v_{t+1}}+\epsilon}\)</span> 表示逐元素相除。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>：数值稳定项，避免除以 0 或极小数。</li>
</ul>
<p>分子给方向，分母给节奏。一个极端例子能看出为什么 Adam 用 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\)</span> 而不是方差：若某维梯度连续很多步都是 <span displaypfx="inline-" class="mathjax-container">\(g_t=10\)</span>，方差为 0 会导致除法不稳定；但 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\approx 100\)</span> 给出稳定尺度，最终更新量级约为 <span displaypfx="inline-" class="mathjax-container">\(10/\sqrt{100}=1\)</span>（再加 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 保底）。</p>
<p>Adam 往往更易调参、早期收敛更快，因此在 Transformer、扩散模型和很多深度网络里是默认起点。但它并不是无条件最好：有些任务上，最终泛化仍可能不如精心调过的 SGD。实践中，配合权重衰减（Weight Decay）时，通常会使用 AdamW，把权重衰减从梯度自适应缩放里解耦出来，效果更稳。</p>
<div class="blog_h3"><span class="graybg">AdamW</span></div>
<p>AdamW（Adam with Decoupled Weight Decay）把权重衰减（Weight Decay）从 Adam 的自适应梯度缩放里<span style="background-color: #c0c0c0;">解耦</span>出来。原因是：在 Adam 这类自适应方法里，如果你把 L2 正则化写进损失（等价于把 <span displaypfx="inline-" class="mathjax-container">\(\lambda\theta\)</span> 加到梯度里），这个正则项也会被二阶矩 <span displaypfx="inline-" class="mathjax-container">\(\hat v_t\)</span> 的缩放影响，从而导致不同参数维度的“衰减强度”不再可控。</p>
<p>正则化本来应该是一个可控的、与梯度尺度无关的收缩力度；解耦后 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 的含义更稳定。</p>
<p>对比两种写法会更清楚：</p>
<ul>
<li>把 L2 正则化并入目标（常被口语化地称为“weight decay”，但在自适应优化器里并不等价）：先算 <span displaypfx="inline-" class="mathjax-container">\(g_t=\nabla_\theta L(\theta_t)+\lambda\theta_t\)</span>，再把这个 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 送入 Adam 的 <span displaypfx="inline-" class="mathjax-container">\(m_t,v_t\)</span> 与自适应步长。</li>
<li>AdamW（解耦权重衰减）：先算纯数据梯度 <span displaypfx="inline-" class="mathjax-container">\(g_t=\nabla_\theta L(\theta_t)\)</span> 并完成 Adam 的自适应更新，然后单独对参数做一次权重衰减。</li>
</ul>
<p>AdamW 的参数更新可写成：</p>
<span displaypfx="" class="mathjax-container">\[\theta_{t+1}=\theta_t-\eta\,\frac{\hat m_{t+1}}{\sqrt{\hat v_{t+1}}+\epsilon}-\eta\,\lambda\,\theta_t\]</span>
<ul>
<li>前半段 <span displaypfx="inline-" class="mathjax-container">\(-\eta\,\frac{\hat m}{\sqrt{\hat v}+\epsilon}\)</span>：Adam 的自适应梯度更新（由数据损失驱动）。</li>
<li>后半段 <span displaypfx="inline-" class="mathjax-container">\(-\eta\,\lambda\,\theta_t\)</span>：解耦的 weight decay（参数按比例收缩），其中 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是衰减强度。</li>
</ul>
<p>其中第一项是 Adam 的自适应更新，第二项是独立的权重衰减（等价于每步把权重按比例拉向 0）。这种分离让 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 更像一个真正可解释的“收缩强度”，在 Transformer 等模型上通常更稳定、更好调。工程实现里常见做法是：对 bias 与归一化层参数（如 LayerNorm 的 <span displaypfx="inline-" class="mathjax-container">\(\gamma,\beta\)</span>）不做 weight decay，以免对尺度/偏置项造成不必要的收缩。</p>
<div class="blog_h3"><span class="graybg">Muon</span></div>
<p>Muon 是一种面向神经网络隐藏层（Hidden Layer）权重矩阵（Weight Matrix）的优化器。它与 Adam/AdamW 的根本差异在于：AdamW 会为每个参数维度单独估计更新尺度，并对梯度/更新量做逐元素（Element-wise）归一化；Muon 则把一个二维权重张量视为一个整体，直接利用矩阵结构来塑造更新方向。因此，Muon 更像一种<span style="background-color: #c0c0c0;">矩阵感知（Matrix-aware）的优化器</span>。</p>
<p>它的核心做法不是改变“沿梯度下降”这个大方向，而是改变“更新张量应该具有什么几何形状”。典型写法可以概括为：先对梯度做动量（Momentum）累积，再把这个矩阵更新做一次正交化（Orthogonalization）后处理，然后才真正更新参数：</p>
<span displaypfx="" class="mathjax-container">\[M_{t+1}=\beta M_t+(1-\beta)G_t\]</span>
<span displaypfx="" class="mathjax-container">\[\Delta W_t=\mathrm{Orth}(M_{t+1}),\quad W_{t+1}=W_t-\eta\,\Delta W_t\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(G_t\)</span> 是当前梯度矩阵， <span displaypfx="inline-" class="mathjax-container">\(M_t\)</span> 是动量缓冲， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Orth}(\cdot)\)</span> 表示把更新矩阵变换到更“接近正交（Orthogonal）”的形状。工程实现里，这一步通常用 Newton-Schulz 迭代（Newton-Schulz Iteration）高效近似完成，因此 Muon 常被概括为“对动量更新做正交化”。</p>
<ul>
<li>优势：对线性层/MLP/注意力里的二维权重矩阵，Muon 往往能给出更结构化的更新方向；在一些大模型训练设定下，它的训练效率与收敛速度优于 AdamW。</li>
<li>边界：Muon 不是全参数通吃的默认方案。它通常只用于隐藏层的二维参数；embedding、bias、归一化参数、输出层等非二维或语义不同的参数，实践中常继续交给 AdamW。</li>
<li>工程特征：它强调更新矩阵的谱结构（Spectral Structure），而不是单个坐标的独立缩放；因此超参数分组与参数类型划分比 AdamW 更重要。</li>
</ul>
<p>可以把 AdamW 与 Muon 的差别记成一句话：<span style="background-color: #c0c0c0;">AdamW 解决“每个参数该走多大步”，Muon 进一步关心“整个矩阵应该以什么形状移动”</span>。因此 Muon 更像是隐藏层矩阵优化的专用工具，而不是对所有参数一视同仁的通用默认项。</p>
<div class="blog_h3"><span class="graybg">学习率调度</span></div>
<p>学习率（Learning Rate）往往是最敏感的超参数之一。即使优化器相同，只要学习率设错，训练就可能完全失败。学习率调度（Learning Rate Scheduling）/退火（Annealing）的核心思想是：前期用相对大的步长快速下降，后期逐渐减小步长做精细收敛。</p>
<p>常见调度方式包括：</p>
<ul>
<li>Step decay：按 epoch 或 step 乘以固定因子衰减，简单直接。</li>
<li>Linear decay：线性下降到较小值或 0，常用于大模型训练后段。</li>
<li>Warmup + decay：先小步热身，再进入正常学习率，最后逐步衰减；对 Transformer 和大 batch 训练尤其常见。</li>
<li>余弦退火（Cosine Annealing）：将学习率从较大值逐步衰减到较小值，把优化从“高温探索”过渡到“低温收敛”，在训练后期降低梯度噪声（gradient noise）与过冲（overshoot）风险，使参数能在极小值附近稳定细化。余弦曲线在起点与终点的一阶导数为 0，避免阶梯式衰减的突变，因而衰减更平滑、后期更柔和。</li>
</ul>
<p>Warmup 为什么有用？因为训练刚开始时，参数还处在一个非常“生”的区域，梯度统计不稳定，若一上来就用很大学习率，模型容易发散。先用较小步长热身几百或几千步，再拉到目标学习率，通常更稳。</p>
<p>余弦退火的典型写法（从 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> 衰减到 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\min}\)</span>）为：</p>
<span displaypfx="" class="mathjax-container">\[\eta(t)=\eta_{\min}+\frac{1}{2}(\eta_{\max}-\eta_{\min})\left(1+\cos\left(\pi\frac{t}{T}\right)\right)\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\eta(t)\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步使用的学习率。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> / <span displaypfx="inline-" class="mathjax-container">\(\eta_{\min}\)</span>：一个退火周期内的最大学习率与最小学习率。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(t\)</span>：当前步数（通常从 0 开始计），<span displaypfx="inline-" class="mathjax-container">\(T\)</span>：该周期的总步数。</li>
</ul>
<p>这个公式可以直接做两次代入来验证边界：当 <span displaypfx="inline-" class="mathjax-container">\(t=0\)</span> 时，<span displaypfx="inline-" class="mathjax-container">\(\cos(0)=1\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(\eta(0)=\eta_{\max}\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(t=T\)</span> 时，<span displaypfx="inline-" class="mathjax-container">\(\cos(\pi)=-1\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(\eta(T)=\eta_{\min}\)</span>。中间学习率按半个余弦周期平滑下降。</p>
<p>通过余弦（Cosine）提供了一个非常干净的<span style="background-color: #c0c0c0;">端点平滑（smooth endpoints）</span>性质：在起点和终点处变化率为 0。对上式求导可得</p>
<span displaypfx="" class="mathjax-container">\[\frac{d\eta}{dt}=-\frac{1}{2}(\eta_{\max}-\eta_{\min})\frac{\pi}{T}\sin\left(\pi\frac{t}{T}\right)\]</span>
<p>因此 <span displaypfx="inline-" class="mathjax-container">\(\sin(0)=\sin(\pi)=0\)</span>，学习率在周期开始与结束都不会出现突兀的“拐点”。相比之下，step decay 有不连续跳变，linear decay 在末端通常会突然到达下限并停止变化（出现不光滑的折点）。在非凸深度网络里，这种平滑退火往往更稳：前期保持较大步长便于探索，后期自然减小步长便于精细收敛。</p>
<p>工程上常见扩展是余弦重启（Cosine Annealing with Warm Restarts, SGDR）：把训练过程切成多个周期，每个周期把 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 重新从 0 开始计（并可逐周期拉长 <span displaypfx="inline-" class="mathjax-container">\(T\)</span>）。在标准 SGDR 中，<span style="background-color: #c0c0c0;">学习率在周期边界是不连续的</span>：它会在一个周期末端衰减到 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\min}\)</span>，并在下一个周期起点从 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> 重新开始（参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 不会重置）。</p>
<p>这种“重启”的作用不是为了制造噪声，而是把优化过程从“后期小步精修”短暂切回“较大步长探索”，以应对非凸问题中的平台区（plateau）与次优盆地（suboptimal basin）。学习率拉高会增大每步更新幅度与梯度噪声（gradient noise）的有效影响，帮助轨迹跳出当前区域并探索新的吸引域；而如果当前区域确实是更稳健的平坦极小值（flat minimal），后续退火通常会把参数再次拉回并在附近更精细地收敛。工程上常见做法是把 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> 设在“不会破坏稳定性”的范围内；若重启瞬间仍担心不稳定，也可以在每次重启后加一个很短的 warmup，让学习率在少量步数内从较小值爬升到 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span>。</p>
<p>没有一种调度策略对所有任务都最好。真正有效的做法是结合损失曲线、梯度稳定性、验证集表现和训练预算一起看：如果前期降不动，往往学习率偏小；如果剧烈震荡甚至发散，往往学习率偏大；如果后期长期卡在平台区，通常需要更细的衰减策略。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/sgdr.jpg"><img class="alignnone size-full wp-image-41059" src="https://blog.gmem.cc/wp-content/uploads/2026/03/sgdr.jpg" alt="sgdr" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">集成策略（Ensemble Learning）</span></div>
<p>集成学习（Ensemble Learning）的核心思想是：<span style="background-color: #c0c0c0;">不要把预测完全押在一个模型上，而是让多个模型以某种组织方式共同决定结果</span>。它背后的统计直觉并不神秘：如果多个模型的误差来源不完全相同，那么组合后的总误差通常会更小、更稳、更抗偶然扰动。</p>
<p>从误差分解视角看，集成学习最常见的两条路线分别在处理两种不同问题。Bagging 更擅长压低方差（Variance），适合“单个模型很容易被训练集波动带偏”的情形；Boosting 更擅长逐步降低偏差（Bias），适合“单个弱学习器表达力不够，但可以通过连续修正变强”的情形。树模型恰好非常适合这两条路线：单棵深树高方差，适合拿去做 Bagging；浅层回归树可作为局部纠错器，适合拿去做 Boosting。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">Bagging</td>
<td style="text-align: center;">Boosting</td>
<td style="text-align: center;">Stacking</td>
</tr>
</thead>
<tbody>
<tr>
<td>核心目标</td>
<td>降低方差，让模型更稳</td>
<td>降低偏差，让模型更准</td>
<td>学习如何组合不同模型，让互补性显式变成一个新模型</td>
</tr>
<tr>
<td>训练方式</td>
<td>多个基模型并行独立训练</td>
<td>多个弱学习器串行依次训练</td>
<td>先训练多个基模型，再训练一个元学习器做组合</td>
</tr>
<tr>
<td>每轮关注什么</td>
<td>同一个原始任务，但训练数据子集和特征视角不同</td>
<td>前一轮没有拟合好的误差、残差或负梯度</td>
<td>不同基模型的输出在什么条件下更可信</td>
</tr>
<tr>
<td>最终聚合</td>
<td>平均或投票</td>
<td>加权累加</td>
<td>由元学习器学习组合规则</td>
</tr>
<tr>
<td>典型代表</td>
<td>随机森林</td>
<td>GBDT、XGBoost、LightGBM、CatBoost</td>
<td>多模型堆叠集成</td>
</tr>
<tr>
<td>更像什么</td>
<td>多位评委独立打分后求平均</td>
<td>接力纠错，每个人只补前面没做好的部分</td>
<td>多位专家先各自判断，再由总负责人学习何时该信谁</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Bagging</span></div>
<p>Bagging（Bootstrap Aggregating）的核心做法是对训练集进行多次自助采样（bootstrap sampling），即反复执行有放回采样（sampling with replacement）生成多个训练子集，并在这些子集上分别训练基模型，以降低方差（Variance）。由于每个模型见到的数据子集略有差异，学到的决策边界不会完全一致；最后对预测结果做平均或多数投票，可抵消一部分过拟合噪声。</p>
<p>Bagging 的关键不在“模型一定很多”，而在<span style="background-color: #c0c0c0;">人为制造模型之间的差异性</span>。如果每个基模型看到的训练数据完全相同、特征也完全相同、优化过程也完全相同，那么把它们重复训练很多次并不会带来真正互补的信息。bootstrap 采样、特征子采样、不同随机初始化，本质上都在做同一件事：让多个模型不要犯完全一样的错。</p>
<p>例：如果单棵决策树很容易被训练集中的偶然样本带偏，那么训练 100 棵在不同 bootstrap 样本上的树，再投票，通常会比只用 1 棵树稳定得多。这也是随机森林的核心直觉。</p>
<div class="blog_h3"><span class="graybg">Boosting</span></div>
<p>Boosting 的思路与 Bagging 相反：它不是“并行训练很多彼此独立的模型”，而是“串行训练一串弱学习器（Weak Learners），让后一个模型专门修正前一个模型没做好的部分”。因此它更像“老师批改作业”：每一轮都盯着上轮最容易出错的题继续强化。</p>
<p>以加法模型视角看，Boosting 逐步构造</p>
<span displaypfx="" class="mathjax-container">\[F_M(x)=\sum_{m=1}^{M}\alpha_m h_m(x)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(h_m(x)\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 个弱学习器， <span displaypfx="inline-" class="mathjax-container">\(\alpha_m\)</span> 是它的权重。公式不是为了堆符号，而是在表达：最终模型是“很多个简单模型的加权和”，每一轮都往当前模型上加一小块“修正项”。</p>
<p>这类“接力纠错”最直观的理解，是把当前模型与真实目标之间的差距看成下一轮的新任务。回归里，这个差距常直接表现为残差；更一般地，在可微损失下，它表现为损失对当前预测的负梯度。于是每一轮不再去重学整个标签，而是只学“接下来该往哪里补”。这就是 GBDT 家族能持续逼近目标函数的根本原因。</p>
<p>这里还可以再区分两种常见实现。较早的 Boosting，如 AdaBoost，更强调<span style="background-color: #c0c0c0;">把之前分错的样本权重调高</span>，让后续弱学习器更多关注难样本；GBDT 这一支则更强调<span style="background-color: #c0c0c0;">直接拟合当前损失的残差或负梯度</span>。两者都属于“串行纠错”，只是把“错误信息”传给下一轮的方式不同。</p>
<p>Boosting 也解释了为什么这类模型通常使用<span style="background-color: #c0c0c0;">较浅的小树</span>作为基学习器。因为每一棵树的职责不是独立解决全问题，而是在当前模型基础上补一个局部修正。如果每棵树都长得很深、很强，单轮就可能把训练集吃得过满，后续串行叠加更容易过拟合；若每棵树只做小而稳的修正，再配合学习率与早停，整体通常更可控。</p>
<div class="blog_h3"><span class="graybg">Stacking</span></div>
<p>Stacking（堆叠集成）不只是平均多个模型输出，而是再训练一个元学习器（Meta-Learner）去学习“什么时候该信哪个模型”。例如一个基模型擅长处理稀疏文本特征，另一个擅长处理数值特征，Stacking 可以学会在不同样本上动态加权它们。</p>
<p>它像“专家会诊 + 总负责人”：若文本模型和表格模型各有专长，元模型就负责综合判断谁在当前病例上更可信。</p>
<div class="blog_h2"><span class="graybg">机器学习编程</span></div>
<p>机器学习编程（Machine Learning Programming）处理的核心问题是：当一个模型被写成代码并真正运行起来时，<span style="background-color: #c0c0c0;">谁负责组织训练流程，谁负责表达数学操作，谁又负责在具体硬件上把这些操作算快</span>。这三层通常分别对应编程框架（Framework）、算子（Operator）和内核（Kernel）。理解这三个层次，有助于把“模型公式”与“工程实现”连接起来。</p>
<p>它们不是彼此独立的三个名词，而是一条自上而下的执行链。研究者或工程师先在框架里定义模型结构与训练流程；框架再把模型拆成一系列算子，例如矩阵乘法、卷积、归一化、激活、softmax；每个算子最终还需要落到某个硬件相关的内核实现上，才能在 CPU、GPU、TPU 或其他加速器上真正执行。因此，<span style="background-color: #c0c0c0;">框架决定开发体验，算子决定计算图语义，内核决定实际运行效率</span>。</p>
<div class="blog_h3"><span class="graybg">框架（Framework）</span></div>
<p>框架（Framework）并不是单一层次的概念。在现代机器学习工程里，至少要区分两层：一层是基础框架（Foundational Framework），负责张量（Tensor）、自动求导（Automatic Differentiation）、算子调度与底层执行；另一层是高层框架（High-level Framework），负责把某一类模型、某一类训练范式或某一类工程流程封装成更直接的接口。前者提供“地基”，后者提供“脚手架”和“现成结构”。</p>
<p>因此，PyTorch、TensorFlow、JAX 这类系统更适合放在基础框架层；而 Transformers、DeepSpeed、ModelScope、Lightning 这类工具，则更适合放在高层框架层。它们之间不是替代关系，而是典型的上下层关系：<span style="background-color: #c0c0c0;">高层框架通常建立在基础框架之上，用更强的任务抽象、更少的模板代码和更完整的工程能力，把常见训练与推理流程直接组织起来</span>。</p>
<div class="blog_h3"><span class="graybg">常见框架简介</span></div>
<p>从工程使用频率看，开发者最常接触的是一组已经按职责分层的常见框架：高层框架负责组织训练与推理流程，基础框架负责真正执行张量计算，某些系统还进一步偏向执行优化与部署。把这些名字放回分层结构里理解，比孤立记忆框架名称更清楚。</p>
<div class="blog_h4"><span class="graybg">高层框架（High-level Framework）</span></div>
<p>高层框架的核心价值在于，它们不重新发明张量计算和自动求导，而是在基础框架之上增加更强的任务语义、训练编排、模型生态或分布式能力。很多工程里，开发者日常接触最多的其实是这一层。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">高层框架</td>
<td style="text-align: center;">主要依赖的基础框架</td>
<td style="text-align: center;">核心定位</td>
<td style="text-align: center;">替你封装了什么</td>
<td style="text-align: center;">最适合的场景</td>
<td style="text-align: center;">与基础框架的关系</td>
</tr>
</thead>
<tbody>
<tr>
<td>Transformers</td>
<td>以 PyTorch 为主，也支持 TensorFlow 与 JAX / Flax</td>
<td>预训练 Transformer 模型与任务头生态</td>
<td>模型定义、权重加载、tokenizer / processor、Trainer、pipeline、任务头</td>
<td>NLP、LLM、多模态模型的微调与推理</td>
<td>通常不是自己实现底层训练系统，而是调用底层框架完成张量计算与反向传播</td>
</tr>
<tr>
<td>DeepSpeed</td>
<td>主要建立在 PyTorch 之上</td>
<td>大模型训练与推理优化框架</td>
<td>ZeRO、参数分片、优化器状态管理、分布式训练编排、推理加速</td>
<td>超大模型训练、多卡 / 多机扩展、显存受限训练</td>
<td>本质上是对 PyTorch 训练过程的增强与重写，而不是替代 PyTorch</td>
</tr>
<tr>
<td>Unsloth</td>
<td>主要建立在 PyTorch 之上，并与 Transformers、PEFT、TRL 等 Hugging Face 生态深度协同</td>
<td>面向 LLM 微调与对齐的性能导向高层框架</td>
<td>快速加载与训练配方、QLoRA / DoRA / RL 配置、量化微调、长上下文训练、导出到 GGUF / Ollama / vLLM / Hugging Face</td>
<td>单卡或少卡进行 LLM 微调、偏好对齐、消费级显卡上的高性价比实验</td>
<td>它不替代 PyTorch 或 Transformers，而是在它们之上把“高效微调 + 导出部署”这条链路进一步封装并做性能优化</td>
</tr>
<tr>
<td>ModelScope</td>
<td>主要建立在 PyTorch 之上，也兼容其他学习框架与平台能力</td>
<td>模型社区 + SDK + 训练 / 推理工作流</td>
<td>模型获取、pipeline、训练入口、评测、部署衔接、领域模型集成</td>
<td>中文生态、多模态任务、快速调用开源模型并做微调</td>
<td>更像“模型平台层 + 高层开发框架”，下层训练仍要落到基础框架执行</td>
</tr>
<tr>
<td>PyTorch Lightning</td>
<td>PyTorch</td>
<td>训练流程组织框架</td>
<td>Trainer、设备放置、日志、checkpoint、验证循环、分布式训练模板</td>
<td>希望保留 PyTorch 灵活性，同时减少训练样板代码</td>
<td>把 PyTorch 代码组织得更规范，但底层模型与梯度仍然是 PyTorch</td>
</tr>
<tr>
<td>Accelerate</td>
<td>PyTorch</td>
<td>分布式训练与多设备执行抽象</td>
<td>多 GPU / 多机启动、混合精度、设备管理、统一训练脚本适配</td>
<td>想在尽量少改代码的前提下把 PyTorch 训练扩展到分布式环境</td>
<td>不取代 PyTorch 训练代码，而是让同一份 PyTorch 代码更容易跨设备运行</td>
</tr>
<tr>
<td>Keras 3</td>
<td>可运行在 TensorFlow、JAX、PyTorch 之上，推理还可对接 OpenVINO</td>
<td>高层模型开发接口</td>
<td>Layer / Model 抽象、训练接口、callback、分布式 API、生态组件</td>
<td>需要高层建模接口且希望在多后端之间切换</td>
<td>处于基础框架之上，强调统一建模接口，而不是直接取代底层后端</td>
</tr>
<tr>
<td>Sentence Transformers</td>
<td>主要建立在 Transformers 与 PyTorch 之上</td>
<td>Embedding 与 reranker 高层框架</td>
<td>文本向量化、相似度训练、检索 / rerank 训练器、评测工具</td>
<td>语义检索、向量召回、文本匹配、reranking</td>
<td>属于面向特定任务族的高层框架，下层依赖 Transformers 与 PyTorch</td>
</tr>
<tr>
<td>MMEngine / OpenMMLab</td>
<td>主要建立在 PyTorch 之上</td>
<td>通用训练引擎与视觉算法框架底座</td>
<td>Runner、Hook、Config、数据流、训练 / 验证 / 测试流程组织</td>
<td>检测、分割、姿态估计等视觉任务</td>
<td>用统一工程抽象组织 PyTorch 训练，尤其适合复杂视觉实验体系</td>
</tr>
</tbody>
</table>
<p>这一层最容易让人产生“它自己就能训练模型”的直觉，但从执行链条看，它们大多只是把训练过程组织得更高级。以 Transformers 为例，<span style="background-color: #c0c0c0;">Trainer 可以发起训练，但底层的张量、梯度、优化器、自动求导与设备执行，通常仍然由 PyTorch 负责</span>；Lightning、Accelerate、DeepSpeed 也是同样的逻辑，只是它们封装的侧重点不同。</p>
<div class="blog_h4"><span class="graybg">Unsloth</span></div>
<p>Unsloth 最适合放在高层框架这一层理解。它并不重新定义 Tensor、自动求导（Autograd）或底层计算图，而是在 PyTorch、Transformers、PEFT、TRL 这一整套既有生态之上，把大语言模型（Large Language Model, LLM）的高频训练流程重新组织成更偏性能导向的开发体验。它解决的核心问题不是“怎样从零实现神经网络”，而是<span style="background-color: #c0c0c0;">怎样在尽可能小的显存和尽可能少的工程样板下，把 LLM 微调、对齐、导出与部署这条链路跑通</span>。</p>
<p>从开发者视角看，Unsloth 的典型工作流通常仍然是“加载 Hugging Face 模型 → 挂接 PEFT 适配器 → 用监督微调（SFT）或强化学习（RL）训练 → 导出到下游推理栈”。它的价值在于把这条链上几个最痛的环节一起压平：第一，量化微调（如 LoRA / QLoRA）与长上下文训练更容易直接落地；第二，针对 GRPO 等对齐训练给出更直接的入口；第三，把训练后的模型或适配器导出到 GGUF、Ollama、vLLM、SGLang、Hugging Face 等下游环境的路径做得更短。换言之，Unsloth 不是另起炉灶，而是把已有生态串得更紧，并对其中最贵的显存、最长的上下文和最复杂的导出环节做专项优化。</p>
<p>它与其他高层框架的边界也需要分清。Transformers 的优势在于模型家族与任务头生态最通用；Accelerate 更像多设备执行抽象；DeepSpeed 更偏分布式训练、参数分片与大规模集群优化；Unsloth 则把重心压在“单机到小规模多卡场景下，如何更快、更省显存地完成 LLM 微调与对齐”。因此，它尤其适合消费级 GPU、本地实验、小团队快速验证、Notebook 驱动的训练流程，以及以 LoRA / QLoRA / RL 为主的大模型增量训练。若任务是超大规模集群预训练或需要复杂的跨机并行策略，DeepSpeed / Megatron 一类系统通常仍然更中心；若任务是通用模型调用与标准微调，Transformers 仍然是最基础的入口。</p>
<p>工程上可以把 Unsloth 看成“面向 LLM 微调的高性能工作台”。它一端连接 Hugging Face 模型与适配器生态，另一端连接本地推理、GGUF、Ollama、vLLM、SGLang 等部署路径，中间则用一套更偏性能调优的训练封装把它们粘起来。对初学者，它降低的是上手门槛：少改几处配置就能跑通量化微调或 RL；对熟悉生态的工程师，它降低的是试验成本：同样的显存预算下，能尝试更长上下文、更复杂的对齐流程，或更快地把训练结果导出到不同推理栈。它的本质依然是高层工程抽象，而不是底层深度学习框架的替代品。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">比较维度</td>
<td style="text-align: center;">Transformers</td>
<td style="text-align: center;">DeepSpeed</td>
<td style="text-align: center;">Unsloth</td>
</tr>
</thead>
<tbody>
<tr>
<td>核心目标</td>
<td>统一模型与任务接口</td>
<td>放大训练规模与显存效率</td>
<td>压低 LLM 微调 / 对齐门槛并提升单机效率</td>
</tr>
<tr>
<td>最擅长的问题</td>
<td>模型加载、任务头、Trainer、pipeline</td>
<td>ZeRO、分布式并行、大模型集群训练</td>
<td>QLoRA、GRPO、长上下文、快速导出部署</td>
</tr>
<tr>
<td>典型使用者</td>
<td>几乎所有 NLP / LLM 开发者</td>
<td>大模型平台与多机训练团队</td>
<td>本地实验者、个人开发者、小团队 LLM 微调工程师</td>
</tr>
<tr>
<td>与 PyTorch 的关系</td>
<td>调用其张量与训练能力</td>
<td>增强其训练与并行系统</td>
<td>在其之上重组 LLM 微调与导出流程</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">基础框架（Foundational Framework）</span></div>
<p>基础框架直接定义张量运算、计算图、自动求导、优化器、算子调度与设备执行能力。它们离硬件更近，也离“神经网络真正怎样被算出来”更近。高层框架能否存在，首先取决于这一层是否提供了足够稳定和强大的底座。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">基础框架</td>
<td style="text-align: center;">核心抽象</td>
<td style="text-align: center;">主要优势</td>
<td style="text-align: center;">最适合的场景</td>
<td style="text-align: center;">典型局限</td>
</tr>
</thead>
<tbody>
<tr>
<td>PyTorch</td>
<td>Tensor、<pre class="crayon-plain-tag">nn.Module</pre>、autograd、动态图（Dynamic Computation Graph）</td>
<td>灵活、直观、研究生态最强，训练与调试体验优秀</td>
<td>研究、论文复现、大模型训练、需要自定义训练逻辑的任务</td>
<td>如果完全手写训练循环，工程样板代码较多，部署链路常需额外工具配合</td>
</tr>
<tr>
<td>TensorFlow</td>
<td>Tensor、Layer / Model、自动求导、图执行与编译</td>
<td>训练与部署体系完整，服务化、端侧与工业链路成熟</td>
<td>企业级生产环境、需要完整训练到部署闭环的场景</td>
<td>研究阶段的编码与调试直观性通常不如 PyTorch</td>
</tr>
<tr>
<td>JAX</td>
<td>数组（Array）+ 函数变换 + XLA 编译</td>
<td>编译优化强，函数式表达清晰，适合大规模并行数值计算</td>
<td>需要强编译能力、自定义并行策略、科研数值实验的任务</td>
<td>函数式编程习惯要求更高，工程生态相对更偏高级用户</td>
</tr>
<tr>
<td>PaddlePaddle</td>
<td>Tensor、动态图 / 静态图、训练与产业工具链</td>
<td>中文生态与产业落地支持强，训练推理工具链完整</td>
<td>产业应用、教育场景、中文任务与本土化生态</td>
<td>国际社区规模与通用论文实现数量通常少于 PyTorch</td>
</tr>
</tbody>
</table>
<p>如果把机器学习工程比作建楼，那么基础框架提供的是钢筋、水泥、电路和承重结构。高层框架之所以能让开发速度显著提升，正是因为底层这些张量、梯度和算子能力已经由基础框架稳定提供。</p>
<div class="blog_h4"><span class="graybg">执行与部署系统（Execution and Deployment Stack）</span></div>
<p>除了高层框架和基础框架，还存在一类经常与“框架”混称、但职责不同的系统：执行与部署系统。它们的核心目标不是让用户更方便地写模型，而是让已经定义好的模型在特定硬件上更快、更省、更稳定地运行。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">系统</td>
<td style="text-align: center;">主要定位</td>
<td style="text-align: center;">最常见作用</td>
<td style="text-align: center;">典型场景</td>
<td style="text-align: center;">与前两层的关系</td>
</tr>
</thead>
<tbody>
<tr>
<td>ONNX Runtime</td>
<td>跨框架推理执行系统</td>
<td>加载 ONNX 图，做图优化、算子调度与多后端执行</td>
<td>统一部署、跨框架导出后的推理执行</td>
<td>位于模型定义之后，更接近运行时（Runtime）而不是训练框架</td>
</tr>
<tr>
<td>TensorRT</td>
<td>NVIDIA GPU 推理优化系统</td>
<td>图优化、层融合、量化与 kernel 自动选择</td>
<td>低延迟在线推理、高吞吐批量服务</td>
<td>通常接收上层框架导出的模型，再做更深的硬件侧优化</td>
</tr>
<tr>
<td>OpenVINO</td>
<td>Intel 硬件推理栈</td>
<td>模型转换、图优化、Intel CPU / iGPU / VPU 推理</td>
<td>Intel 服务器与边缘设备部署</td>
<td>更接近部署后端，而不是通用训练框架</td>
</tr>
<tr>
<td>TVM</td>
<td>深度学习编译栈</td>
<td>自动调优、代码生成、异构硬件适配</td>
<td>边缘部署、自定义芯片、性能工程</td>
<td>站在算子和内核之间，为不同硬件生成更优执行实现</td>
</tr>
</tbody>
</table>
<p>因此，讨论“框架”时最好先分清是哪一层：<span style="background-color: #c0c0c0;">高层框架负责把任务和流程组织起来，基础框架负责把张量与梯度真正算出来，执行与部署系统负责把已经定义好的模型在目标硬件上跑到更优</span>。这三层一旦混在一起，很多看似相近的名词就会失去边界。</p>
<div class="blog_h3"><span class="graybg">算子（Operator）</span></div>
<p>算子（Operator）是计算图（Computation Graph）的基本运算单元。它定义一个明确的数学变换：输入什么张量（Tensor）、输出什么张量、张量的形状（Shape）和数据类型（Data Type）如何变化，以及反向传播时梯度如何计算。框架层写出的模块、层、网络块，最终都会被拆解成一串更细粒度的算子。</p>
<p>从工程实现上看，算子这一层负责表达“数学语义”。例如一个线性层（Linear Layer）会被拆成 MatMul 与 Bias Add；一个自注意力（Self-Attention）模块会被拆成 Q/K/V 投影、MatMul、Scale、Mask、Softmax、再一次 MatMul、Dropout、LayerNorm 等。高层模块能否被编译优化，本质上取决于这些算子能否被识别、融合与高效执行。</p>
<p>常用算子可以分成四大类：线性代数与张量形状类、神经网络前向计算类、序列与索引操作类、训练与优化类。下面的表格按这一方式展开。</p>
<div class="blog_h4"><span class="graybg">线性代数与张量形状类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>MatMul / GEMM</td>
<td>矩阵乘法，完成线性投影与特征混合</td>
<td>二维或更高维张量，输出新的线性组合</td>
<td>全连接层、注意力投影、MLP</td>
<td>最核心的高密度算子之一，常直接决定训练吞吐</td>
</tr>
<tr>
<td>Batch MatMul</td>
<td>批量矩阵乘法，多个矩阵对并行相乘</td>
<td>三维及以上张量</td>
<td>多头注意力（Multi-Head Attention）</td>
<td>常与转置、缩放、mask 连用</td>
</tr>
<tr>
<td>Add / Bias Add</td>
<td>逐元素加法</td>
<td>两个可广播张量</td>
<td>残差连接、偏置项、特征融合</td>
<td>常被融合到前后算子中减少访存</td>
</tr>
<tr>
<td>Sub / Mul / Div</td>
<td>逐元素减法、乘法、除法</td>
<td>逐元素张量运算</td>
<td>归一化、门控、缩放</td>
<td>广播规则必须和张量形状匹配</td>
</tr>
<tr>
<td>Scale</td>
<td>用常数或向量对张量缩放</td>
<td>输入张量与标量 / 向量</td>
<td>attention 中的 <span displaypfx="inline-" class="mathjax-container">\(1/\sqrt{d_k}\)</span> 缩放</td>
<td>经常被编译器与前后算子融合</td>
</tr>
<tr>
<td>Transpose / Permute</td>
<td>重排维度顺序</td>
<td>输入张量到相同元素、不同布局的张量</td>
<td>NCHW / NHWC 转换，多头维度重排</td>
<td>逻辑上不改值，但常改变内存访问模式</td>
</tr>
<tr>
<td>Reshape / View</td>
<td>改变张量形状而不改变元素总数</td>
<td>同样的数据，不同 shape</td>
<td>展平、分头、合并头、batch 展开</td>
<td>若内存不连续，可能触发额外复制</td>
</tr>
<tr>
<td>Expand / BroadcastTo</td>
<td>按广播规则扩展维度</td>
<td>低维张量扩展为高维张量</td>
<td>偏置广播、mask 扩展</td>
<td>逻辑扩展不一定真实复制数据</td>
</tr>
<tr>
<td>Squeeze / Unsqueeze</td>
<td>删除或插入长度为 1 的维度</td>
<td>维度数变化，数据值不变</td>
<td>batch / channel 维调整</td>
<td>常用于接口对齐和算子拼接</td>
</tr>
<tr>
<td>Concat / Stack</td>
<td>拼接多个张量</td>
<td>多个同类型张量合并为一个</td>
<td>多特征合并、多分支网络</td>
<td>Concat 沿已有维度拼接，Stack 会新增维度</td>
</tr>
<tr>
<td>Split / Chunk</td>
<td>将一个张量拆成多个子张量</td>
<td>一个张量拆成若干块</td>
<td>Q/K/V 切分、多分支路径</td>
<td>与 Concat、Stack 常成对出现</td>
</tr>
<tr>
<td>ReduceSum / ReduceMean / ReduceMax</td>
<td>沿某些维度做聚合</td>
<td>高维张量压缩成低维张量</td>
<td>池化、loss 聚合、统计量计算</td>
<td>归约（Reduction）通常对并行实现要求较高</td>
</tr>
<tr>
<td>EinSum</td>
<td>用爱因斯坦求和规则表达复合张量运算</td>
<td>多个张量到一个张量</td>
<td>复杂线性代数、注意力原型实现</td>
<td>表达力强，但实际性能往往依赖后端是否能分解优化</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">神经网络前向计算类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Convolution</td>
<td>局部感受野加权求和</td>
<td>特征图与卷积核，输出新的特征图</td>
<td>CNN、视觉 backbone、语音模型</td>
<td>步幅、填充、分组卷积会显著影响性能与感受野</td>
</tr>
<tr>
<td>Depthwise / Group Convolution</td>
<td>按通道组或单通道卷积</td>
<td>特征图到特征图</td>
<td>MobileNet、轻量视觉网络</td>
<td>减少计算量，但对 kernel 实现要求更高</td>
</tr>
<tr>
<td>Pooling（Max / Avg）</td>
<td>局部下采样与聚合</td>
<td>特征图压缩为空间更小的特征图</td>
<td>CNN、时序聚合</td>
<td>降低分辨率与计算量，也带来信息损失</td>
</tr>
<tr>
<td>Adaptive Pooling</td>
<td>把输入压到固定输出尺寸</td>
<td>任意空间尺寸到固定尺寸</td>
<td>视觉分类头、全局池化</td>
<td>方便不同输入尺寸统一到下游全连接层</td>
</tr>
<tr>
<td>ReLU</td>
<td>负值截断为 0 的激活函数</td>
<td>逐元素非线性变换</td>
<td>MLP、CNN、分类头</td>
<td>实现简单，稀疏性强</td>
</tr>
<tr>
<td>GELU</td>
<td>平滑激活，保留小负值的连续变化</td>
<td>逐元素非线性变换</td>
<td>Transformer、LLM MLP</td>
<td>现代语言模型最常见的激活之一</td>
</tr>
<tr>
<td>SiLU / Swish</td>
<td>输入与 sigmoid 门控的乘积</td>
<td>逐元素非线性变换</td>
<td>高性能视觉与语言模型</td>
<td>平滑、效果稳定，常见于新型 backbone</td>
</tr>
<tr>
<td>Sigmoid</td>
<td>把实数映射到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span></td>
<td>逐元素概率化</td>
<td>门控单元、二分类输出、多标签任务</td>
<td>饱和区梯度小，深层网络中通常不作主激活</td>
</tr>
<tr>
<td>Tanh</td>
<td>把实数映射到 <span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span></td>
<td>逐元素非线性变换</td>
<td>早期 RNN、门控结构</td>
<td>零中心，但同样存在饱和问题</td>
</tr>
<tr>
<td>Softmax</td>
<td>把一组分数归一化为概率分布</td>
<td>类别分数到概率</td>
<td>多分类头、attention 权重</td>
<td>常与交叉熵和 mask 配合，数值稳定性关键</td>
</tr>
<tr>
<td>LayerNorm</td>
<td>对单个样本的最后若干维做归一化</td>
<td>输入张量到同 shape 张量</td>
<td>Transformer、LLM</td>
<td>不依赖 batch 统计量，适合变长序列</td>
</tr>
<tr>
<td>BatchNorm</td>
<td>利用 batch 统计量做归一化</td>
<td>输入张量到同 shape 张量</td>
<td>CNN、视觉任务</td>
<td>训练与推理行为不同，小 batch 时效果可能下降</td>
</tr>
<tr>
<td>RMSNorm</td>
<td>基于均方根做归一化</td>
<td>输入张量到同 shape 张量</td>
<td>许多现代大语言模型</td>
<td>比 LayerNorm 更简洁，计算更轻</td>
</tr>
<tr>
<td>Attention / SDPA</td>
<td>基于相似度对值向量加权聚合</td>
<td>Q、K、V 到上下文表示</td>
<td>Transformer、跨模态模型</td>
<td>高层看是算子族，底层常映射到 FlashAttention 等 kernel</td>
</tr>
<tr>
<td>Embedding Lookup</td>
<td>根据离散索引查表取向量</td>
<td>token id 到 embedding 向量</td>
<td>NLP、推荐系统、类别特征</td>
<td>本质是参数矩阵的索引读取，不是普通 MatMul</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">序列与索引操作类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Gather</td>
<td>按给定索引抽取元素或切片</td>
<td>源张量与索引张量</td>
<td>embedding、beam search、采样</td>
<td>访问模式离散，容易受内存带宽限制</td>
</tr>
<tr>
<td>Scatter / ScatterAdd</td>
<td>按索引写回或累加</td>
<td>目标张量、索引、更新值</td>
<td>图神经网络、稀疏更新</td>
<td>并发写冲突和原子操作代价常是性能瓶颈</td>
</tr>
<tr>
<td>Index Select</td>
<td>按某一维选取指定位置</td>
<td>张量与一维索引</td>
<td>子序列抽取、类别筛选</td>
<td>语义上比通用 gather 更窄，但常更清晰</td>
</tr>
<tr>
<td>Slice / Narrow</td>
<td>截取连续区间</td>
<td>大张量切出子张量</td>
<td>窗口注意力、局部特征抽取</td>
<td>若数据连续，可几乎零开销视图化</td>
</tr>
<tr>
<td>Mask Fill / Select</td>
<td>按布尔掩码选择或填充值</td>
<td>张量与 mask</td>
<td>attention mask、padding 屏蔽</td>
<td>对变长序列与非法位置处理非常关键</td>
</tr>
<tr>
<td>Where</td>
<td>按条件在两个值之间选择</td>
<td>条件张量与候选张量</td>
<td>条件计算、loss 屏蔽、数值裁剪</td>
<td>本质是逐元素条件分支</td>
</tr>
<tr>
<td>Argmax / Argmin</td>
<td>返回最大 / 最小值所在索引</td>
<td>张量到索引</td>
<td>分类预测、贪心解码</td>
<td>输出是位置而非概率，通常不可导</td>
</tr>
<tr>
<td>TopK</td>
<td>返回前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个值及其索引</td>
<td>张量到值与索引</td>
<td>检索、beam search、采样截断</td>
<td>常与排序、候选筛选结合</td>
</tr>
<tr>
<td>Sort / Argsort</td>
<td>排序并返回顺序</td>
<td>张量到排序结果</td>
<td>排序损失、候选重排</td>
<td>复杂度高，尽量只在必要路径中使用</td>
</tr>
<tr>
<td>Pad</td>
<td>在边界补零或补指定值</td>
<td>原张量到更大张量</td>
<td>卷积前处理、batch 对齐、序列补齐</td>
<td>padding 策略会影响有效计算比例</td>
</tr>
<tr>
<td>Pack / Unpack Sequence</td>
<td>压缩或还原变长序列表示</td>
<td>变长序列与紧凑表示之间转换</td>
<td>RNN、语音与时序模型</td>
<td>用于减少 padding 带来的无效计算</td>
</tr>
<tr>
<td>Position Encoding Add</td>
<td>注入位置信息</td>
<td>token 表示与位置编码</td>
<td>Transformer、序列模型</td>
<td>绝对位置、相对位置、RoPE 在实现形式上不同</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">训练与优化类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Dropout</td>
<td>训练时随机失活部分单元</td>
<td>输入张量到稀疏化后的张量</td>
<td>MLP、attention、分类头</td>
<td>训练和推理行为不同，推理阶段通常关闭</td>
</tr>
<tr>
<td>Cross-Entropy Loss</td>
<td>衡量预测分布与真实类别的差距</td>
<td>logits 与标签到标量损失</td>
<td>分类、语言模型、token 分类</td>
<td>实现中常与 log-softmax 融合提高稳定性</td>
</tr>
<tr>
<td>NLL Loss</td>
<td>负对数似然损失</td>
<td>对数概率与标签到损失</td>
<td>分类任务、序列建模</td>
<td>通常接在 log-softmax 之后</td>
</tr>
<tr>
<td>MSE Loss</td>
<td>均方误差</td>
<td>预测值与目标值到损失</td>
<td>回归、蒸馏、表示对齐</td>
<td>对异常值较敏感</td>
</tr>
<tr>
<td>L1 / SmoothL1 Loss</td>
<td>绝对误差或平滑绝对误差</td>
<td>预测值与目标值到损失</td>
<td>目标检测、鲁棒回归</td>
<td>比 MSE 对异常值更稳</td>
</tr>
<tr>
<td>KL Divergence</td>
<td>衡量两个分布之间的差异</td>
<td>两个概率分布到标量</td>
<td>知识蒸馏、VAE、分布对齐</td>
<td>输入通常需要是概率或对数概率</td>
</tr>
<tr>
<td>Backward / Gradient</td>
<td>沿计算图反向传播梯度</td>
<td>损失到各参数梯度</td>
<td>所有训练流程</td>
<td>框架自动求导的核心能力就体现在这里</td>
</tr>
<tr>
<td>Gradient Clip</td>
<td>限制梯度范数或幅值</td>
<td>梯度到裁剪后梯度</td>
<td>RNN、大模型训练</td>
<td>控制梯度爆炸，提升训练稳定性</td>
</tr>
<tr>
<td>Optimizer Step（SGD / Adam / AdamW）</td>
<td>根据梯度更新参数</td>
<td>参数、梯度、状态到新参数</td>
<td>每一步训练迭代</td>
<td>常被实现为 fused optimizer kernel 以减少开销</td>
</tr>
<tr>
<td>Weight Decay</td>
<td>对参数施加正则化收缩</td>
<td>参数到受约束更新</td>
<td>分类、语言模型、视觉模型</td>
<td>现代实现常与 AdamW 解耦</td>
</tr>
<tr>
<td>AllReduce</td>
<td>跨设备聚合梯度或统计量</td>
<td>多卡张量到同步后的张量</td>
<td>数据并行训练</td>
<td>严格说更接近通信算子，但在训练图中极常见</td>
</tr>
<tr>
<td>AllGather / ReduceScatter</td>
<td>跨设备收集或切分张量</td>
<td>多设备张量通信</td>
<td>张量并行、序列并行、ZeRO</td>
<td>大模型分布式训练不可缺少</td>
</tr>
</tbody>
</table>
<p>因此，算子层是连接“模型定义”和“底层执行”的语义中枢。看懂模型实际调用了哪些算子，基本就等于看懂了它在做哪些数学步骤，以及这些步骤能否被进一步融合和加速。</p>
<div class="blog_h3"><span class="graybg">内核（Kernel）</span></div>
<p>内核（Kernel）是算子的底层实现。它规定了某个算子如何映射到具体硬件：线程如何组织、数据如何分块、是否使用共享内存（Shared Memory）、是否调用向量化指令、是否走 Tensor Core、是否把多个小算子融合成一次执行。若说算子定义的是“做什么”，那么内核定义的就是“怎样在这台机器上把它做快”。</p>
<p>从性能工程角度看，内核层回答的是：<span style="background-color: #c0c0c0;">同一个数学算子，针对不同硬件、数据形状、精度格式和访存模式，哪种实现最优</span>。这也是为什么表面上同样是 MatMul、LayerNorm 或 Attention，不同框架和后端的速度会相差很大。</p>
<p>常见内核或内核族可以按下表理解：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">内核 / 内核族</td>
<td style="text-align: center;">主要服务的算子</td>
<td style="text-align: center;">典型硬件 / 后端</td>
<td style="text-align: center;">核心优化手段</td>
<td style="text-align: center;">最典型的收益</td>
<td style="text-align: center;">常见场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>cuBLAS GEMM kernel</td>
<td>MatMul、Linear、Batch MatMul</td>
<td>NVIDIA GPU</td>
<td>tile blocking、Tensor Core、流水线、寄存器复用</td>
<td>把矩阵乘法做到接近硬件峰值吞吐</td>
<td>全连接层、attention 投影、MLP</td>
</tr>
<tr>
<td>cuDNN Convolution kernel</td>
<td>Convolution、Pooling、部分归一化与激活</td>
<td>NVIDIA GPU</td>
<td>direct convolution、im2col + GEMM、Winograd、FFT 自动选择</td>
<td>按输入形状自动切换最优卷积路径</td>
<td>CNN、视觉 backbone、语音前端</td>
</tr>
<tr>
<td>oneDNN / MKL kernel</td>
<td>MatMul、Convolution、Normalization</td>
<td>x86 CPU</td>
<td>SIMD 向量化、cache blocking、线程并行</td>
<td>提升 CPU 推理与训练效率</td>
<td>服务器 CPU 推理、无 GPU 环境</td>
</tr>
<tr>
<td>NCCL communication kernel</td>
<td>AllReduce、AllGather、ReduceScatter、Broadcast</td>
<td>NVIDIA 多 GPU</td>
<td>环形通信（Ring）、树形通信（Tree）、链路拓扑优化</td>
<td>降低多卡同步开销</td>
<td>数据并行、张量并行、大模型训练</td>
</tr>
<tr>
<td>FlashAttention kernel</td>
<td>Scaled Dot-Product Attention</td>
<td>NVIDIA GPU 及其他支持相应实现的加速器</td>
<td>分块、在线 softmax、kernel fusion、减少 HBM 访存</td>
<td>把 attention 从显存瓶颈拉回到更接近计算瓶颈</td>
<td>Transformer、LLM、长序列建模</td>
</tr>
<tr>
<td>Fused LayerNorm / RMSNorm kernel</td>
<td>LayerNorm、RMSNorm、Bias Add、Residual Add</td>
<td>GPU / CPU 后端</td>
<td>多步逐元素运算融合为一次访存</td>
<td>显著降低 memory-bound 算子的开销</td>
<td>Transformer block、LLM 推理</td>
</tr>
<tr>
<td>Fused MLP kernel</td>
<td>Bias Add、GELU / SiLU、Dropout、Residual</td>
<td>GPU</td>
<td>把连续逐元素算子合并，减少中间张量写回</td>
<td>减少 kernel launch 次数与显存读写</td>
<td>MLP block、前馈网络</td>
</tr>
<tr>
<td>Triton custom kernel</td>
<td>任意自定义算子或 fused operator</td>
<td>GPU</td>
<td>开发者手写 tile、访存布局与并行策略</td>
<td>在通用库缺少最优实现时获得定制性能</td>
<td>大模型训练、研究型性能优化、自定义融合</td>
</tr>
<tr>
<td>TensorRT generated kernel</td>
<td>部署图中的卷积、MatMul、激活、归一化、量化路径</td>
<td>NVIDIA GPU</td>
<td>图优化、层融合、低精度选择、kernel autotuning</td>
<td>显著降低推理时延并提升吞吐</td>
<td>在线推理服务、边缘推理</td>
</tr>
<tr>
<td>XLA generated kernel</td>
<td>JAX / TensorFlow 图中的可融合算子子图</td>
<td>GPU、TPU 等</td>
<td>图级融合、静态形状分析、编译生成目标代码</td>
<td>让一串算子整体下沉为更大的执行单元</td>
<td>JAX 训练、TPU 训练、编译型执行场景</td>
</tr>
<tr>
<td>TVM generated kernel</td>
<td>自定义张量表达式对应的算子</td>
<td>CPU、GPU、边缘加速器</td>
<td>自动调度、自动搜索、代码生成</td>
<td>跨异构硬件获得针对性实现</td>
<td>端侧部署、自定义芯片适配</td>
</tr>
<tr>
<td>PagedAttention / KV-cache kernel</td>
<td>增量解码中的 attention 与缓存访问</td>
<td>LLM 推理后端</td>
<td>分页管理 KV cache、优化随机访问和 batch 合并</td>
<td>提升长上下文与多请求并发推理效率</td>
<td>大语言模型在线推理</td>
</tr>
</tbody>
</table>
<p>理解内核时最重要的一点是：<span style="background-color: #c0c0c0;">一个算子并不对应唯一实现</span>。同样是卷积，可以选 direct convolution、Winograd 或 FFT；同样是 attention，可以选普通分步实现、FlashAttention 或推理场景下的 paged attention。真正的差异往往不在数学定义，而在访存方式、融合策略、并行粒度和硬件利用率上。</p>
<p>因此，内核并不是建模阶段最先暴露给用户的对象，却往往决定模型最终的吞吐、时延、显存占用、能耗和成本。模型结构决定“上限在哪里”，内核质量决定“这个上限能兑现多少”。</p>
<div class="blog_h3"><span class="graybg">三者关系</span></div>
<p>把三者串起来看，一个卷积层或注意力层的执行路径通常是这样的：开发者先在 PyTorch 或 JAX 中写出一个模块；框架把它拆成若干算子，例如 MatMul、Softmax、LayerNorm 或 Convolution；运行时再为每个算子选择对应硬件上的 kernel，例如 cuBLAS 的 GEMM kernel、cuDNN 的 convolution kernel，或 Triton 写成的自定义 fused kernel。整个训练与推理过程由框架负责调度，算子负责表达数学语义，内核负责把语义变成高性能机器代码。</p>
<p>可以用一个具体例子来理解。若在 PyTorch 中定义一个卷积层，那么：</p>
<ol>
<li>PyTorch 作为框架负责接收模块定义、组织张量、记录梯度关系并调度执行。</li>
<li>卷积（Convolution）作为算子表示“输入特征图与卷积核做局部加权求和”这一数学操作。</li>
<li>底层可能调用 cuDNN 提供的卷积 kernel，在 GPU 上以高度优化的方式完成真正计算。</li>
</ol>
<p>同样地，在 Transformer 中写一层自注意力时，框架会组织前向与反向图；MatMul、Softmax、Mask、LayerNorm 等作为算子组成计算链；而 FlashAttention、fused LayerNorm、paged attention 这类高性能实现，则属于内核或 kernel-level optimization 的范畴。</p>
<p>因此，这三个层次的关系可以概括为：<span style="background-color: #c0c0c0;">框架管理整个建模与执行流程，算子定义模型中的数学步骤，内核负责把这些步骤在具体硬件上高效落地</span>。从研究到工程落地的能力鸿沟，往往正体现在能否同时理解这三层。</p>
<div class="blog_h1"><span class="graybg">经典机器学习</span></div>
<div class="blog_h2"><span class="graybg">选型指南</span></div>
<p>经典机器学习的模型选择，本质上是在<span style="background-color: #c0c0c0;">任务形式、数据规模、特征形态、可解释性要求、训练与推断成本</span>之间做匹配。只要先判断“有没有标签”“输出是什么类型”“样本量有多大”“特征是不是稀疏或非线性”“是否需要概率或规则解释”，大多数任务都可以迅速缩小到少数几个候选模型。</p>
<p>本章中的模型并不是按“先进程度”排序，而是按建模假设来区分。线性模型假设边界或关系接近线性；树模型更擅长表格数据中的非线性交互；概率模型更强调分布解释；近邻模型依赖局部相似性；聚类与降维模型关注无监督结构；HMM（Hidden Markov Model, HMM）和条件随机场（Conditional Random Field, CRF）则处理序列标签之间存在依赖的结构化预测问题。</p>
<p>下面的表格不是概览式罗列，而是直接服务于选型。每一行都回答五个问题：<span style="background-color: #c0c0c0;">什么情况下优先选它、它最依赖什么数据条件、它能解决什么核心诉求、为什么它在该场景里合适、以及什么情况下应当换模型</span>。在大多数工程场景里，先按这些表格缩小范围，再进入具体模型细节，效率最高。</p>
<div class="blog_h3"><span class="graybg">任务类型：分类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>逻辑回归</td>
<td>需要稳定强基线、需要概率输出、需要解释每个特征如何影响类别</td>
<td>特征可以线性分离到一定程度；高维稀疏特征尤其合适，如 one-hot、词袋、统计特征</td>
<td>训练快、概率自然、权重可解释，适合作为第一版可上线模型</td>
<td>类别边界高度非线性、特征交互复杂且没有显式特征工程时</td>
</tr>
<tr>
<td>支持向量机（SVM）</td>
<td>样本中小规模、类别边界较清晰、希望用最大间隔提升泛化稳健性</td>
<td>特征需要做尺度统一；核 SVM 更适合中小规模，不适合超大样本</td>
<td>对边界样本建模强，在线性不可分但结构仍较规整时往往优于纯线性模型</td>
<td>数据量很大、需要快速训练与部署、或者必须输出天然概率时</td>
</tr>
<tr>
<td>决策树</td>
<td>需要把模型直接解释成规则路径，或业务天然是“按阈值分流”的形式</td>
<td>表格特征、混合数值与类别特征都可；不强依赖标准化</td>
<td>规则可视化最直接，便于和业务规则、审计规则对齐</td>
<td>追求最稳泛化性能时；单树通常方差高，容易过拟合</td>
</tr>
<tr>
<td>随机森林</td>
<td>表格分类需要稳健基线、希望少调参、担心单棵树过拟合</td>
<td>适合中等规模表格数据；对噪声、缺失和特征尺度通常更宽容</td>
<td>比单树稳定，通常先于复杂 boosting 模型给出可靠结果</td>
<td>追求表格任务的极致精度，或需要很小的模型体积时</td>
</tr>
<tr>
<td>梯度提升树（GBDT）</td>
<td>表格分类精度优先，特征中存在明显非线性与交互效应</td>
<td>适合结构化特征，不要求线性关系；通常需要一定调参</td>
<td>能逐轮修正前一轮错误，在中等规模表格任务上常是强力基线</td>
<td>需要极快训练、极少调参，或数据已经极大到串行 boosting 成本明显偏高时</td>
</tr>
<tr>
<td>XGBoost</td>
<td>工业表格分类、竞赛任务、缺失值与正则化处理都很重要</td>
<td>适合中大规模结构化数据；对特征工程和超参数较敏感，但工程支持成熟</td>
<td>精度高、鲁棒、缺失值处理成熟，是很多表格分类任务的首选之一</td>
<td>极端大规模、极度强调训练速度与内存效率时，LightGBM 往往更优先</td>
</tr>
<tr>
<td>LightGBM</td>
<td>大规模表格分类、高维稀疏特征、需要更快训练与更低内存消耗</td>
<td>适合样本量大、特征维度高的结构化任务；对类别特征和稀疏特征较友好</td>
<td>训练快、工程效率高，在工业 CTR、风控、推荐特征场景很常见</td>
<td>数据量较小且噪声较大时；叶子生长策略若不控制，容易过拟合</td>
</tr>
<tr>
<td>朴素贝叶斯</td>
<td>需要极快文本分类基线、小样本启动、希望先验证特征是否有判别力</td>
<td>最适合词袋、词频、计数类高维稀疏特征；默认接受条件独立近似</td>
<td>训练和推断都极快，在垃圾邮件、主题粗分类等任务上常有效</td>
<td>强依赖复杂特征相关性、类别边界非线性明显、或需要高精度概率校准时</td>
</tr>
<tr>
<td>K 近邻（KNN）</td>
<td>样本规模小、相似样本应有相似标签、希望不做显式训练</td>
<td>距离度量必须有意义；所有特征应标准化，且维度不能过高</td>
<td>局部模式直观，适合做小数据原型验证或相似样本检索式分类</td>
<td>高维稀疏特征、大规模数据、低延迟在线推断场景</td>
</tr>
<tr>
<td>线性判别分析（LDA）</td>
<td>有监督分类同时希望压缩特征维度，且类别统计结构较稳定</td>
<td>更适合每类近似高斯、类内协方差可估计的情况；样本数不能太少</td>
<td>把“降维”和“分类判别”结合起来，适合先压缩再分类的流程</td>
<td>类别分布非常复杂、强非高斯、强非线性时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：回归</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>线性回归</td>
<td>需要连续值预测、希望建立可解释、可审计、可稳定复现的基线</td>
<td>目标与特征大致满足线性关系，或经过变换后接近线性</td>
<td>系数解释直观，便于分析“哪个因素把目标推高或拉低”</td>
<td>目标关系明显呈现复杂分段、交互或强非线性时</td>
</tr>
<tr>
<td>Lasso（L1 正则化）</td>
<td>特征很多、怀疑只有少数特征真正有效、希望模型自动做变量筛选</td>
<td>高维特征尤其适合；允许部分权重被压到 0</td>
<td>回归同时完成特征选择，适合构建更稀疏、更简洁的模型</td>
<td>大量强相关特征共同起作用时；它可能只保留其中部分特征</td>
</tr>
<tr>
<td>岭回归 / L2 正则化</td>
<td>特征共线性明显、担心普通线性回归权重不稳定</td>
<td>适合多个相关特征共同解释目标，而不希望稀疏淘汰其中一部分</td>
<td>通过收缩权重降低方差，使模型在相关特征场景下更稳定</td>
<td>首要目标是做特征筛选、希望很多系数直接变成 0 时</td>
</tr>
<tr>
<td>Elastic Net（L1 + L2 正则化）</td>
<td>既想做特征选择，又不希望在相关特征组上过于不稳定</td>
<td>高维、相关特征并存的回归任务最常见</td>
<td>综合 Lasso 与岭回归的优点，在稀疏性和稳定性之间折中</td>
<td>特征数量不多、模型目标非常简单时；其调参成本高于纯 L1 或 L2</td>
</tr>
<tr>
<td>决策树</td>
<td>目标值与输入关系近似分段函数，或业务逻辑天然围绕阈值展开</td>
<td>表格特征、混合类型特征都可；不需要严格线性假设</td>
<td>能学出“满足什么条件时预测值跳到哪个区间”的规则</td>
<td>希望预测曲线平滑、稳定，或希望泛化误差尽可能低时</td>
</tr>
<tr>
<td>随机森林</td>
<td>希望回归稳健、抗噪声、少调参，先拿到可靠效果</td>
<td>表格回归场景最常见；对特征尺度和局部异常较稳</td>
<td>综合多个树模型平均结果，通常比单树更不容易过拟合</td>
<td>要求外推能力强，或希望模型极度轻量、延迟极低时</td>
</tr>
<tr>
<td>梯度提升树（GBDT）</td>
<td>表格回归精度优先，目标和特征之间存在复杂非线性</td>
<td>适合异构表格特征；对异常值和长尾目标通常也较有韧性</td>
<td>在房价、评分、收益、风险等典型表格回归中常是强基线</td>
<td>算力非常紧、需要极低训练成本时</td>
</tr>
<tr>
<td>XGBoost / LightGBM</td>
<td>工业级表格回归、大规模特征、希望兼顾精度与工程效率</td>
<td>适合结构化数据；XGBoost 更稳健成熟，LightGBM 更强调速度与规模</td>
<td>常作为表格回归默认候选，能直接处理大量非线性和特征交互</td>
<td>数据关系本来就简单线性、并且强依赖系数解释时</td>
</tr>
<tr>
<td>K 近邻（KNN）</td>
<td>局部相似样本的目标值应当接近，希望用邻域平均直接预测</td>
<td>小数据、低维、有意义的距离度量是前提</td>
<td>局部平滑性强时实现简单有效，适合作为相似样本回归基线</td>
<td>高维、大样本、分布稀疏或在线延迟敏感时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：聚类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>K-Means</td>
<td>需要快速把样本分成若干组，并且预期每组都围绕某个均值中心展开</td>
<td>簇大致是球形或凸形；欧氏距离有意义；需要先给定 <span displaypfx="inline-" class="mathjax-container">\(K\)</span></td>
<td>速度快、实现简单，适合作为无监督分群第一选择</td>
<td>簇形状复杂、密度差异大、离群点很多或无法预先估计簇数时</td>
</tr>
<tr>
<td>层次聚类</td>
<td>不仅想得到分组结果，还想知道各组是如何逐层合并成层级结构的</td>
<td>更适合中小规模数据；需要能接受 <span displaypfx="inline-" class="mathjax-container">\(O(N^2)\)</span> 级别距离矩阵成本</td>
<td>能输出树状图，适合做群体结构分析与多粒度解释</td>
<td>数据量很大、只关心最终聚类标签、不关心层级关系时</td>
</tr>
<tr>
<td>DBSCAN</td>
<td>希望识别任意形状簇，并把稀疏孤立点单独作为噪声剔除</td>
<td>密度尺度相对统一；距离度量有意义；参数 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 邻域半径和最小点数可估计</td>
<td>不需要预设簇数，能自然处理非凸簇和离群点</td>
<td>不同区域密度差异很大时；单一密度阈值难兼顾所有簇</td>
</tr>
<tr>
<td>HDBSCAN</td>
<td>簇的密度明显不一致，希望保留 DBSCAN 的密度思想但更自适应</td>
<td>数据存在多密度结构；仍需合理距离度量</td>
<td>比 DBSCAN 更能处理“有的簇很密、有的簇较松”的真实数据</td>
<td>只需要一个快速、简单、可复现的基础分群结果时</td>
</tr>
<tr>
<td>高斯混合模型（GMM）</td>
<td>希望得到软聚类结果，或者认为每个簇更像椭球形概率团块</td>
<td>连续特征较适合；默认每个簇可近似为一个高斯成分</td>
<td>不仅给簇标签，还给每个样本属于各簇的概率</td>
<td>簇形状非常复杂、非高斯、多峰结构难用少量高斯逼近时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：降维与可视化</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>主成分分析（PCA）</td>
<td>希望做线性降维、压缩冗余特征、去噪或为下游模型降成本</td>
<td>主要结构能由少数线性主方向解释；不依赖标签</td>
<td>保留最大方差方向，是最稳健、最常用的无监督降维起点</td>
<td>真正关心的是类别判别性而不是总体方差，或数据结构强非线性时</td>
</tr>
<tr>
<td>线性判别分析（LDA）</td>
<td>有标签并希望把不同类别拉开后再做分类或可视化</td>
<td>类别标签可靠；类内散度与类间散度都可稳定估计</td>
<td>直接围绕“可分性”找投影，比 PCA 更贴近分类目标</td>
<td>没有标签、类别边界强非线性、或类别数太少导致可降维空间有限时</td>
</tr>
<tr>
<td>t-SNE</td>
<td>想把高维嵌入压到二维或三维，只为看局部邻域是否形成簇</td>
<td>更适合可视化，不适合直接做可逆特征压缩</td>
<td>局部邻域展示能力强，适合分析表征是否把相似样本聚到一起</td>
<td>需要把降维结果直接送入生产模型，或需要保留严格全局距离关系时</td>
</tr>
<tr>
<td>UMAP</td>
<td>希望做更快的大规模可视化，兼顾局部结构与部分全局连通性</td>
<td>适合高维嵌入、文本向量、图表示等复杂表征</td>
<td>通常比 t-SNE 更快，也更容易保留大体流形结构</td>
<td>需要线性可解释主方向，或需要结果完全稳定可重复到坐标级别时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：异常检测</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>孤立森林（Isolation Forest）</td>
<td>通用表格异常检测，希望先得到一个稳健、扩展性好的异常分数</td>
<td>适合中大规模数据；不要求明确概率分布形式</td>
<td>通过随机切分隔离少数样本，通常是异常检测第一基线</td>
<td>异常定义依赖非常精细的局部密度差异时</td>
</tr>
<tr>
<td>局部异常因子（LOF）</td>
<td>异常并不是全局离群，而是“在本地邻域里显得稀疏”</td>
<td>距离度量必须合理；样本规模不宜过大</td>
<td>能发现那些整体位置不极端、但局部密度明显偏低的点</td>
<td>高维距离失真严重、或需要高吞吐在线检测时</td>
</tr>
<tr>
<td>单类支持向量机（One-Class SVM）</td>
<td>只有正常样本，目标是学习正常数据的封闭边界</td>
<td>特征需标准化；更适合中小规模；核方法对边界形状有帮助</td>
<td>适合“正常类定义清楚、异常类没有稳定样本”的场景</td>
<td>数据量很大、特征维度很高、参数难以稳定选择时</td>
</tr>
<tr>
<td>高斯混合模型（GMM）</td>
<td>希望把异常定义为低概率区域，并明确得到似然分数</td>
<td>连续数据更合适；分布能用若干高斯混合近似</td>
<td>异常判定有明确概率语义，适合风险评分和阈值分析</td>
<td>数据分布非常复杂、非高斯、或异常并不等价于低密度时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：序列标注与结构化预测</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>隐马尔可夫模型（HMM）</td>
<td>序列较短、转移规律明显、希望在较小数据和较强先验下完成基础序列建模</td>
<td>状态转移与观测发射假设大致成立；问题适合生成式描述</td>
<td>结构清晰、推断高效，适合作为经典序列建模入门和小规模基线</td>
<td>标签强依赖全局上下文、特征复杂、需要大量判别式特征时</td>
</tr>
<tr>
<td>条件随机场（CRF）</td>
<td>输出不是单点分类而是整条标签序列，并且相邻标签的合法性非常关键</td>
<td>一维链式序列最合适；上游特征或表示需要至少能提供较强局部证据</td>
<td>通过整体解码约束标签转移，使最终标签序列更一致、更符合任务结构</td>
<td>长距离语义主要由强表征模型决定、标签约束作用很弱，或任务更适合 span 建模时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：密度估计与概率建模</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>朴素贝叶斯</td>
<td>希望快速得到后验概率分类器，并接受条件独立近似</td>
<td>高维稀疏离散特征最合适，如文本词频、词出现计数</td>
<td>概率形式直接、估计简单，适合快速建模和在线系统</td>
<td>特征依赖结构复杂、需要精细表达联合分布时</td>
</tr>
<tr>
<td>高斯混合模型（GMM）</td>
<td>希望显式建模连续数据分布，或需要软聚类与密度估计统一完成</td>
<td>连续数据、多峰分布、可近似为若干高斯成分</td>
<td>每个样本都能得到各成分责任概率，解释和阈值分析都较自然</td>
<td>分布极端复杂、重尾严重、或高斯成分数难合理确定时</td>
</tr>
<tr>
<td>隐马尔可夫模型（HMM）</td>
<td>不仅要建模观测分布，还要建模隐藏状态在时间上的转移机制</td>
<td>观测是序列，且状态依赖主要体现在相邻时刻</td>
<td>把“序列生成机制”和“时序转移规律”统一到一个概率模型里</td>
<td>长程依赖很强、局部马尔可夫假设明显不成立时</td>
</tr>
</tbody>
</table>
<p>若只是要一个工程上可执行的默认起点，可以进一步压缩成如下规则：表格分类与回归优先从随机森林、GBDT、XGBoost、LightGBM 和线性模型里选；文本高维稀疏分类优先看逻辑回归和朴素贝叶斯；无监督分群先看 K-Means，再根据簇形状和噪声情况转向 DBSCAN、HDBSCAN 或 GMM；需要可视化时先区分是要线性压缩还是只要展示结构，再在 PCA、LDA、t-SNE、UMAP 中选择；涉及标签序列依赖时再进入 HMM 与 CRF。</p>
<div class="blog_h2"><span class="graybg">线性模型</span></div>
<p>线性模型（Linear Models）的核心不是“世界一定是线性的”，而是先用一个可解释、可优化、常常足够强的基线去刻画输入与输出的关系。很多复杂模型也可以看作“在线性读出层之前先做更强的特征变换”。</p>
<div class="blog_h3"><span class="graybg">线性回归</span></div>
<p>线性回归（Linear Regression）在回归任务中建模“输入特征的加权求和如何产生连续输出”。它的价值在于：<span style="background-color: #c0c0c0;">可解释</span>（权重直接对应特征影响）与<span style="background-color: #c0c0c0;">可优化</span>（凸问题，训练稳定）。</p>
<div class="blog_h4"><span class="graybg">模型与符号</span></div>
<p>给定训练集 <span displaypfx="inline-" class="mathjax-container">\(\{(\mathbf{x}_i,y_i)\}_{i=1}^{N}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\in\mathbb{R}^d\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的特征向量， <span displaypfx="inline-" class="mathjax-container">\(y_i\in\mathbb{R}\)</span> 是对应标签。线性回归假设单样本预测为：</p>
<span displaypfx="" class="mathjax-container">\[\hat y_i=\mathbf{w}^\top \mathbf{x}_i+b\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\hat y_i\)</span>：对 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 的预测。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\in\mathbb{R}^d\)</span>：权重向量（每个维度对应一个特征）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(b\in\mathbb{R}\)</span>：偏置（Bias），用于整体平移。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top \mathbf{x}_i\)</span>：内积（Dot Product），表示“按特征加权求和”。</li>
</ul>
<p>把全部样本写成矩阵形式。令设计矩阵（Design Matrix）<span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}\in\mathbb{R}^{N\times d}\)</span> 的第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 行为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\)</span>，标签向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\in\mathbb{R}^{N}\)</span> 的第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量为 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span>，全 1 向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{1}\in\mathbb{R}^{N}\)</span>。则：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\mathbf{y}}=\mathbf{X}\mathbf{w}+b\mathbf{1}\]</span>
<p>直觉类比：它像“按因素打分再加总”。例如房价预测里，面积、地段评分、楼龄都可作为特征；权重正负决定影响方向，绝对值大小决定影响强弱。</p>
<div class="blog_h4"><span class="graybg">目标函数（最小二乘）</span></div>
<p>最常见的训练目标是最小化均方误差（Mean Squared Error, MSE）的总和（或平均）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{EN}}(\mathbf{w},b)=\frac{1}{N}\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\|_2^2+\lambda_1\|\mathbf{w}\|_1+\lambda_2\|\mathbf{w}\|_2^2\]</span>
<p>这个目标可以拆成三部分理解：第一项 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{N}\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\|_2^2\)</span> 是数据拟合误差，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\)</span> 是所有样本的残差向量；第二项 <span displaypfx="inline-" class="mathjax-container">\(\lambda_1\|\mathbf{w}\|_1\)</span> 倾向把一部分权重直接压到 0；第三项 <span displaypfx="inline-" class="mathjax-container">\(\lambda_2\|\mathbf{w}\|_2^2\)</span> 倾向把权重整体缩小得更平滑。这里 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 是样本数， <span displaypfx="inline-" class="mathjax-container">\(\lambda_1,\lambda_2\)</span> 是正则化强度。若两者都取 0，就退化为普通最小二乘。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(J(\mathbf{w},b)\)</span>：目标函数（Objective）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_2\)</span>：二范数（Euclidean Norm），向量的长度。</li>
</ul>
<p>平方项会对大残差施加更大惩罚，因此训练会优先修正“偏得很离谱”的样本。并且在高斯噪声假设下，最小二乘等价于最大似然估计（Maximum Likelihood Estimation, MLE）导出的解。</p>
<div class="blog_h4"><span class="graybg">解析解（正规方程）</span></div>
<p>把偏置吸收到特征中更方便：定义增广特征 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{x}}_i=[\mathbf{x}_i;1]\in\mathbb{R}^{d+1}\)</span>，增广参数 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{w}}=[\mathbf{w};b]\in\mathbb{R}^{d+1}\)</span>，增广矩阵 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}=[\mathbf{X},\mathbf{1}]\in\mathbb{R}^{N\times(d+1)}\)</span>。则 <span displaypfx="inline-" class="mathjax-container">\(\hat{\mathbf{y}}=\tilde{\mathbf{X}}\tilde{\mathbf{w}}\)</span>，目标为 <span displaypfx="inline-" class="mathjax-container">\(\min_{\tilde{\mathbf{w}}}\|\mathbf{y}-\tilde{\mathbf{X}}\tilde{\mathbf{w}}\|_2^2\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\)</span> 可逆，则正规方程（Normal Equation）给出闭式解：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\mathbf{w}}^*=\left(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\right)^{-1}\tilde{\mathbf{X}}^\top\mathbf{y}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{w}}^*\)</span> 表示最优增广参数，前 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维对应原权重 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span>，最后一维对应偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>。 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\)</span> 汇总了特征之间的相关结构， <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\mathbf{y}\)</span> 汇总了特征与标签之间的相关程度，因此这个闭式解本质上是在“用整体相关关系一次性解出最优线性系数”。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\in\mathbb{R}^{(d+1)\times(d+1)}\)</span>：Gram 矩阵，度量特征之间的相关性。</li>
<li><span displaypfx="inline-" class="mathjax-container">\((\cdot)^{-1}\)</span>：矩阵逆；不可逆时通常用伪逆（Pseudo-inverse）或加正则化处理。</li>
</ul>
<p>工程实现中，直接求逆并不推荐；更稳定的做法是解线性方程组或用 QR/SVD 分解。</p>
<div class="blog_h4"><span class="graybg">实例：把公式算一遍</span></div>
<p>用一维数据拟合 <span displaypfx="inline-" class="mathjax-container">\(y\approx wx+b\)</span>。给定三点 <span displaypfx="inline-" class="mathjax-container">\((x,y)\in\{(0,1),(1,3),(2,5)\}\)</span>。构造增广矩阵与标签：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\mathbf{X}}=\begin{bmatrix}0 &amp; 1\\ 1 &amp; 1\\ 2 &amp; 1\end{bmatrix},\quad \mathbf{y}=\begin{bmatrix}1\\ 3\\ 5\end{bmatrix}\]</span>
<p>这个增广矩阵的每一行对应一个样本；第一列是原始特征 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，第二列固定为 1，用来把偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 并入矩阵乘法。标签向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 则把三个样本的真实输出按顺序堆叠起来。</p>
<p>计算：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}=\begin{bmatrix}5 &amp; 3\\ 3 &amp; 3\end{bmatrix},\quad \tilde{\mathbf{X}}^\top\mathbf{y}=\begin{bmatrix}13\\ 9\end{bmatrix}\]</span>
<p>左边的矩阵可以看成所有样本在“特征与偏置”两个方向上的二阶统计量： <span displaypfx="inline-" class="mathjax-container">\(5\)</span> 来自 <span displaypfx="inline-" class="mathjax-container">\(0^2+1^2+2^2\)</span>， <span displaypfx="inline-" class="mathjax-container">\(3\)</span> 来自 <span displaypfx="inline-" class="mathjax-container">\(0+1+2\)</span>，右边向量 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\mathbf{y}\)</span> 则分别对应 <span displaypfx="inline-" class="mathjax-container">\(\sum_i x_i y_i\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\sum_i y_i\)</span>。这正是正规方程所需要的汇总量。</p>
<p>解线性方程 <span displaypfx="inline-" class="mathjax-container">\(\left(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\right)\tilde{\mathbf{w}}=\tilde{\mathbf{X}}^\top\mathbf{y}\)</span>，即：</p>
<span displaypfx="" class="mathjax-container">\[\begin{cases}5w+3b=13\\ 3w+3b=9\end{cases}\Rightarrow w=2,\ b=1\]</span>
<p>这组方程的未知量只有两个：斜率 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 和偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>。解出 <span displaypfx="inline-" class="mathjax-container">\(w=2\)</span> 表示特征每增加 1，预测值增加 2；解出 <span displaypfx="inline-" class="mathjax-container">\(b=1\)</span> 表示当 <span displaypfx="inline-" class="mathjax-container">\(x=0\)</span> 时，模型基线输出为 1。</p>
<p>因此预测为 <span displaypfx="inline-" class="mathjax-container">\(\hat y=2x+1\)</span>，恰好穿过三点。这个例子展示了：正规方程把“最小化平方误差”的优化问题，转换成一个线性方程组。</p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>需要可解释的特征贡献（权重）与可校验的线性关系。</li>
<li>表格数据（Tabular）中，关系接近线性或可通过特征变换/交互项线性化。</li>
<li>作为强基线：先用线性模型定位数据问题、特征质量与噪声水平，再决定是否需要更复杂模型。</li>
<li>高维稀疏特征（如 one-hot、文本 bag-of-words）下，配合正则化可获得稳定解。</li>
</ul>
<p>当特征很多、共线性（Collinearity）强或数据量相对不足时，普通最小二乘（Ordinary Least Squares, OLS）会出现高方差：训练集拟合很好、验证集误差上升。正则化（Regularization）通过惩罚参数规模，把“拟合训练误差”与“控制模型复杂度”写进同一个目标函数。</p>
<div class="blog_h3"><span class="graybg">Lasso（L1 正则化）</span></div>
<p>Lasso 把参数惩罚写成一范数（<span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> Norm）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{lasso}}(\mathbf{w},b)=\frac{1}{N}\left\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\right\|_2^2+\lambda\|\mathbf{w}\|_1\]</span>
<p>这个式子里，前半部分仍然是拟合误差，衡量预测和真实标签之间差多少；后半部分 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\mathbf{w}\|_1\)</span> 是稀疏惩罚，其中 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_1=\sum_j |w_j|\)</span> 会优先把不重要的权重压成 0。于是 Lasso 不只是“把权重变小”，而是经常顺带完成特征选择。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>：正则化强度（Regularization Strength）。越大表示越强的收缩。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_1=\sum_{j=1}^{d}|w_j|\)</span>：一范数，鼓励稀疏（Sparsity）。</li>
<li>通常不惩罚偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>；否则会把整体均值也一起往 0 拉。</li>
<li>Lasso 由于 <span displaypfx="inline-" class="mathjax-container">\(|\cdot|\)</span> 在 0 点不可导，一般没有像岭回归那样简洁的闭式解；常用坐标下降（Coordinate Descent）、近端梯度（Proximal Gradient）或 LARS 等算法求解。</li>
</ul>
<p>Lasso 的关键作用是让一部分权重被压到精确 0，而不是均匀缩小全部权重。它更像给参数设置了一个阈值：弱相关、贡献不足以抵消惩罚的特征，会被直接剔除。因此 Lasso 同时完成复杂度控制与特征选择（Feature Selection）。</p>
<p>几何上，在相同训练误差轮廓线下， <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 约束对应菱形/正八面体；这些尖角更容易与最优点相交在坐标轴上，因此常出现某些 <span displaypfx="inline-" class="mathjax-container">\(w_j=0\)</span> 的解。</p>
<ul>
<li>适合高维稀疏特征、希望自动筛特征的场景，例如广告、推荐、文本 one-hot 特征。</li>
<li>当特征高度相关时，Lasso 往往只保留其中少数几个，因此解的稳定性通常弱于岭回归。</li>
</ul>
<div class="blog_h3"><span class="graybg">岭回归 / L2 正则化</span></div>
<p>岭回归把参数惩罚写成二范数平方（<span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> Norm Squared）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{ridge}}(\mathbf{w},b)=\frac{1}{N}\left\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\right\|_2^2+\lambda\|\mathbf{w}\|_2^2\]</span>
<p>岭回归的结构与普通线性回归相同，只是在误差项之外额外加入了 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\mathbf{w}\|_2^2\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2^2=\sum_j w_j^2\)</span> 会连续惩罚过大的权重，因此它更擅长缓解共线性和过拟合，而不是像 Lasso 那样主动做稀疏选择。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>：正则化强度。越大表示越强的收缩。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2^2=\sum_{j=1}^{d}w_j^2\)</span>：二范数平方，连续惩罚大权重。</li>
<li>通常不惩罚偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>；否则会把整体均值也一起往 0 拉。</li>
</ul>
<p>从梯度角度看，惩罚项 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\mathbf{w}\|_2^2\)</span> 对单个参数 <span displaypfx="inline-" class="mathjax-container">\(w_j\)</span> 的梯度贡献是 <span displaypfx="inline-" class="mathjax-container">\(2\lambda w_j\)</span>。这就是 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则化在深度学习中被称为权重衰减（Weight Decay）的原因：它持续把权重按比例拉回 0。若取 <span displaypfx="inline-" class="mathjax-container">\(\lambda=1\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(w_j=10\)</span> 时，梯度为 <span displaypfx="inline-" class="mathjax-container">\(20\)</span>；优化器会施加强烈收缩，把这个大权重猛拉回去。当 <span displaypfx="inline-" class="mathjax-container">\(w_j=0.1\)</span> 时，梯度只有 <span displaypfx="inline-" class="mathjax-container">\(0.2\)</span>；往 0 拉的力量就很弱。也就是说，参数一旦变大， <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 惩罚立刻增强；参数已经很小时，它几乎不再干预。</p>
<p>岭回归仍是凸二次问题，并有闭式解（忽略/已中心化处理 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 的情况）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}^*=\left(\mathbf{X}^\top\mathbf{X}+N\lambda\mathbf{I}\right)^{-1}\mathbf{X}^\top\mathbf{y}\]</span>
<p>与普通最小二乘相比，这里多出来的 <span displaypfx="inline-" class="mathjax-container">\(N\lambda\mathbf{I}\)</span> 是沿对角线加入的一项稳定化修正。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{I}\)</span> 是单位矩阵，它不会改变特征之间的相对结构，但会让矩阵更容易求逆，因此在特征高度相关时更稳定。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{I}\in\mathbb{R}^{d\times d}\)</span>：单位矩阵（Identity Matrix）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}^\top\mathbf{X}+N\lambda\mathbf{I}\)</span>：对角线上加了 <span displaypfx="inline-" class="mathjax-container">\(N\lambda\)</span> 的稳定项，缓解共线性导致的病态（Ill-conditioning）。</li>
</ul>
<p>用一个最小可算的例子说明 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 如何改变解。考虑无偏置的一维回归 <span displaypfx="inline-" class="mathjax-container">\(\hat y=wx\)</span>，目标为：</p>
<span displaypfx="" class="mathjax-container">\[J(w)=\sum_{i=1}^{N}(y_i-wx_i)^2+\lambda w^2\]</span>
<p>这是单变量岭回归的简化形式。前一项把所有样本的平方误差加总，后一项 <span displaypfx="inline-" class="mathjax-container">\(\lambda w^2\)</span> 惩罚过大的斜率。它清楚展示了岭回归的核心：既要求拟合数据，又限制参数不要长得太大。</p>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 求导并令其为 0 得：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\mathrm{d}J}{\mathrm{d}w}=-2\sum_{i=1}^{N}x_i(y_i-wx_i)+2\lambda w=0\Rightarrow w^*=\frac{\sum_{i=1}^{N}x_i y_i}{\sum_{i=1}^{N}x_i^2+\lambda}\]</span>
<p>这个结果说明岭回归解和普通最小二乘解非常接近，只是分母里多了一个 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span>。它会把估计值往 0 方向收缩： <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越大，分母越大，得到的 <span displaypfx="inline-" class="mathjax-container">\(w^*\)</span> 就越保守。</p>
<p>可见 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 直接进入分母，把 <span displaypfx="inline-" class="mathjax-container">\(w^*\)</span> 持续拉向 0。岭回归不会像 Lasso 那样大量制造精确 0，而是把所有权重更平滑地压小，因此更像“把所有旋钮都往小一点拧”，让模型整体更保守、更稳健。</p>
<ul>
<li>适合特征高度相关、希望保留全部特征但降低方差的场景；常见于经济学、医学和一般表格数据。</li>
<li>当既希望稀疏，又希望在强相关特征间保持稳定时，Elastic Net（<span displaypfx="inline-" class="mathjax-container">\(L_1+L_2\)</span>）通常比纯 Lasso 更稳。</li>
</ul>
<div class="blog_h3"><span class="graybg">Elastic Net（L1 + L2 正则化）</span></div>
<p>Elastic Net 把 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 惩罚合并到同一个目标中：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{EN}}(\mathbf{w},b)=\frac{1}{N}\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\|_2^2+\lambda_1\|\mathbf{w}\|_1+\lambda_2\|\mathbf{w}\|_2^2\]</span>
<p>它同时保留两类效应： <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 项负责产生稀疏性（Sparsity），把一部分弱特征压到 0； <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 项负责平滑收缩，在特征高度相关时提高解的稳定性。因此 Elastic Net 通常用于“既希望自动做特征选择，又不希望在强相关特征之间选得过于激进”的场景。</p>
<ul>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(\lambda_2=0\)</span> 时，退化为 Lasso。</li>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(\lambda_1=0\)</span> 时，退化为岭回归（Ridge Regression）。</li>
<li>当特征高度相关且维度很高时，Elastic Net 往往比纯 Lasso 更稳，也比纯岭回归更稀疏。</li>
</ul>
<div class="blog_h3"><span class="graybg">逻辑回归</span></div>
<p>逻辑回归（Logistic Regression）是二分类的标准基线：它先用线性函数产生打分，再把该打分通过 sigmoid（Logistic Function）映射到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>，从而得到条件概率模型。</p>
<div class="blog_h4"><span class="graybg">模型、概率与 logit</span></div>
<p>对二分类，令标签随机变量（Random Variable）<span displaypfx="inline-" class="mathjax-container">\(Y\in\{0,1\}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(1\)</span> 表示正类， <span displaypfx="inline-" class="mathjax-container">\(0\)</span> 表示负类。给定输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 后，先定义线性打分：</p>
<span displaypfx="" class="mathjax-container">\[z=\mathbf{w}^\top\mathbf{x}+b\]</span>
<p>再定义 sigmoid 函数：</p>
<span displaypfx="" class="mathjax-container">\[\sigma(z)=\frac{1}{1+e^{-z}}\]</span>
<p>于是，在给定输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的条件下，标签取正类的条件概率写成：</p>
<span displaypfx="" class="mathjax-container">\[p(Y=1\mid \mathbf{x})=\sigma(z)=\sigma(\mathbf{w}^\top\mathbf{x}+b)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mid\)</span> 表示“在给定……条件下”；左侧 <span displaypfx="inline-" class="mathjax-container">\(Y=1\)</span> 表示事件“标签随机变量取值为正类 1”。因此这个式子表示：在输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 已知时，事件 <span displaypfx="inline-" class="mathjax-container">\(Y=1\)</span> 发生的概率。</p>
<p>相应地，负类概率为：</p>
<span displaypfx="" class="mathjax-container">\[p(Y=0\mid \mathbf{x})=1-\sigma(z)\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\in\mathbb{R}^d\)</span>：特征向量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\in\mathbb{R}^d\)</span>：权重向量（Weight Vector）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(b\in\mathbb{R}\)</span>：偏置（Bias）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}\)</span>：线性打分；它也是 logit（对数几率，log-odds）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sigma(\cdot)\)</span>：把任意实数映射到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 的非线性函数。</li>
</ul>
<p>logit 的含义来自恒等式：</p>
<span displaypfx="" class="mathjax-container">\[\log\frac{p(Y=1\mid \mathbf{x})}{1-p(Y=1\mid \mathbf{x})}=z=\mathbf{w}^\top\mathbf{x}+b\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(\frac{p(Y=1\mid \mathbf{x})}{1-p(Y=1\mid \mathbf{x})}\)</span> 称为几率（Odds），表示“正类概率与负类概率之比”；再对它取对数，就得到 logit（对数几率，log-odds）：<span displaypfx="inline-" class="mathjax-container">\(\log\frac{p}{1-p}\)</span>。因此上式的含义不是再引入一个无关的新量，而是说明：逻辑回归把<span style="background-color: #c0c0c0;">正类概率的对数几率</span>建模为输入特征的线性函数。</p>
<p>换言之，在二分类逻辑回归里， <span displaypfx="inline-" class="mathjax-container">\(z=\mathbf{w}^\top\mathbf{x}+b\)</span> 就是线性部分的原始输出值，而这个原始输出值恰好等于 logit。它本身不是概率，可以取任意实数；经过 sigmoid 之后才变成 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 内的概率。多分类情形中，softmax 之前那一组线性输出通常统称为 logits。</p>
<p>因此 <span displaypfx="inline-" class="mathjax-container">\(w_j\)</span> 可以被解释为：特征 <span displaypfx="inline-" class="mathjax-container">\(x_j\)</span> 增加一个单位，会把对数几率增加 <span displaypfx="inline-" class="mathjax-container">\(w_j\)</span>（在其他特征不变时）。这就是逻辑回归的核心可解释性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/logistic.jpg"><img class="alignnone size-full wp-image-41171" src="https://blog.gmem.cc/wp-content/uploads/2026/03/logistic.jpg" alt="logistic" width="1216" height="874" /></a></p>
<div class="blog_h4"><span class="graybg">训练目标（NLL / Cross-Entropy）</span></div>
<p>给定训练集 <span displaypfx="inline-" class="mathjax-container">\(\{(\mathbf{x}_i,y_i)\}_{i=1}^{N}\)</span>，记 <span displaypfx="inline-" class="mathjax-container">\(p_i=\sigma(\mathbf{w}^\top\mathbf{x}_i+b)\)</span>。逻辑回归通过最大化似然（Likelihood）训练；等价地，它最小化负对数似然（Negative Log-Likelihood, NLL），也即二分类交叉熵（Binary Cross-Entropy）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ L(\mathbf{w},b)=-\sum_{i=1}^{N}\left(y_i\log p_i+(1-y_i)\log(1-p_i)\right)\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\log p_i\)</span>：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=1\)</span> 时，鼓励 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 变大。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\log(1-p_i)\)</span>：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=0\)</span> 时，鼓励 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 变小。</li>
</ul>
<p>加入 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则化时，常见形式为 <span displaypfx="inline-" class="mathjax-container">\(L(\mathbf{w},b)+\lambda\|\mathbf{w}\|_2^2\)</span>（通常不惩罚 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>）。该目标是凸的，因此不存在“坏局部最优”的训练不稳定问题。</p>
<div class="blog_h4"><span class="graybg">梯度：公式如何驱动参数更新</span></div>
<p>把样本堆叠成矩阵 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}\)</span>、标签向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span>，预测概率向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{p}\)</span>（第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量为 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span>）。则无正则项时梯度为：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_{\mathbf{w}}L=\mathbf{X}^\top(\mathbf{p}-\mathbf{y}),\quad \frac{\partial L}{\partial b}=\mathbf{1}^\top(\mathbf{p}-\mathbf{y})\]</span>
<p>这两个梯度式子都围绕同一个误差向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{p}-\mathbf{y}\)</span> 展开：它表示“模型给出的正类概率”和“真实标签”之间的偏差。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}^\top(\mathbf{p}-\mathbf{y})\)</span> 表示把这种偏差按特征方向汇总起来，从而告诉每个权重该往哪个方向改； <span displaypfx="inline-" class="mathjax-container">\(\mathbf{1}^\top(\mathbf{p}-\mathbf{y})\)</span> 则把所有偏差直接相加，用来更新偏置。</p>
<p>这两个式子揭示了训练机制：如果某个样本真实标签是 1 但模型给出小概率（<span displaypfx="inline-" class="mathjax-container">\(p_i-y_i&lt;0\)</span>），则梯度会推动 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 朝着增加该样本 logit 的方向更新；反之亦然。</p>
<div class="blog_h4"><span class="graybg">实例：单样本算概率、算损失、算一次梯度</span></div>
<p>考虑一维特征 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>，参数 <span displaypfx="inline-" class="mathjax-container">\(w=1,b=-1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(z=wx+b=1\)</span>，概率：</p>
<span displaypfx="" class="mathjax-container">\[p=\sigma(1)=\frac{1}{1+e^{-1}}\approx 0.731\]</span>
<p>若真实标签 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span>，该样本的负对数似然为：</p>
<span displaypfx="" class="mathjax-container">\[\ell=-\log p\approx 0.313\]</span>
<p>该样本对 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 的梯度为（单样本形式）：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \ell}{\partial w}=(p-y)x=(0.731-1)\cdot 2\approx -0.538\]</span>
<p>用学习率 <span displaypfx="inline-" class="mathjax-container">\(\eta\)</span> 做一次梯度下降： <span displaypfx="inline-" class="mathjax-container">\(w\leftarrow w-\eta\frac{\partial\ell}{\partial w}\)</span>。由于梯度为负，更新会把 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 增大，从而增大 <span displaypfx="inline-" class="mathjax-container">\(z\)</span>、提升 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，使模型更倾向把该样本判为正类。</p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>二分类且需要概率输出/可校准阈值（例如欺诈检测、流失预测、医学风险评分）。</li>
<li>高维稀疏特征（如 one-hot、文本 bag-of-words），逻辑回归常是强基线。</li>
<li>对可解释性、训练稳定性要求高的工程场景（可用权重做审计/特征诊断）。</li>
<li>当决策边界高度非线性且特征工程不足时，需要树模型或神经网络补上非线性。</li>
</ul>
<div class="blog_h3"><span class="graybg">支持向量机（SVM）</span></div>
<p>支持向量机（Support Vector Machine, SVM）是一类以<span style="background-color: #c0c0c0;">最大间隔分类</span>为核心原则的判别模型。对线性可分的二分类问题，它要寻找一个超平面（Hyperplane），使两类样本被正确分开，同时边界到两侧样本的最小距离尽可能大。这个最小距离对应的缓冲区称为间隔（Margin）。</p>
<p>也正因为优化目标是最大间隔，SVM 并不是“随便找一条能分开两类样本的直线/超平面”，而是要找那条对两类样本都留出最大安全缓冲区的边界。边界离两类样本都越远，模型对噪声、标注扰动与局部数据波动通常越稳。</p>
<p>从数学形式看，SVM 最终会落到二次规划（Quadratic Programming, QP）。所谓二次规划，就是：<span style="background-color: #c0c0c0;">目标函数是变量的二次函数，约束是线性等式或线性不等式</span>。SVM 的目标是最小化 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\mathbf{w}\|_2^2\)</span>，约束则是每个样本都必须被放到正确一侧并留出至少 1 的函数间隔，因此它正好属于这一类优化问题。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/svm.jpg"><img class="alignnone size-full wp-image-41179" src="https://blog.gmem.cc/wp-content/uploads/2026/03/svm.jpg" alt="svm" width="1408" height="768" /></a></p>
<p>从结构上看，SVM 把三件事连在一起：</p>
<ol>
<li>几何：先定义“什么叫分得开”，以及“什么叫分得最稳”。</li>
<li>优化：把“最稳的边界”写成一个可求解的凸二次规划。</li>
<li>对偶：把问题改写成只依赖样本内积的形式，从而自然导出核技巧（Kernel Trick）。</li>
</ol>
<div class="blog_h4"><span class="graybg">从几何直觉到“最大间隔”</span></div>
<p>对二分类任务，设标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{-1,+1\}\)</span>。线性分类器先计算一个打分函数（Score Function）：</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathbf{w}^\top\mathbf{x}+b\]</span>
<p>这里最好先把“函数写法”和“几何边界写法”区分清楚。像 <span displaypfx="inline-" class="mathjax-container">\(y=x\)</span> 这样的形式，强调的是“给定自变量（Independent Variable） <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，应变量（Dependent Variable） <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 如何变化”；但对分类器来说，更关键的问题不是“谁依赖谁”，而是<span style="background-color: #c0c0c0;">哪些点恰好落在边界上，以及点位于边界哪一侧</span>。因此同一条直线在几何里通常改写成隐式形式（Implicit Form） <span displaypfx="inline-" class="mathjax-container">\(x-y=0\)</span>。</p>
<p>一旦写成 <span displaypfx="inline-" class="mathjax-container">\(x-y=0\)</span>，就能直接看成二维超平面方程 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span>：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x,y)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=(1,-1)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(b=0\)</span>，便有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=x-y\)</span>。此时直线方向向量（Direction Vector）可以取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}=(1,1)^\top\)</span>，因为沿这条线移动时 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 同时增加；而 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{v}=1\cdot 1+(-1)\cdot 1=0\)</span>，说明 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 与直线切向方向正交，所以它正是这条直线的法向量（Normal Vector）。更一般地，对任意边界 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span>，系数向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 都垂直于边界，因此天然决定“正侧、负侧”和距离的度量方向。</p>
<p>再用它的符号做判别：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\mathrm{sign}(f(\mathbf{x}))=\mathrm{sign}(\mathbf{w}^\top\mathbf{x}+b)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 是超平面的法向量（Normal Vector）。沿 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 方向移动， <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})\)</span> 会增大；沿反方向移动， <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})\)</span> 会减小。因此：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})=0\)</span>：点就在分类超平面上。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})&gt;0\)</span>：点落在法向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 指向的那一侧。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})&lt;0\)</span>：点落在另一侧。</li>
</ul>
<p>超平面把空间分成正半空间（Positive Half-space）与负半空间（Negative Half-space）；分数的正负号直接给出样本位于哪一侧。</p>
<p>令 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}=\{\mathbf{x}:\mathbf{w}^\top\mathbf{x}+b=0\}\)</span> 表示超平面，令单位法向量（Unit Normal Vector）为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{n}=\mathbf{w}/\|\mathbf{w}\|_2\)</span>。对样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\)</span>，记 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_{\Pi,i}\)</span> 为它在超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span> 上的正交投影点（Orthogonal Projection），因此 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_{\Pi,i}+b=0\)</span>，且 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i-\mathbf{x}_{\Pi,i}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 平行。点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\)</span> 到超平面的带符号距离（Signed Distance）定义为该位移在单位法向量方向上的投影长度：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}_{\pm}(\mathbf{x}_i,\mathcal{H})=\mathbf{n}^\top(\mathbf{x}_i-\mathbf{x}_{\Pi,i})=\frac{\mathbf{w}^\top\mathbf{x}_i+b}{\|\mathbf{w}\|_2}\]</span>
<p>分子 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b\)</span> 衡量点在法向量方向上偏离超平面的代数量；分母 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2\)</span> 对法向量做归一化，去掉“同一个超平面可写成不同倍数方程”的尺度影响；符号保留样本位于超平面哪一侧的信息。</p>
<p>带符号距离区分了超平面的两侧，但正类样本与负类样本的正确侧相反。将标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{-1,+1\}\)</span> 乘入后，得到几何间隔（Geometric Margin）：</p>
<span displaypfx="" class="mathjax-container">\[\gamma_i=\frac{y_i(\mathbf{w}^\top\mathbf{x}_i+b)}{\|\mathbf{w}\|_2}\]</span>
<p>几何间隔是<span style="background-color: #c0c0c0;">用标签修正后的带符号距离</span>。因此：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\gamma_i&gt;0\)</span>：样本在正确一侧，被正确分类。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\gamma_i=0\)</span>：样本正好压在边界上。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\gamma_i&lt;0\)</span>：样本落到错误一侧，被误分类。</li>
</ul>
<p>SVM 的目标是让所有训练样本中最小的那个 <span displaypfx="inline-" class="mathjax-container">\(\gamma_i\)</span> 尽可能大，即最大化“最坏样本到边界的正确方向距离”：</p>
<span displaypfx="" class="mathjax-container">\[\gamma=\min_i \gamma_i\]</span>
<p>这就是最大间隔（Maximum Margin）的含义：不是让“平均样本”离边界远，而是让最危险、最靠近边界的样本也尽量安全。</p>
<div class="blog_h4"><span class="graybg">硬间隔 SVM（Hard-margin SVM）原始问题</span></div>
<p>先看线性可分（Linearly Separable）的情形。所谓线性可分，就是存在某个 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{w},b)\)</span>，使每个样本都在与自己标签一致的一侧。这件事可以统一写成：</p>
<span displaypfx="" class="mathjax-container">\[y_i(\mathbf{w}^\top\mathbf{x}_i+b)&gt;0,\quad \forall i\]</span>
<p>为什么这里是 <span displaypfx="inline-" class="mathjax-container">\(&gt;0\)</span>？因为它等价于“符号一致”：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=+1\)</span> 时，要求 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b&gt;0\)</span>，即正类样本必须落在正半空间；当 <span displaypfx="inline-" class="mathjax-container">\(y_i=-1\)</span> 时，要求 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b&lt;0\)</span>，即负类样本必须落在负半空间。若只等于 0，则样本恰好压在分类边界上，不属于严格可分，因为此时它没有任何安全间隔，符号判别也处在临界点。</p>
<p>不过， <span displaypfx="inline-" class="mathjax-container">\(y_i(\mathbf{w}^\top\mathbf{x}_i+b)\)</span> 还不是几何距离，因为把 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{w},b)\)</span> 同时乘以任意正常数 <span displaypfx="inline-" class="mathjax-container">\(t&gt;0\)</span>，超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span> 完全不变，但这个量会整体乘上 <span displaypfx="inline-" class="mathjax-container">\(t\)</span>。因此它只是一个未归一化的间隔量，通常称为函数间隔（Functional Margin）。</p>
<p>为了消掉这个缩放自由度，SVM 采用一个标准定标：强制所有样本的最小函数间隔等于 1，也就是要求</p>
<span displaypfx="" class="mathjax-container">\[y_i(\mathbf{w}^\top\mathbf{x}_i+b)\ge 1,\quad \forall i\]</span>
<p>这条约束把两类样本同时编码进来：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=+1\)</span> 时，它变成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b\ge 1\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(y_i=-1\)</span> 时，它变成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b\le -1\)</span>。于是分类边界两侧又出现两条平行的“间隔边界”（Margin Boundaries）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}^\top\mathbf{x}+b=+1,\quad \mathbf{w}^\top\mathbf{x}+b=-1\]</span>
<p>因为平行超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=c_1\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=c_2\)</span> 之间的距离是 <span displaypfx="inline-" class="mathjax-container">\(|c_1-c_2|/\|\mathbf{w}\|_2\)</span>，所以这两条间隔边界之间的宽度为</p>
<span displaypfx="" class="mathjax-container">\[\frac{|(+1)-(-1)|}{\|\mathbf{w}\|_2}=\frac{2}{\|\mathbf{w}\|_2}\]</span>
<p>在这个定标下，离分类边界最近的样本几何间隔恰好是</p>
<span displaypfx="" class="mathjax-container">\[\gamma=\min_i \frac{y_i(\mathbf{w}^\top\mathbf{x}_i+b)}{\|\mathbf{w}\|_2}=\frac{1}{\|\mathbf{w}\|_2}\]</span>
<p>因此，最大化几何间隔就等价于最小化 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2\)</span>；为了得到标准的凸二次目标，通常写成：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2\quad \text{s.t.}\quad y_i(\mathbf{w}^\top \mathbf{x}_i+b)\ge 1\]</span>
<p>这就是硬间隔 SVM 的原始问题（Primal Problem）。现在“二次规划”这个术语也具体了：目标函数 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\mathbf{w}\|_2^2\)</span> 是关于参数的凸二次函数，而约束 <span displaypfx="inline-" class="mathjax-container">\(y_i(\mathbf{w}^\top \mathbf{x}_i+b)\ge 1\)</span> 对 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{w},b)\)</span> 是线性的，所以这是一个凸二次规划，并且可以求到全局最优解。</p>
<div class="blog_h4"><span class="graybg">拉格朗日函数与 KKT：为什么只剩少数“支持向量”</span></div>
<p>这一段只回答一个问题：训练集中明明有很多样本，为什么最后真正决定分类边界的，往往只有少数几个点。这个结论的严格来源是 KKT 条件，但它的直观含义并不抽象：<span style="background-color: #c0c0c0;">只有那些真正把最大间隔边界“卡住”的样本，才会在最优解里留下非零权重</span>。</p>
<p>硬间隔 SVM 的原始问题是：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\boldsymbol{w},b}\frac{1}{2}\|\boldsymbol{w}\|_2^2\quad \text{s.t.}\quad y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)\ge 1,\ \forall i\]</span>
<p>这里目标函数 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\boldsymbol{w}\|_2^2\)</span> 想把边界做得尽量“简单”，也就是让法向量尽量短，从而把间隔做大；而每个训练样本都在提出自己的硬约束：它不仅要被分对，还必须距离边界至少有 1 个单位的函数间隔。把这两股力量写到同一个式子里，就得到拉格朗日函数（Lagrangian）：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(\boldsymbol{w},b,\boldsymbol{\alpha})=\frac{1}{2}\|\boldsymbol{w}\|_2^2-\sum_{i=1}^{N}\alpha_i\big(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1\big),\quad \alpha_i\ge 0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本对应的拉格朗日乘子（Lagrange Multiplier）。在 SVM 里，可以把它直接理解为“第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本对当前分类边界施加了多大压力”。压力越大，说明这个样本越在真正影响边界的位置；压力为 0，说明这个样本虽然在训练集里，但最优边界并不需要它来支撑。</p>
<p>这个问题的读法可以想成一个“边界往外推、样本往回顶”的平衡过程：</p>
<ul>
<li><span style="background-color: #c0c0c0;">模型一侧</span> 想让 <span displaypfx="inline-" class="mathjax-container">\(\|\boldsymbol{w}\|_2\)</span> 尽量小，从而把间隔尽量做大。</li>
<li><span style="background-color: #c0c0c0;">约束一侧</span> 则由每个样本通过 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 施加压力：谁越接近边界、越可能破坏间隔，谁就越值得保留权重。</li>
</ul>
<p>于是，离边界很远的样本会发生什么，就变得非常直观。若某个样本满足</p>
<span displaypfx="" class="mathjax-container">\[y_i(\boldsymbol{w}^\top\boldsymbol{x}_i+b)&gt;1\]</span>
<p>说明它不仅分对了，而且还有额外安全余量。这个点对“边界能否继续外推”没有形成真正阻碍，因此最优时它对应的 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 会被压到 0。相反，若某个样本恰好满足</p>
<span displaypfx="" class="mathjax-container">\[y_i(\boldsymbol{w}^\top\boldsymbol{x}_i+b)=1\]</span>
<p>它就正贴在间隔边界上，是“再往外推一点就会出问题”的临界点。这样的样本才有资格在最优解中保留非零权重。</p>
<p>KKT 条件（Karush–Kuhn–Tucker Conditions）把这个直觉写成严格公式。第一条来自驻点条件（Stationarity）：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_{\boldsymbol{w}}\mathcal{L}=0\Rightarrow \boldsymbol{w}=\sum_{i=1}^{N}\alpha_i y_i \boldsymbol{x}_i\]</span>
<p>这条式子的含义非常重要。它说明最终的法向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 并不是由全部样本平均决定的，而是由训练样本的加权和决定的。这里：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个训练样本；</li>
<li><span displaypfx="inline-" class="mathjax-container">\(y_i\alpha_i\)</span> 是它的“带符号权重”；</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i=0\)</span>，该样本就完全不会出现在 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 的表达式里。</li>
</ul>
<p>第二条关键条件是互补松弛（Complementary Slackness）：</p>
<span displaypfx="" class="mathjax-container">\[\alpha_i\big(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1\big)=0\]</span>
<p>这条式子可以直接逐项阅读：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本对边界施加的压力；</li>
<li><span displaypfx="inline-" class="mathjax-container">\(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1\)</span> 是该样本相对于间隔边界的“松弛量”；当它大于 0 时，说明样本在安全区里；当它等于 0 时，说明样本正好贴边。</li>
</ul>
<p>由于这两个量的乘积必须等于 0，所以只可能出现两种情况：</p>
<ul>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1&gt;0\)</span>，说明该样本有安全余量，那么必须有 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i=0\)</span>。</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i&gt;0\)</span>，说明该样本仍在对边界施压，那么必须有 <span displaypfx="inline-" class="mathjax-container">\(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)=1\)</span>。</li>
</ul>
<p>这就是支持向量（Support Vector）的严格定义来源：<span style="background-color: #c0c0c0;">在硬间隔 SVM 中，只有恰好贴在间隔边界上的样本，才可能对应非零 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>；这些样本共同决定最终超平面的位置</span>。其余样本虽然被正确分类，但因为离边界还有余量，所以在最优解里不再起作用。</p>
<p>如果继续沿着这个结论往下看，就会发现 SVM 的稀疏性完全不是偶然现象，而是 KKT 的直接产物。训练集里可以有大量“安全样本”，但最优边界只需要被少数临界样本支撑起来。名字“支持向量”说的正是这件事：这些点不是普通数据点，而是真正在几何上把边界撑住的点。</p>
<div class="blog_h4"><span class="graybg">对偶问题（Dual）：把“求边界”改写成“给样本分权重”</span></div>
<p>前面已经看到：KKT 让 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 变成了“谁在真正顶住边界”的刻度。但如果这里只停在 KKT，还是会留下一个疑问：<span style="background-color: #c0c0c0;">对偶问题到底是怎么从原始问题里长出来的</span>？关键动作只有一步：先把 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 固定住，把拉格朗日函数当成关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 的函数来最小化；然后再回过头，只对 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 做最大化。</p>
<p>也就是说，原始问题里我们直接求“哪条边界最好”：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2\quad \text{s.t.}\quad y_i(\mathbf{w}^\top\mathbf{x}_i+b)\ge 1\]</span>
<p>而引入拉格朗日乘子之后，可以先看下面这个函数：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(\mathbf{w},b,\alpha)=\frac{1}{2}\|\mathbf{w}\|_2^2-\sum_{i=1}^{N}\alpha_i\Big(y_i(\mathbf{w}^\top \mathbf{x}_i+b)-1\Big),\quad \alpha_i\ge 0\]</span>
<p>对固定的 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span>，它就是一个关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 的凸函数。于是我们先做内层最小化：</p>
<span displaypfx="" class="mathjax-container">\[g(\alpha)=\min_{\mathbf{w},b}\ \mathcal{L}(\mathbf{w},b,\alpha)\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(g(\alpha)\)</span> 就叫对偶函数（Dual Function）。它表示：<span style="background-color: #c0c0c0;">假设每个样本的施压强度已经给定，边界那一侧最好的回应会是什么</span>。</p>
<p>这一步的好处是， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 可以被显式消掉。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 求驻点条件，有：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial \mathbf{w}}=0\Rightarrow \mathbf{w}=\sum_{i=1}^{N}\alpha_i y_i\mathbf{x}_i\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial b}=0\Rightarrow \sum_{i=1}^{N}\alpha_i y_i=0\]</span>
<p>这两条式子非常关键：第一条说明法向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 完全由训练样本线性组合出来；第二条说明正负两类样本的“总施压”必须平衡。把它们代回去，原来依赖 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 的问题就变成只依赖 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 的问题：</p>
<span displaypfx="" class="mathjax-container">\[\max_{\alpha}\ \sum_{i=1}^{N}\alpha_i-\frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<span displaypfx="" class="mathjax-container">\[\text{s.t.}\quad \alpha_i\ge 0,\quad \sum_{i=1}^{N}\alpha_i y_i=0\]</span>
<p>这就是 SVM 的对偶问题（Dual Problem）。现在可以看出它为什么叫“对偶”：它没有再直接问“<span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 应该是多少”，而是改问“每个样本应该分到多大的权重 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>，才能共同把最优边界顶出来”。</p>
<p>这样改写有两个直接收益。第一，支持向量为什么稀疏会变得一眼可见：若某个样本最终 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*=0\)</span>，它就自动从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=\sum_i \alpha_i y_i\mathbf{x}_i\)</span> 里消失。第二，对偶目标里出现的样本方式只剩内积 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\mathbf{x}_j\)</span>，这正是后面引入核函数（Kernel）的入口。</p>
<p>把最优权重 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*\)</span> 求出来后，分类器可以直接写成：</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathrm{sign}\left(\sum_{i=1}^{N}\alpha_i^* y_i\,\mathbf{x}_i^\top \mathbf{x}+b^*\right)\]</span>
<p>这个式子的含义很具体：新样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 会与训练样本做内积（Inner Product），也就是计算相似度；每个训练样本按自己的类别符号 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 和影响系数 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*\)</span> 投一票；最后把这些票加总，再加上偏置 <span displaypfx="inline-" class="mathjax-container">\(b^*\)</span>，看结果落在哪一侧。</p>
<p>由于绝大多数样本的 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*=0\)</span>，真正参与这次投票的通常只有支持向量。因此 SVM 的预测阶段常带有一个很强的稀疏性（Sparsity）：<span style="background-color: #c0c0c0;">不是所有训练样本都在持续发声，真正起作用的只是边界附近那一小部分点</span>。</p>
<p>偏置 <span displaypfx="inline-" class="mathjax-container">\(b^*\)</span> 可以用任一支持向量恢复。若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_k\)</span> 是一个支持向量，则它满足 <span displaypfx="inline-" class="mathjax-container">\(y_k(\mathbf{w}^{*\top}\mathbf{x}_k+b^*)=1\)</span>，因此</p>
<span displaypfx="" class="mathjax-container">\[b^*=y_k-\mathbf{w}^{*\top}\mathbf{x}_k\]</span>
<div class="blog_h4"><span class="graybg">核函数（Kernel）：把“线性边界”搬到更合适的空间</span></div>
<p>对偶问题真正重要的地方，不只是“把变量从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 换成了 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>”，而是它把 SVM 的全部数据依赖压缩成了样本之间的内积（Inner Product）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<p>这一步非常关键，因为它说明：<span style="background-color: #c0c0c0;">SVM 在对偶形式里并不需要直接看到样本坐标本身，它只需要知道样本彼此有多相似</span>。只要这种“相似度”还能写成某个空间里的内积，SVM 的训练与预测公式就都可以照搬。</p>
<p>于是核技巧（Kernel Trick）的引入就变得自然了。设有一个特征映射（Feature Map）<span displaypfx="inline-" class="mathjax-container">\(\phi(\mathbf{x})\)</span>，它把原始样本送到更高维、甚至无限维的特征空间（Feature Space）。如果我们真的显式去算这个高维向量，代价往往很大；但对偶形式只关心内积，因此只要能直接计算</p>
<span displaypfx="" class="mathjax-container">\[K(\mathbf{x}_i,\mathbf{x}_j)=\phi(\mathbf{x}_i)^\top\phi(\mathbf{x}_j)\]</span>
<p>就等价于“隐式地”在特征空间里做线性 SVM，而不必真的把 <span displaypfx="inline-" class="mathjax-container">\(\phi(\mathbf{x})\)</span> 写出来。这个直接在原空间里计算特征空间内积的技巧，就叫核函数（Kernel Function）或核技巧（Kernel Trick）。</p>
<p>这也解释了为什么 kernel 是从 dual 里长出来的，而不是额外拼上去的：若你还停留在原始问题里，眼前看到的仍是显式参数 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span>；而一旦进入对偶形式，表达式里只剩 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\mathbf{x}_j\)</span>，把它替换成别的“合法相似度”就成了最自然的一步。</p>
<p>于是决策函数从</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathrm{sign}\left(\sum_{i=1}^{N}\alpha_i^* y_i\,\mathbf{x}_i^\top \mathbf{x}+b^*\right)\]</span>
<p>变成</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathrm{sign}\left(\sum_{i=1}^{N}\alpha_i^* y_i\,K(\mathbf{x}_i,\mathbf{x})+b^*\right)\]</span>
<p>要点是：<span style="background-color: #c0c0c0;">特征空间里仍然是线性超平面；只是映回原空间后，边界看起来变成了弯的</span>。因此 kernel 不是“把线性 SVM 换成非线性模型”，而是“先把数据换到更容易线性可分的表示里，再继续做线性 SVM”。</p>
<p>从直觉上看，核函数本质上是在重新定义“两个样本像不像”。线性核（Linear Kernel）比较原始方向是否一致；多项式核（Polynomial Kernel）强调特征之间的组合关系；RBF / Gaussian 核更强调局部邻近性，因此很容易形成局部、弯曲的决策边界。</p>
<ul>
<li>线性核（Linear Kernel）：<span displaypfx="inline-" class="mathjax-container">\(K(\mathbf{x},\mathbf{z})=\mathbf{x}^\top\mathbf{z}\)</span>。</li>
<li>多项式核（Polynomial Kernel）：<span displaypfx="inline-" class="mathjax-container">\(K(\mathbf{x},\mathbf{z})=(\mathbf{x}^\top\mathbf{z}+c)^d\)</span>。</li>
<li>RBF / Gaussian 核：<span displaypfx="inline-" class="mathjax-container">\(K(\mathbf{x},\mathbf{z})=\exp(-\gamma\|\mathbf{x}-\mathbf{z}\|_2^2)\)</span>。</li>
</ul>
<p>一个典型例子是“同心圆”二分类：在二维平面里，内圈和外圈无法用一条直线分开；但如果映射到包含半径平方等特征的空间，类别就可能被一个超平面分开。核方法的价值就在这里：<span style="background-color: #c0c0c0;">原空间里看到的是弯曲边界，特征空间里做的仍然是线性分类</span>。</p>
<div class="blog_h4"><span class="graybg">软间隔 SVM（Soft-margin SVM）：允许少量违约，但要付代价</span></div>
<p>现实数据通常含噪声、离群点（Outlier）或类别重叠。若仍然要求“所有点都必须在间隔之外”，硬间隔 SVM 很可能根本无解，或者被少数异常点强行拉歪。软间隔 SVM（Soft-margin SVM）因此引入松弛变量（Slack Variable）<span displaypfx="inline-" class="mathjax-container">\(\xi_i\ge 0\)</span>，允许个别样本违反间隔约束：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b,\xi}\ \frac{1}{2}\|\mathbf{w}\|_2^2+C\sum_{i=1}^{N}\xi_i\quad \text{s.t.}\quad y_i(\mathbf{w}^\top \mathbf{x}_i+b)\ge 1-\xi_i,\ \xi_i\ge 0\]</span>
<p>这个式子只表达一件事：边界仍然希望尽量大，但违约要交罚款，罚款强度由 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 决定。对单个样本， <span displaypfx="inline-" class="mathjax-container">\(\xi_i\)</span> 的含义可以直接按大小来读：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\xi_i=0\)</span>：样本分类正确，且在间隔边界上或之外。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(0&lt;\xi_i\le 1\)</span>：样本仍在正确一侧，但已经挤进了间隔内部。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\xi_i&gt;1\)</span>：样本跨过了分类边界，已经被误分。</li>
</ul>
<p><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 控制的是“对违约有多敏感”：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 大：更重视把训练集分对，边界更硬，对噪声也更敏感。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 小：更重视大间隔和整体稳定性，允许少量训练误差。</li>
</ul>
<p>软间隔下，支持向量的范围也更宽：不仅贴着间隔边界的点重要，落在间隔内部甚至被误分的点也会直接影响最优解。对应到对偶变量，常见情形是 <span displaypfx="inline-" class="mathjax-container">\(0&lt;\alpha_i&lt;C\)</span> 的点贴在间隔上，而 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i=C\)</span> 的点往往是间隔内样本或误分类样本。</p>
<p>把松弛变量消去后，软间隔 SVM 还可以写成更常见的合页损失（Hinge Loss）形式：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2+C\sum_{i=1}^{N}\max\big(0,\ 1-y_i(\mathbf{w}^\top \mathbf{x}_i+b)\big)\]</span>
<p>第一项限制模型复杂度，第二项惩罚分类违约。SVM 因此可以被看作“<span style="background-color: #c0c0c0;">大间隔 + 违约惩罚</span>”的组合，而支持向量则是这两股力量平衡后仍然留在最前线的样本。</p>
<div class="blog_h2"><span class="graybg">树模型与集成方法</span></div>
<p>这一类方法处理的核心问题是：当输入与输出之间存在明显非线性、阈值效应与高阶特征交互时，线性模型往往表达力不足，但工程上仍然希望模型具备较强可解释性、对表格数据友好、并且训练稳定。树模型通过递归切分（Recursive Partitioning）把输入空间划成若干局部区域；集成方法则进一步通过 Bagging 或 Boosting 提升泛化能力与精度。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">模型数量与训练逻辑</td>
<td style="text-align: center;">集成策略 / 学习机制</td>
<td style="text-align: center;">单棵树特点</td>
<td style="text-align: center;">主要在优化什么</td>
<td style="text-align: center;">过拟合风险</td>
<td style="text-align: center;">更适合什么场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策树</td>
<td>单棵独立模型，直接在原始数据上递归切分，没有集成过程</td>
<td>无；直接学习输入到输出的分段规则</td>
<td>树深完全由数据与约束参数决定；过深会记住噪声，过浅又会表达不足</td>
<td>没有显式“降偏差 / 降方差”分工，偏差和方差都要靠剪枝、树深和叶子约束一起控制</td>
<td>高；单树很容易把训练集中的偶然模式学进去</td>
<td>可解释性要求极高的简单任务、规则基线、集成模型的基学习器</td>
</tr>
<tr>
<td>随机森林</td>
<td>多棵树并行独立训练，树与树之间无依赖，可利用多核 CPU 并行</td>
<td>Bagging：bootstrap 采样 + 特征子采样 + 投票 / 平均</td>
<td>单棵树通常故意训练得较深，先把单树偏差压低，再依靠集成抵消高方差</td>
<td>主要降低方差，让整体模型更稳、更抗数据扰动</td>
<td>低；Bagging 和平均机制天然有正则化效果</td>
<td>中小规模表格数据、强 baseline、噪声较大且希望训练稳定的场景</td>
</tr>
<tr>
<td>GBDT / XGBoost</td>
<td>多棵树严格串行训练；后一棵树必须等前一棵树完成后，才能开始学习新的修正项</td>
<td>Boosting：拟合残差或负梯度，逐轮加权累加</td>
<td>单棵树通常较浅，只负责局部纠错；单树偏差高，但方差相对更低</td>
<td>主要降低偏差，通过接力纠错不断提升拟合能力</td>
<td>中等；若树数太多、学习率太大或树过深，容易持续把训练集吃满</td>
<td>结构化数据里追求高精度的任务，如风控、推荐、广告、竞赛</td>
</tr>
<tr>
<td>LightGBM</td>
<td>仍然属于串行 Boosting，但单棵树训练做了更激进的近似和工程优化</td>
<td>Boosting + 直方图分桶 + leaf-wise 生长 + GOSS / EFB</td>
<td>单棵树依然是浅到中等深度的纠错器，但 leaf-wise 会把最值得细分的局部继续挖深</td>
<td>继续降低偏差，同时极力优化训练速度与内存效率</td>
<td>中等偏高；若 leaf-wise 缺少足够约束，局部树会很快长深</td>
<td>大规模表格数据、高维稀疏特征、需要频繁重训的工业流水线</td>
</tr>
<tr>
<td>CatBoost</td>
<td>仍然属于串行 Boosting，但重点放在类别特征处理与训练偏移控制</td>
<td>Boosting + ordered target statistics + ordered boosting + symmetric tree</td>
<td>单棵树结构更规整，对称树推理快；模型内部原生吸收类别特征统计信息</td>
<td>继续降低偏差，同时尽量减少类别编码带来的泄露与偏移</td>
<td>中等；总体可控，但仍需学习率、树数和正则化配合</td>
<td>类别特征很多、希望少做手工编码、快速起一个强模型的表格任务</td>
</tr>
</tbody>
</table>
<p>这张表背后的逻辑可以压缩成一句话：<span style="background-color: #c0c0c0;">决策树是基础，随机森林主要靠并行平均降低方差，GBDT 家族主要靠串行纠错降低偏差，而 XGBoost、LightGBM、CatBoost 则是在 GBDT 主线上的工业级强化</span>。因此它们虽然都属于“树模型”，训练逻辑、误差控制方式和工程侧优势并不相同。</p>
<div class="blog_h3"><span class="graybg">决策树</span></div>
<p>决策树（Decision Tree）把预测过程写成一串逐层切分的规则：内部节点负责提问，边负责根据答案分流，叶节点负责输出最终结果。它的优势不在于公式复杂，而在于模型结构与业务规则天然同构：每一条从根到叶的路径，都对应一条可读的判断链。</p>
<div class="blog_h4"><span class="graybg">决策树、分类树、回归树的关系</span></div>
<p>决策树是总称；分类树（Classification Tree）与回归树（Regression Tree）是它在两类监督学习任务上的具体形式。三者共享同一种树结构，但目标变量、切分准则与叶子输出不同。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">概念</td>
<td style="text-align: center;">预测目标</td>
<td style="text-align: center;">叶子输出</td>
<td style="text-align: center;">常用切分准则</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策树（Decision Tree）</td>
<td>树模型的总称</td>
<td>取决于具体任务</td>
<td>取决于具体任务</td>
</tr>
<tr>
<td>分类树（Classification Tree）</td>
<td>离散标签（如“流失/不流失”）</td>
<td>类别或类别概率</td>
<td>基尼不纯度（Gini）、熵（Entropy）、信息增益（Information Gain）</td>
</tr>
<tr>
<td>回归树（Regression Tree）</td>
<td>连续数值（如“价格”“时长”）</td>
<td>一个数值常数</td>
<td>平方误差（Squared Error）、MSE / SSE 下降</td>
</tr>
</tbody>
</table>
<p>因此不要把“决策树”和“分类树”当成并列概念。更准确的表述是：<span style="background-color: #c0c0c0;">分类树与回归树都是决策树；前者预测类别，后者预测数值</span>。</p>
<div class="blog_h4"><span class="graybg">结构与统一公式</span></div>
<p>设某个节点上落入的样本集合为 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，大小为 <span displaypfx="inline-" class="mathjax-container">\(|S|\)</span>。对某个候选切分（Split）<span displaypfx="inline-" class="mathjax-container">\(\phi\)</span>，例如“第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个特征 <span displaypfx="inline-" class="mathjax-container">\(x_j\le t\)</span>”，样本会被分成左右两个子节点：</p>
<span displaypfx="" class="mathjax-container">\[S_L(\phi)=\{(\mathbf{x}_i,y_i)\in S:\ x_{i,j}\le t\},\quad S_R(\phi)=S\setminus S_L(\phi)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(S_L\)</span> 是满足切分条件的样本集合，<span displaypfx="inline-" class="mathjax-container">\(S_R\)</span> 是剩余样本集合。树在每个节点都会尝试多个候选切分 <span displaypfx="inline-" class="mathjax-container">\(\phi\)</span>，并保留收益最大的那个。</p>
<p>为了避免分别记忆分类树与回归树的训练目标，可以先写成统一形式。设 <span displaypfx="inline-" class="mathjax-container">\(I(S)\)</span> 表示“节点 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 当前有多乱”或“在该节点上预测误差有多大”，则一次切分的收益可统一写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Gain}(S,\phi)=I(S)-\frac{|S_L(\phi)|}{|S|}I\!\left(S_L(\phi)\right)-\frac{|S_R(\phi)|}{|S|}I\!\left(S_R(\phi)\right)\]</span>
<p>这条式子的含义非常直接：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(I(S)\)</span>：切分前，这个节点有多混乱。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(I(S_L),I(S_R)\)</span>：切分后，左右子节点各自还有多混乱。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\frac{|S_L|}{|S|},\frac{|S_R|}{|S|}\)</span>：左右子节点占父节点样本的比例。要乘这个比例，是因为大子节点对总体误差的影响更大，小子节点不能和大子节点拥有同样权重。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Gain}(S,\phi)\)</span> 越大，说明这次切分越有效。</li>
</ul>
<p>分类树与回归树的差别，正体现在 <span displaypfx="inline-" class="mathjax-container">\(I(S)\)</span> 具体取什么。</p>
<div class="blog_h4"><span class="graybg">决策树：一个整体例子</span></div>
<p>在贷款审批（Loan Approval）场景中，决策树可以直接写成规则链。根节点先按负债收入比（Debt-to-Income Ratio）切分；若负债收入比过高，再看是否有逾期记录；若负债收入比正常，再看信用评分（Credit Score）与近 6 个月收入稳定性。最终某个叶节点可能对应“直接通过”，另一个叶节点对应“人工复核”，再另一个叶节点对应“拒绝”。这就是决策树最重要的工程价值：它不只是给出一个分数，还给出一条可追溯的判断路径。</p>
<div class="blog_h4"><span class="graybg">分类树：目标、公式与含义</span></div>
<p>分类树的目标是让每个叶节点里的标签尽量单一。若一个节点里几乎都是同一类样本，这个节点就“纯”；若各类样本混在一起，这个节点就“不纯”。</p>
<p>设类别集合为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{K}\)</span>，类别 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 在节点 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 中的占比记为：</p>
<span displaypfx="" class="mathjax-container">\[p_k(S)=\frac{\text{节点 }S\text{ 中标签为 }k\text{ 的样本数}}{|S|}\]</span>
<p>常用的不纯度（Impurity）有两种。</p>
<p>基尼不纯度（Gini Impurity）：</p>
<span displaypfx="" class="mathjax-container">\[G(S)=1-\sum_{k\in\mathcal{K}}p_k(S)^2\]</span>
<p>它可以读成“随机抽两个样本时，标签不一致的倾向有多强”。若节点里全是同一类，则某个 <span displaypfx="inline-" class="mathjax-container">\(p_k=1\)</span>、其余为 0，此时 <span displaypfx="inline-" class="mathjax-container">\(G(S)=0\)</span>，说明节点已经纯净；若二分类里两类各占一半，则 <span displaypfx="inline-" class="mathjax-container">\(G(S)=1-(0.5^2+0.5^2)=0.5\)</span>，说明混杂程度较高。</p>
<p>熵（Entropy）：</p>
<span displaypfx="" class="mathjax-container">\[H(S)=-\sum_{k\in\mathcal{K}}p_k(S)\log p_k(S)\]</span>
<p>熵衡量的是“不确定性”。若节点里全是同一类，则不需要再猜，熵为 0；若各类比例接近，说明不确定性高，熵也更大。</p>
<p>信息增益（Information Gain）就是“切分前的不确定性”减去“切分后的加权不确定性”：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{IG}(S,\phi)=H(S)-\frac{|S_L(\phi)|}{|S|}H\!\left(S_L(\phi)\right)-\frac{|S_R(\phi)|}{|S|}H\!\left(S_R(\phi)\right)\]</span>
<p>这条公式的每一部分都对应一个明确动作：</p>
<ul>
<li>第一项 <span displaypfx="inline-" class="mathjax-container">\(H(S)\)</span> 是切分前的混乱程度。</li>
<li>后两项是切分后左右子节点各自的混乱程度，并按样本占比加权求和。</li>
<li>两者相减，就是这次切分让节点“变纯”了多少。</li>
</ul>
<p>有些实现使用基尼下降而不是信息增益，本质上是同一件事：<span style="background-color: #c0c0c0;">选择那个能让子节点更纯、让标签更集中的切分</span>。</p>
<div class="blog_h4"><span class="graybg">分类树：实际例子——用户流失预警</span></div>
<p>设任务是预测“一个用户未来 30 天是否流失”。标签只有两类：流失 / 未流失，因此这是典型分类树问题。候选特征可以包括：最近 7 天登录次数、最近 30 天是否投诉、是否还有未使用优惠券、最近一次下单距今天数。</p>
<p>假设根节点先尝试切分“最近 7 天登录次数 &lt; 2”。这次切分后，左子节点里的用户大多已经很久不活跃，且流失比例显著升高；右子节点里的用户则活跃度更高、留存率更好。此时无论用基尼还是熵计算，左右节点的加权不纯度都会明显低于父节点，因此这会成为一个高质量切分。</p>
<p>继续往下，左子节点还可以再按“最近 30 天是否投诉”切分：低活跃且有投诉的用户，叶节点里可能出现“82% 最终流失”；低活跃但无投诉的用户，叶节点里可能是“61% 流失”。此时叶子不只给出类别，还可给出经验概率。工程上，这样的输出就能直接用于运营动作：高风险叶子推召回优惠券，中风险叶子推客服回访，低风险叶子不干预。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/descision-tree-1.jpg"><img class="alignnone size-full wp-image-41221" src="https://blog.gmem.cc/wp-content/uploads/2026/03/descision-tree-1.jpg" alt="descision-tree-1" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">回归树：目标、公式与含义</span></div>
<p>回归树处理的是连续数值目标，例如价格、时长、销量、能耗。它不追求“类别更纯”，而追求“同一个叶节点里的数值尽量接近”。</p>
<p>若某个叶节点 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 最终只输出一个常数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，那么在平方误差（Squared Error）下，最优输出不是中位数，而是均值：</p>
<span displaypfx="" class="mathjax-container">\[c^*(S)=\arg\min_c\sum_{(\mathbf{x}_i,y_i)\in S}(y_i-c)^2=\frac{1}{|S|}\sum_{(\mathbf{x}_i,y_i)\in S}y_i\]</span>
<p>之所以是均值，是因为平方误差会把所有偏差向两边拉平，而均值正是使平方偏差和最小的那个常数。</p>
<p>在这个叶节点上，最小平方误差和（Sum of Squared Errors, SSE）为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{SSE}(S)=\sum_{(\mathbf{x}_i,y_i)\in S}\left(y_i-c^*(S)\right)^2\]</span>
<p>它表示节点里所有样本值围绕叶子预测值 <span displaypfx="inline-" class="mathjax-container">\(c^*(S)\)</span> 的总波动。SSE 越大，说明这个节点里的样本值越分散，单用一个常数代表它们的效果越差。</p>
<p>因此一次切分的目标是让切分后的总误差尽量小，也可写成误差下降尽量大：</p>
<span displaypfx="" class="mathjax-container">\[\Delta(S,\phi)=\mathrm{SSE}(S)-\mathrm{SSE}\!\left(S_L(\phi)\right)-\mathrm{SSE}\!\left(S_R(\phi)\right)\]</span>
<p>若更喜欢看平均误差，也可以把它写成 MSE（Mean Squared Error）形式：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{MSE}(S)=\frac{1}{|S|}\mathrm{SSE}(S)\]</span>
<p>等价地，也可以最小化切分后的加权平均误差：<span displaypfx="" class="mathjax-container">\[\frac{|S_L|}{|S|}\mathrm{MSE}(S_L)+\frac{|S_R|}{|S|}\mathrm{MSE}(S_R)\]</span></p>
<p>两种写法完全等价：SSE 强调总误差，MSE 强调平均误差；本质都是寻找让子节点内部数值更集中的切分。</p>
<div class="blog_h4"><span class="graybg">回归树：实际例子——外卖配送时长预测</span></div>
<p>设任务是预测一笔订单从接单到送达需要多少分钟。这是连续数值目标，因此属于回归树。候选特征可以包括：配送距离、是否下雨、是否晚高峰、商家出餐速度、骑手当前手中订单数。</p>
<p>根节点可能先按“配送距离是否大于 3 公里”切分。因为近距离订单与远距离订单的时长分布差异很大，这一步通常能显著降低节点内部方差。对远距离子节点，再按“是否下雨”切分；下雨天路况更慢、波动更大。对近距离子节点，则可能按“是否处于午晚高峰”切分。</p>
<p>假设某个叶节点对应“距离 &gt; 3 公里、下雨、晚高峰”这类订单，这个叶节点里的训练样本平均送达时长是 47 分钟，那么该叶子的预测值就是 47。另一个叶节点若对应“距离 &lt; 2 公里、不下雨、非高峰”，其平均时长可能只有 18 分钟。回归树的预测逻辑不是拟合一条全局直线，而是把不同业务情境分段，再在每段内给出一个局部平均值。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/decision-tree-2.png"><img class="alignnone size-full wp-image-41227" src="https://blog.gmem.cc/wp-content/uploads/2026/03/decision-tree-2.png" alt="decision-tree-2" width="1469" height="1035" /></a></p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>需要把模型输出翻译成可审计规则，例如风控、审批、运营分层、客服分流。</li>
<li>数据以表格（Tabular）为主，且存在显著非线性、阈值效应或特征交互。</li>
<li>希望同时处理连续特征与离散特征，并保留较强可解释性。</li>
<li>注意：单棵树方差高、容易过拟合，通常需要限制最大深度、最小叶子样本数或配合集成方法。</li>
</ul>
<div class="blog_h3"><span class="graybg">随机森林</span></div>
<p>随机森林（Random Forest）是 Bagging（Bootstrap Aggregating）在树模型上的经典实现：用 bootstrap 采样生成多份训练子集，训练多棵通常偏差较低、但对训练数据扰动高度敏感（高方差）的决策树，再把它们的输出聚合，以显著降低整体方差并提升鲁棒性。</p>
<p>这里的“高方差（High Variance）”指模型的估计方差（Estimator Variance）或预测方差（Prediction Variance）：如果训练集稍有变化，单棵树学到的分裂结构与最终预测就可能明显变化。随机森林利用多棵树的平均/投票，把这种由数据扰动带来的波动相互抵消。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.jpg"><img class="alignnone size-full wp-image-41231" src="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.jpg" alt="random-forest" width="1287" height="816" /></a></p>
<div class="blog_h4"><span class="graybg">它和单棵树真正差在哪里</span></div>
<p>随机森林并不是“很多棵树简单叠起来”这么粗糙。单棵树的主要问题，是一旦在上层节点做出某个早期切分，后面的整条子树都会被这个局部选择锁定，因此对数据扰动极敏感。随机森林通过 bootstrap 采样与特征子采样，让每棵树在“数据视角”和“可用特征视角”上都略有不同，再把这些不同视角的判断平均起来，从而大幅削弱某一棵树早期错误切分的破坏力。</p>
<div class="blog_h4"><span class="graybg">算法与符号</span></div>
<p>给定训练集 <span displaypfx="inline-" class="mathjax-container">\(D=\{(\mathbf{x}_i,y_i)\}_{i=1}^{N}\)</span>。对 <span displaypfx="inline-" class="mathjax-container">\(m=1,\dots,M\)</span>：</p>
<ol>
<li>bootstrap 采样：从 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 有放回采样 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 次得到 <span displaypfx="inline-" class="mathjax-container">\(D_m\)</span>。所谓“有放回（Sampling with Replacement）”，是指每次抽到一个样本后，都先把它放回原数据集，再进行下一次抽样；因此同一个样本可能被重复抽中，而有些样本在这一轮里一次也没有被抽到。</li>
<li>训练一棵树 <span displaypfx="inline-" class="mathjax-container">\(T_m\)</span>：每个节点分裂时，只在随机选取的 <span displaypfx="inline-" class="mathjax-container">\(d'\)</span> 个特征上搜索最优切分（特征子采样，feature subsampling）。</li>
</ol>
<p>预测时，回归取平均，分类取多数投票：</p>
<span displaypfx="" class="mathjax-container">\[\hat y_{\text{reg}}(\mathbf{x})=\frac{1}{M}\sum_{m=1}^{M}T_m(\mathbf{x}),\quad \hat y_{\text{clf}}(\mathbf{x})=\mathrm{mode}\left(\{T_m(\mathbf{x})\}_{m=1}^{M}\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T_m(\mathbf{x})\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 棵树对样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的预测。回归任务把所有树的输出做平均，以减少波动；分类任务取众数 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{mode}(\cdot)\)</span>，也就是票数最多的类别。随机森林的稳定性正来自这种“多棵树共同决定”的聚合机制。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(M\)</span>：树的数量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(d'\)</span>：每次分裂考虑的特征数（常见经验：分类用 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{d}\)</span>，回归用 <span displaypfx="inline-" class="mathjax-container">\(d/3\)</span>）。</li>
</ul>
<div class="blog_h4"><span class="graybg">为什么有效：方差下降与“去相关”</span></div>
<p>若单棵树预测的方差为 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span>，不同树之间的相关系数近似为 <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span>（<span displaypfx="inline-" class="mathjax-container">\(0\le\rho\le 1\)</span>），则平均后的方差近似为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}\!\left(\frac{1}{M}\sum_{m=1}^{M}T_m(\mathbf{x})\right)\approx \sigma^2\left(\rho+\frac{1-\rho}{M}\right)\]</span>
<p>这个近似式子把随机森林为什么有效说得很清楚： <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 是单棵树自身的预测波动， <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span> 是树与树之间的相关性， <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 是树数。树越多， <span displaypfx="inline-" class="mathjax-container">\(\frac{1-\rho}{M}\)</span> 越小；树之间越不相似， <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span> 越低，最终平均后的波动就越小。</p>
<p>因此随机森林有两条主线：</p>
<ul>
<li>增加 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 降低 <span displaypfx="inline-" class="mathjax-container">\((1-\rho)/M\)</span> 项。</li>
<li>通过 bootstrap + 特征子采样降低相关性 <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span>，让集成真正“互补”。</li>
</ul>
<div class="blog_h4"><span class="graybg">OOB：不用额外验证集的误差估计</span></div>
<p>bootstrap 采样会重复抽到某些样本。对固定样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span>，一次采样中没被抽到的概率为 <span displaypfx="inline-" class="mathjax-container">\((1-\frac{1}{N})^N\approx e^{-1}\approx 0.368\)</span>。因此每棵树大约有 36.8% 的样本是袋外（Out-of-Bag, OOB）样本，可用它们评估该树对未见数据的表现，并对全森林给出近似验证误差。</p>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\((1-\frac{1}{N})\)</span> 表示“一次抽样没抽到某个固定样本”的概率，连续做 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 次后得到 <span displaypfx="inline-" class="mathjax-container">\((1-\frac{1}{N})^N\)</span>。当 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 足够大时，它逼近 <span displaypfx="inline-" class="mathjax-container">\(e^{-1}\)</span>，也就是约 36.8%。这就是为什么随机森林天然拥有一批“没参与这棵树训练”的 OOB 样本。</p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>表格数据（Tabular）的强默认基线：非线性、特征交互、缺失值与尺度不一致都较鲁棒。</li>
<li>对超参数不敏感、训练稳定；可用特征重要性（Feature Importance）做解释与特征筛查。</li>
<li>当需要极致精度时，GBDT 家族往往更强；当需要更快推理/更小模型时，线性/浅层模型更合适。</li>
</ul>
<div class="blog_h3"><span class="graybg">梯度提升树（GBDT）</span></div>
<p>梯度提升树（Gradient Boosting Decision Tree, GBDT）是一类按序叠加回归树的加法模型（Additive Model）。模型从简单的初始预测 <span displaypfx="inline-" class="mathjax-container">\(F_0\)</span> 出发，在每一轮加入一棵新树，用于修正当前模型尚未拟合好的部分，从而逐步降低训练集上的经验风险（Empirical Risk）。</p>
<p>每一轮新增的树都对应一个修正函数（Correction Function）。它不直接重新学习标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>，而是拟合当前模型输出与目标之间尚未被解释的差异。多轮修正连续叠加后，模型会从粗糙预测逐步逼近目标函数。</p>
<p>一个直观比喻是：GBDT 像一组按顺序接手的阅卷老师。第一位老师先给出一个粗略分数，后面的每一位老师都不重做整张卷子，只专门检查前面模型错得最明显的地方，并在这些地方补上修正意见。树一棵接一棵叠加后，最终预测会越来越接近真实值。</p>
<p>这里每棵小树都不是独立完成任务的“大模型”，而是一个局部纠错器。它关心的是当前模型还没解释好的误差：哪些样本被高估了，哪些被低估了，以及这些误差集中出现在哪些特征区域。“梯度”对应当前损失下降最快的修正方向，“提升”则表示把这些小修正持续累加成一个更强的整体模型。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/gbdt.jpg"><img class="alignnone size-full wp-image-41247" src="https://blog.gmem.cc/wp-content/uploads/2026/03/gbdt.jpg" alt="gbdt" width="1408" height="768" /></a></p>
<div class="blog_h4"><span class="graybg">目标：最小化经验风险</span></div>
<p>GBDT 背后的目标非常直接：寻找一个函数 <span displaypfx="inline-" class="mathjax-container">\(F\)</span>，使训练集上的总损失最小：</p>
<span displaypfx="" class="mathjax-container">\[ \min_F\ \sum_{i=1}^{N}\ell\big(y_i,F(\mathbf{x}_i)\big) \]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\ell(y,F(\mathbf{x}))\)</span> 是单样本损失函数，平方损失、对数损失等都可以放进来。难点在于：函数 <span displaypfx="inline-" class="mathjax-container">\(F\)</span> 不是一个普通标量参数，而是一个复杂的预测函数；如果一次性同时优化所有树的结构和叶子输出，组合空间过大，几乎不可直接求解。</p>
<p>因此 GBDT 采用前向分步加法（Forward Stagewise Additive Modeling）：不一次求整个 <span displaypfx="inline-" class="mathjax-container">\(F\)</span>，而是把它写成逐步累加的形式，只在第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮新增一个修正函数 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[ F_M(\mathbf{x})=F_0(\mathbf{x})+\nu\sum_{m=1}^{M}f_m(\mathbf{x}) \]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(F_M(\mathbf{x})\)</span> 表示：模型经过总共 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 轮提升后，对输入样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 输出的最终预测值。下标 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 表示“已经累计做了 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 次修正”，不是幂次，也不是某一棵单独的树。GBDT 的最终模型是许多轮小修正叠加后的总结果。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span>：一个输入样本的特征向量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(F_0(\mathbf{x})\)</span>：初始模型对样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的预测，常取常数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 使 <span displaypfx="inline-" class="mathjax-container">\(\sum_i \ell(y_i,c)\)</span> 最小。它可以理解为模型在还没有长出任何树之前给出的第一版粗略判断。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f_m(\mathbf{x})\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮新增的回归树在样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 上给出的修正值，负责弥补当前模型尚未拟合好的部分。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_{m=1}^{M}f_m(\mathbf{x})\)</span>：把前 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 轮所有修正树的输出加起来，得到总修正量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\nu\in(0,1]\)</span>：学习率（Shrinkage），控制每一步修正只走多大。它的作用不是改变方向，而是缩小步长，使训练更稳定、泛化更好。</li>
</ul>
<p>这个公式也可以按“底稿 + 反复批改”来理解： <span displaypfx="inline-" class="mathjax-container">\(F_0(\mathbf{x})\)</span> 是第一版预测，后面的每个 <span displaypfx="inline-" class="mathjax-container">\(f_m(\mathbf{x})\)</span> 都是在已有结果上补一小笔修正，最终的 <span displaypfx="inline-" class="mathjax-container">\(F_M(\mathbf{x})\)</span> 就是经历 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 次修正后的版本。</p>
<span displaypfx="" class="mathjax-container">\[ f_m=\arg\min_f\sum_{i=1}^{N}\ell\big(y_i,F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\big) \]</span>
<p>这条式子表示：在第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮，模型要寻找一个新的修正函数 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span>，使它加到旧模型 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}\)</span> 上之后，训练集的总损失尽可能小。这里的 <span displaypfx="inline-" class="mathjax-container">\(\arg\min_f\)</span> 可以读作“在所有候选函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 里，找出那个能让目标最小的函数”。因此，求出来的不是一个数，而是当前这一轮最合适的新树。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(F_{m-1}(\mathbf{x}_i)\)</span>：前 <span displaypfx="inline-" class="mathjax-container">\(m-1\)</span> 轮模型在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本上的当前预测值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x}_i)\)</span>：候选新树在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本上的修正值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\)</span>：把新树加进去之后，这个样本的新预测值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\ell\big(y_i,F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\big)\)</span>：该样本在新预测下的损失。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_{i=1}^{N}\ell(\cdot)\)</span>：把所有样本的损失加总，得到这一轮希望尽量压低的整体目标。</li>
</ul>
<p>这一轮之所以写成 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}+f\)</span>，而不是重新求一个全新的 <span displaypfx="inline-" class="mathjax-container">\(F_m\)</span>，原因在于 GBDT 采用的是逐步修正策略：旧模型已经学到的部分先保留，新树只负责补上当前还没有拟合好的误差。这样每一轮只解决一个更小的局部问题，计算上更可行，也更符合“不断纠错”的直觉。</p>
<p>第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮只优化新增的修正函数 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span>，而把已有模型 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}\)</span> 视为固定量。这样做把原本难以整体求解的函数优化问题，拆成了一系列可逐步求解的局部优化问题。</p>
<div class="blog_h4"><span class="graybg">负梯度的来源</span></div>
<p>上面的子问题仍然不容易直接做，因为“最佳新树”本身还是一个复杂的函数搜索问题。GBDT 的关键近似是：在当前模型 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}\)</span> 附近，对损失做一阶展开，只看局部下降方向。</p>
<p>对单个样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span>，把新增修正记为 <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x}_i)\)</span>，则有：</p>
<span displaypfx="" class="mathjax-container">\[ \ell\big(y_i,F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\big) \approx \ell\big(y_i,F_{m-1}(\mathbf{x}_i)\big) + \frac{\partial \ell(y_i,F(\mathbf{x}_i))}{\partial F(\mathbf{x}_i)}\Big|_{F=F_{m-1}} f(\mathbf{x}_i) \]</span>
<p>一阶展开表明，新增函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 的最优局部方向由损失对模型输出的负梯度决定。因此第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮的伪残差（Pseudo-residual）定义为：</p>
<span displaypfx="" class="mathjax-container">\[ r_i^{(m)}=-\left.\frac{\partial \ell\left(y_i,F(\mathbf{x}_i)\right)}{\partial F(\mathbf{x}_i)}\right|_{F=F_{m-1}} \]</span>
<p>接下来的动作就自然了：不用树直接拟合标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span>，而是用一棵回归树去拟合这些伪残差 <span displaypfx="inline-" class="mathjax-container">\(r_i^{(m)}\)</span>。这棵树学到的，就是“当前模型在输入空间不同区域里，应该往哪个方向、修正多大”的分段常数近似。</p>
<p>模型更新写成：</p>
<span displaypfx="" class="mathjax-container">\[ F_m(\mathbf{x})=F_{m-1}(\mathbf{x})+\nu f_m(\mathbf{x}) \]</span>
<p>“梯度提升”这一名称对应的正是上述更新方式：优化对象不是有限维参数向量，而是预测函数本身；每一轮更新都沿着损失在函数空间中的负梯度方向加入一个新的修正函数。</p>
<div class="blog_h4"><span class="graybg">平方损失下的残差形式</span></div>
<p>若损失取平方损失</p>
<span displaypfx="" class="mathjax-container">\[ \ell(y,F)=\frac{1}{2}(y-F)^2 \]</span>
<p>则对模型输出求导：</p>
<span displaypfx="" class="mathjax-container">\[ \frac{\partial \ell(y,F)}{\partial F}=F-y \]</span>
<p>因此第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮的负梯度为：</p>
<span displaypfx="" class="mathjax-container">\[ r_i^{(m)}=-\left(F_{m-1}(\mathbf{x}_i)-y_i\right)=y_i-F_{m-1}(\mathbf{x}_i) \]</span>
<p>这正是当前预测与真实值之间的残差（Residual）。所以在回归任务里，人们常把 GBDT 说成“不断拟合残差”；这并不是另一套经验规则，而是平方损失下负梯度公式的直接结果。</p>
<div class="blog_h4"><span class="graybg">不同损失下学到的是什么</span></div>
<p>“每轮拟合残差”只在平方损失下最直观。更一般地，GBDT 拟合的是当前损失对模型输出的负梯度，因此不同任务下，每轮新树学到的对象并不完全相同。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">任务 / 损失</td>
<td style="text-align: center;">当前轮拟合的量</td>
<td style="text-align: center;">含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>平方损失（MSE）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y-F(x)\)</span></td>
<td>真实值与当前预测的残差</td>
</tr>
<tr>
<td>绝对损失（MAE）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{sign}(y-F(x))\)</span></td>
<td>只关心修正方向，不强调误差幅度；对异常值更鲁棒</td>
</tr>
<tr>
<td>二分类对数损失（Log Loss）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y-p(x)\)</span></td>
<td>真实标签与当前预测概率之间的差</td>
</tr>
</tbody>
</table>
<p>因此，GBDT 不是被固定死在“回归残差拟合”这一种形式上，而是一套更一般的函数优化框架。平方损失下它表现为残差学习；分类任务下，它表现为不断修正当前概率估计。</p>
<div class="blog_h4"><span class="graybg">树作为局部修正器</span></div>
<p>因为树天然产生分段常数（Piecewise Constant）的局部修正。若某一轮中，一棵树把样本空间切成若干区域 <span displaypfx="inline-" class="mathjax-container">\(R_1,\dots,R_J\)</span>，那么它输出的是：</p>
<span displaypfx="" class="mathjax-container">\[ f_m(\mathbf{x})=\sum_{j=1}^{J}\gamma_j\mathbf{1}(\mathbf{x}\in R_j) \]</span>
<p>这意味着：模型不是对整个输入空间统一加一个修正，而是在每个局部区域里分别加一个常数修正 <span displaypfx="inline-" class="mathjax-container">\(\gamma_j\)</span>。这正适合处理表格数据里的阈值效应、非线性和特征交互。</p>
<div class="blog_h4"><span class="graybg">叶子输出的解析解</span></div>
<p>在平方损失下，若某个叶子覆盖的样本集合为 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，并用常数 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 拟合这一叶内的残差 <span displaypfx="inline-" class="mathjax-container">\(r_i\)</span>，则该叶子的局部目标是：</p>
<span displaypfx="" class="mathjax-container">\[ \min_{\gamma}\sum_{i\in S}(r_i-\gamma)^2 \]</span>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 求导并令其为 0：</p>
<span displaypfx="" class="mathjax-container">\[ \frac{d}{d\gamma}\sum_{i\in S}(r_i-\gamma)^2=-2\sum_{i\in S}(r_i-\gamma)=0 \]</span>
<span displaypfx="" class="mathjax-container">\[ \sum_{i\in S}r_i-|S|\gamma=0 \]</span>
<span displaypfx="" class="mathjax-container">\[ \gamma^*=\frac{1}{|S|}\sum_{i\in S}r_i \]</span>
<p>因此叶子输出之所以是均值，不是经验设定，而是平方损失下这个局部最小二乘问题的解析解。每一轮加入一棵树，本质上是在每个局部区域里补上一段“平均残差修正”。</p>
<div class="blog_h4"><span class="graybg">训练流程</span></div>
<ol>
<li>初始化 <span displaypfx="inline-" class="mathjax-container">\(F_0\)</span>，通常取使总体损失最小的常数模型。</li>
<li>对第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮，计算所有样本的伪残差 <span displaypfx="inline-" class="mathjax-container">\(r_i^{(m)}\)</span>。</li>
<li>训练一棵回归树 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span> 去拟合 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{x}_i,r_i^{(m)})\)</span>。</li>
<li>把这棵树按学习率 <span displaypfx="inline-" class="mathjax-container">\(\nu\)</span> 缩小后加到当前模型上。</li>
<li>重复多轮，直到验证集误差不再下降，或达到预设树数。</li>
</ol>
<p><span style="background-color: #c0c0c0;">GBDT 的本质，是把复杂的函数优化问题拆成许多轮局部修正，并在每一轮用一棵小树逼近当前损失下降最快的方向。</span></p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>结构化表格任务中的强模型：广告、推荐、风控、运营排序、传统特征工程场景。</li>
<li>对特征尺度、非线性、缺失值较鲁棒；能自动学习高阶交互。</li>
<li>注意：对超参数更敏感（树深、学习率、树数、采样比例）；需要用验证集早停（Early Stopping）防止过拟合。</li>
</ul>
<div class="blog_h4"><span class="graybg">为什么它在表格数据上强、在别的场景不一定强</span></div>
<p>GBDT 家族在表格数据上长期强势，并不是偶然。表格任务里常见的模式，本来就很适合树模型表达：阈值效应、离散规则、局部非线性、特征交互、缺失值路径、不同子群体的分段行为。树的分裂结构天然能把“收入高且近期投诉过的老用户”和“收入低但活跃度高的新用户”这类组合规则直接切出来，而不需要像线性模型那样依赖大量手工交叉特征。</p>
<p>但同样的归纳偏置，在高维稀疏文本、图像、语音这类输入上就未必占优。因为这些任务的有效模式通常不是少数几个阈值切分，而是分布在海量维度上的连续结构、局部平滑模式或长程依赖。此时树模型虽然能做基线，但往往不如专门面向非结构化数据的线性稀疏模型、卷积网络或 Transformer。</p>
<div class="blog_h4"><span class="graybg">优点与局限</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>优点</td>
<td>表格数据精度高；对特征缩放不敏感；可处理非线性与高阶交互；通常还能给出特征重要性</td>
</tr>
<tr>
<td>局限 1</td>
<td>串行训练，训练速度通常慢于随机森林等并行集成</td>
</tr>
<tr>
<td>局限 2</td>
<td>对学习率、树数、树深、采样比例等超参数较敏感，容易出现“稍调不慎就欠拟合或过拟合”</td>
</tr>
<tr>
<td>局限 3</td>
<td>在超高维稀疏文本或图像等非结构化输入上，通常不是最自然的主力模型</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">XGBoost</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>XGBoost（Extreme Gradient Boosting）是在梯度提升树（Gradient Boosting Decision Tree, GBDT）基础上的工程化强化版本。它关注的是：在保持高表达能力的同时，让树的生长过程更稳定、目标函数更明确、复杂度控制更系统。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>XGBoost 仍然采用加法模型（Additive Model）：当前模型由多棵树的输出求和得到，第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮只学习一棵新树 <span displaypfx="inline-" class="mathjax-container">\(f_t\)</span> 作为修正项。它的关键强化点有两条：第一，把新增树的学习写成显式的带正则优化问题；第二，对该目标做二阶近似，同时利用梯度（Gradient）和海森（Hessian）信息计算叶子输出与分裂增益。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>设第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮预测为 <span displaypfx="inline-" class="mathjax-container">\(\tilde y_i^{(t)}=\tilde y_i^{(t-1)}+f_t(\boldsymbol{x}_i)\)</span>。XGBoost 在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮优化的目标可写为：</p>
<span displaypfx="" class="mathjax-container">\[\text{Obj}^{(t)}=\sum_{i=1}^{N} \ell(y_i,\tilde y_i^{(t)})+\Omega(f_t)+\text{const}\]</span>
<p>在这个目标里， <span displaypfx="inline-" class="mathjax-container">\(\ell(y_i,\tilde y_i^{(t)})\)</span> 衡量第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮更新后的预测误差， <span displaypfx="inline-" class="mathjax-container">\(\Omega(f_t)\)</span> 惩罚新树本身的复杂度， <span displaypfx="inline-" class="mathjax-container">\(\text{const}\)</span> 则表示与当前轮新树无关的常数项。也就是说，XGBoost 每一轮都在平衡“把误差降下去”和“不要把树长得太复杂”。</p>
<p>其中正则项通常取：</p>
<span displaypfx="" class="mathjax-container">\[\Omega(f)=\gamma T+\frac{\lambda}{2}\sum_{j=1}^{T}w_j^2\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 是叶子数， <span displaypfx="inline-" class="mathjax-container">\(\gamma T\)</span> 惩罚“树长出太多叶子”， <span displaypfx="inline-" class="mathjax-container">\(\frac{\lambda}{2}\sum_j w_j^2\)</span> 惩罚“每个叶子的输出值过大”。前者控制结构复杂度，后者控制数值幅度，两者合起来让模型更稳。</p>
<p>对损失在当前预测附近做二阶泰勒展开，定义：</p>
<span displaypfx="" class="mathjax-container">\[g_i=\frac{\partial \ell(y_i,\tilde y_i)}{\partial \tilde y_i}\Big|_{\tilde y_i=\tilde y_i^{(t-1)}},\qquad h_i=\frac{\partial^2 \ell(y_i,\tilde y_i)}{\partial \tilde y_i^2}\Big|_{\tilde y_i=\tilde y_i^{(t-1)}}\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(g_i\)</span> 是一阶导数，表示“当前这个样本朝哪个方向改，损失下降最快”； <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 是二阶导数，表示“这个方向有多陡、多稳定”。XGBoost 同时使用这两项信息，所以比只用一阶梯度的做法更精细。</p>
<p>则近似目标为：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\text{Obj}}^{(t)}\approx \sum_{i=1}^{N} \left(g_i f_t(\boldsymbol{x}_i)+\frac{1}{2}h_i f_t(\boldsymbol{x}_i)^2\right)+\Omega(f_t)\]</span>
<p>这条近似目标里， <span displaypfx="inline-" class="mathjax-container">\(f_t(\boldsymbol{x}_i)\)</span> 是新树在样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 上给出的修正值。线性项 <span displaypfx="inline-" class="mathjax-container">\(g_i f_t(\boldsymbol{x}_i)\)</span> 反映“沿当前方向修正是否有利”，二次项 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}h_i f_t(\boldsymbol{x}_i)^2\)</span> 反映“修正过大时会不会带来额外代价”。</p>
<p>若固定树结构，记落在叶子 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的样本集合为 <span displaypfx="inline-" class="mathjax-container">\(I_j\)</span>，并定义 <span displaypfx="inline-" class="mathjax-container">\(G_j=\sum_{i\in I_j}g_i\)</span>、<span displaypfx="inline-" class="mathjax-container">\(H_j=\sum_{i\in I_j}h_i\)</span>，则该叶子的最优输出为：</p>
<span displaypfx="" class="mathjax-container">\[w_j^*=-\frac{G_j}{H_j+\lambda}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(G_j\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个叶子里所有样本一阶梯度的总和， <span displaypfx="inline-" class="mathjax-container">\(H_j\)</span> 是二阶梯度总和。分子告诉模型这个叶子整体应该往哪个方向修正，分母则用二阶信息和正则项 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 把修正幅度稳住。</p>
<p>某次分裂把父叶拆成左右两叶后的增益（Gain）为：</p>
<span displaypfx="" class="mathjax-container">\[\text{Gain}=\frac{1}{2}\left(\frac{G_L^2}{H_L+\lambda}+\frac{G_R^2}{H_R+\lambda}-\frac{G^2}{H+\lambda}\right)-\gamma\]</span>
<p>这个增益式子比较的是“父叶不分裂”和“分成左右两叶”谁更划算。 <span displaypfx="inline-" class="mathjax-container">\(G_L,H_L\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(G_R,H_R\)</span> 分别是左右子叶的梯度统计量， <span displaypfx="inline-" class="mathjax-container">\(G,H\)</span> 是父叶统计量；最后减去 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span>，表示每多长一个叶子都要付复杂度代价。</p>
<p>这说明 XGBoost 的分裂同时考虑“收益”和“复杂度惩罚”，并以此控制结构扩张。</p>
<div class="blog_h4"><span class="graybg">相对经典 GBDT 的强化点</span></div>
<p>XGBoost 的工程优势并不只来自“实现更快”。它把几项在工业界非常关键的能力同时做扎实了：显式复杂度正则化、二阶梯度优化、行采样与列采样、对稀疏特征的处理、缓存友好的分裂搜索，以及成熟的早停和监控接口。也正因为如此，它长期被视作结构化数据建模的稳健默认选项。</p>
<ul>
<li>正则化更明确：通过叶子数惩罚与叶子权重 L2 正则显式控制树复杂度。</li>
<li>分裂选择更精细：同时使用一阶梯度与二阶梯度，不只看“该往哪边改”，还看“这一步有多稳”。</li>
<li>随机化更充分：支持样本子采样 <span displaypfx="inline-" class="mathjax-container">\(\text{subsample}\)</span> 和列采样 <span displaypfx="inline-" class="mathjax-container">\(\text{colsample\_bytree}\)</span>，既降计算量也降过拟合。</li>
<li>工程实现更成熟：对稀疏输入、缓存访问和大规模数据训练都做了专门优化。</li>
</ul>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>用当前模型计算每个样本的一阶梯度 <span displaypfx="inline-" class="mathjax-container">\(g_i\)</span> 与二阶梯度 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span>。</li>
<li>在候选特征与候选阈值上搜索分裂增益最大的切分。</li>
<li>固定树结构后，利用 <span displaypfx="inline-" class="mathjax-container">\(w_j^*\)</span> 计算每个叶子的最优输出。</li>
<li>把新树加入现有模型，继续下一轮迭代。</li>
<li>预测时把所有树的输出累加，再映射到分类或回归结果。</li>
</ol>
<div class="blog_h4"><span class="graybg">训练控制与早停</span></div>
<p>XGBoost 在工程实践里常配合较大的树数上限和验证集早停一起使用。做法通常不是先死板地决定“到底训 300 棵还是 800 棵树”，而是给一个偏大的上限，再观察验证集误差或任务指标何时不再提升。这样，树数不再是拍脑袋设定的固定值，而变成由验证集驱动的可学习训练预算。</p>
<p>这对 Boosting 家族尤其重要，因为学习率较小时，往往需要更多轮小步修正；若学习率较大，又更容易在后期进入过拟合区域。早停在这里的作用，是把“拟合能力很强”与“不要无限继续纠错”之间的边界交给验证集来判定。</p>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在点击率预估（CTR Prediction）中，用户、广告和上下文之间往往存在复杂交互。XGBoost 可以通过逐轮加树自动学习“什么样的用户在什么时间、看到什么样的广告更容易点击”这类组合规则，因此在工业级表格任务中长期保持强竞争力。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：表格数据表现强，目标函数清晰，正则化明确，工程生态成熟。</li>
<li>局限：超参数较多；树深、学习率、树数、采样比例之间存在明显耦合。</li>
<li>适用场景：风控、广告、推荐、排序与一般结构化表格建模。</li>
</ul>
<div class="blog_h3"><span class="graybg">LightGBM</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>LightGBM 是面向大规模数据与高维稀疏特征优化的 GBDT 实现。它关注的核心问题是：当样本量和特征维度都很大时，如何降低分裂搜索的时间与内存成本，同时尽量不牺牲精度。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>LightGBM 的两个关键设计是直方图分桶（Histogram Binning）和叶子优先生长（Leaf-wise Growth）。前者把连续特征离散到有限个桶上，后者每一步都继续分裂当前收益最大的叶子，从而在相同叶子预算下更快降低训练误差。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>在优化目标上，LightGBM 与 GBDT / XGBoost 一致，仍然是逐轮加入树来降低损失。它的区别主要体现在计算方式：若某个连续特征被离散到 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 个桶，则算法只需在桶边界上搜索分裂点，而不必在所有实数取值上逐一枚举。这样，每个节点维护的是桶级梯度统计量，而非逐样本的原始实数值。</p>
<p>叶子优先生长意味着：算法始终选择当前分裂增益最大的叶子继续向下扩展。它通常比按层生长（Level-wise）更激进，因而在同样叶子数限制下更容易得到较低训练误差，但也更容易在局部区域长出很深的树。</p>
<div class="blog_h4"><span class="graybg">速度来自哪些具体机制</span></div>
<p>LightGBM 的“快”并不是单一技巧带来的，而是多项近似与工程策略叠加的结果。</p>
<ul>
<li>直方图分桶：把连续特征先离散到有限个桶，例如默认 255 或 256 个桶。这样找切分点时，复杂度从“遍历大量原始取值”变成“遍历固定桶边界”。</li>
<li>Leaf-wise 生长：每一轮只扩展当前最值得继续分裂的叶子，在相同叶子预算下往往比 level-wise 更快压低误差。</li>
<li>GOSS（Gradient-based One-Side Sampling）：优先保留梯度较大的样本，因为这些样本更能代表当前模型最需要修正的区域；对梯度较小的样本只随机保留一部分，以减少计算量。</li>
<li>EFB（Exclusive Feature Bundling）：把互斥的稀疏特征打包到同一组表示里，降低高维稀疏输入的有效维度。</li>
</ul>
<p>其中 GOSS 和 EFB 的意义非常工程化。前者服务于“大样本下不必每轮都看全量样本”，后者服务于“高维稀疏特征下不必把所有稀疏列都单独维护”。也正因为如此，LightGBM 尤其适合推荐、广告、风控这类既大规模又高度稀疏的表格任务。</p>
<p>LightGBM 也因此更依赖约束配套。leaf-wise 生长虽然更激进，但如果不同时限制 <pre class="crayon-plain-tag">max_depth</pre>、<pre class="crayon-plain-tag">num_leaves</pre>、<pre class="crayon-plain-tag">min_data_in_leaf</pre> 这类参数，局部树会很快长得过深，训练误差降得很漂亮，验证集却未必跟着受益。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>将连续特征离散到有限个桶，并统计桶级梯度信息。</li>
<li>在每个节点上基于桶统计量搜索最优切分。</li>
<li>选择全局增益最大的叶子继续分裂。</li>
<li>使用最大深度、最大叶子数、最小叶子样本数等约束抑制过拟合。</li>
<li>预测时沿树路径到达叶子并累加所有树的输出。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在大规模推荐粗排任务中，输入特征通常极高维且高度稀疏。LightGBM 能在保留较强拟合能力的同时显著缩短训练时间，因此非常适合需要频繁重训的工业流水线。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：训练快、内存占用低，对大规模稀疏特征友好。</li>
<li>局限：叶子优先策略若约束不足，局部树会过深，过拟合风险更高。</li>
<li>适用场景：超大规模表格数据、稀疏特征建模、追求训练效率的工业场景。</li>
</ul>
<div class="blog_h3"><span class="graybg">CatBoost</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>CatBoost 是另一条非常重要的 Boosting 工业路线。它关注的核心问题不是“比 XGBoost 更通用”或“比 LightGBM 更快”，而是：<span style="background-color: #c0c0c0;">当数据里有大量高基数类别特征时，如何在不引入严重数据泄露和预测偏移的前提下，把这些特征真正用好</span>。广告、推荐、电商、用户画像、商品属性等任务里，这个问题尤其关键。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>CatBoost 的两项代表性设计是有序目标统计（Ordered Target Statistics）与有序提升（Ordered Boosting）。前者处理类别特征，后者处理训练偏移。它们共同服务于同一个目标：不要让模型在训练时偷看到本不该知道的目标信息。</p>
<div class="blog_h4"><span class="graybg">类别特征为什么难</span></div>
<p>对树模型而言，类别特征的难点不只是“字符串不能直接喂进去”，更在于很多类别列的取值空间极大，例如用户 ID、商品 ID、城市、品牌、广告位、渠道来源。若简单做 one-hot 编码，维度会迅速膨胀；若直接用目标均值编码，又很容易把标签信息泄露进训练过程，导致离线效果虚高、线上泛化变差。</p>
<p>CatBoost 的思路是：用类别对应的目标统计量来表示类别，但计算某个样本的统计量时，只允许使用它之前样本的信息，而不允许看见它自己以及它之后的样本标签。这样得到的类别表示虽然仍然利用了监督信号，却显著降低了数据泄露与目标泄露风险。</p>
<div class="blog_h4"><span class="graybg">有序目标统计与有序提升</span></div>
<p>设某个类别值为 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，其编码值可以理解为“在训练顺序中，当前位置之前出现过的同类样本的目标统计量，再配合一个全局先验做平滑”。这样一来，模型在看到当前样本时，只能利用“过去”信息，不能直接偷看当前标签。</p>
<p>同样的思路也延伸到 Boosting 训练本身。传统目标编码或普通 Boosting 在训练时，常会让同一批样本之间发生微妙的信息穿透，形成预测偏移（Prediction Shift）。CatBoost 通过 ordered boosting 尽量让每一步的残差估计更接近“真正未见样本上的估计误差”，从而让训练分布和推理分布更一致。</p>
<div class="blog_h4"><span class="graybg">对称树</span></div>
<p>CatBoost 还大量使用对称树（Symmetric Tree，也常称 Oblivious Tree）：同一层的所有节点共享同一个切分规则。这种树结构比一般决策树更受约束，表达上没那么自由，但推理路径规整、计算高效，也更利于工程实现和高吞吐推断。</p>
<p>从工程角度看，CatBoost 的价值就在于把“类别特征处理”从手工特征工程里拿回模型内部。很多场景里，团队不再需要先在外部纠结 one-hot、频次编码、目标编码、平滑规则和泄露控制，而是可以把类别列直接交给模型，让训练流程自己处理这套问题。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：对类别特征支持最好，默认配置往往更稳，推理效率高。</li>
<li>局限：当类别特征优势不明显时，未必总能胜过 XGBoost 或 LightGBM；训练生态也没有前两者那么普遍。</li>
<li>适用场景：类别特征很多、ID 类特征很多、需要尽量减少手工编码工作的表格任务。</li>
</ul>
<div class="blog_h2"><span class="graybg">概率模型</span></div>
<p>这一类方法处理的核心问题是：模型不仅要给出“预测是什么”，还要明确回答“这个结果有多可信、数据是如何生成的、隐藏结构是什么”。因此它们直接建模概率分布（Probability Distribution）、隐变量（Latent Variables）或序列依赖关系，在分类、密度估计、软聚类、序列推断与不确定性表达中具有统一优势。</p>
<div class="blog_h3"><span class="graybg">朴素贝叶斯</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>朴素贝叶斯（Naive Bayes）用于分类问题。给定特征 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，它要估计样本属于每个类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的后验概率 <span displaypfx="inline-" class="mathjax-container">\(p(y|\boldsymbol{x})\)</span>。该方法尤其适合高维稀疏、小样本或需要快速概率基线的任务。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>朴素贝叶斯的核心假设是：在给定类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的条件下，各个特征条件独立（Conditional Independence）。这个假设通常并不严格成立，但它把高维联合分布的估计问题，转化为多个一维条件分布的估计问题。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>由贝叶斯公式：</p>
<span displaypfx="" class="mathjax-container">\[p(y|\boldsymbol{x})=\frac{p(\boldsymbol{x}|y)p(y)}{p(\boldsymbol{x})}\]</span>
<p>这个贝叶斯公式把后验概率拆成三部分： <span displaypfx="inline-" class="mathjax-container">\(p(y|\boldsymbol{x})\)</span> 是看到特征后属于类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的概率； <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x}|y)\)</span> 是该类别生成这组特征的可能性； <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 是类别先验； <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x})\)</span> 则是对所有类别做归一化的总证据。</p>
<p>由于 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x})\)</span> 对所有类别相同，分类时只需比较：</p>
<span displaypfx="" class="mathjax-container">\[p(y|\boldsymbol{x})\propto p(y)\prod_{j=1}^{d}p(x_j|y)\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(\propto\)</span> 表示“成正比”。因为对同一个样本来说，分母 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x})\)</span> 对所有类别都相同，所以比较类别大小时只需看右边：先验 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 乘上每个特征条件概率 <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span> 的连乘积。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 是先验概率（Prior）， <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span> 是条件似然（Likelihood）。根据特征类型不同，可以得到高斯朴素贝叶斯、伯努利朴素贝叶斯、多项式朴素贝叶斯等变体。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>从训练集统计每个类别的先验概率 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span>。</li>
<li>估计每个类别下各特征的条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span>。</li>
<li>推断时计算各类别的对数后验分数 <span displaypfx="inline-" class="mathjax-container">\(\log p(y)+\sum_j \log p(x_j|y)\)</span>。</li>
<li>选择分数最大的类别作为预测输出。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在垃圾邮件检测中，若某些词在垃圾邮件中显著更常见，那么这些词对应的条件概率会被估得更大，从而把包含这些词的邮件判到垃圾类别。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：训练快、推断快、对高维稀疏特征友好。</li>
<li>局限：条件独立假设常被破坏；概率校准有时较弱。</li>
<li>适用场景：文本分类、垃圾邮件过滤、简单可靠的概率基线。</li>
</ul>
<div class="blog_h3"><span class="graybg">高斯混合模型（GMM）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>高斯混合模型（Gaussian Mixture Model, GMM）处理的是“数据由多个潜在高斯成分混合生成”的建模问题。与 K-Means 只输出硬划分不同，GMM 希望估计每个样本属于各个簇的概率，并允许不同簇有不同的协方差结构。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>GMM 引入潜变量 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 表示样本来自哪一个高斯成分。整体分布由多个高斯分布按混合系数加权得到，因此每个样本对各个簇的归属是软的（Soft Assignment），并以概率形式表达。</p>
<p>一个直观比喻是：把数据想成混在一起的几类人群，只能看到每个人的外在特征，却看不到他原本属于哪一类。每一类人群都有自己的“中心位置”和“分散形状”，对应一个高斯成分；整个数据集则像这些人群按不同比例叠在一起形成的总体分布。与 K-Means 把每个样本硬塞进某一个簇不同，GMM 会给出“这个样本更像第 1 类，也有一部分像第 2 类”这样的软归属结果。</p>
<p>因此，混合系数 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\)</span> 可以理解为各类人群在总体中的占比，均值 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}_k\)</span> 是每类人群的中心，协方差 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\Sigma}_k\)</span> 描述该群体沿不同方向的扩散方式，而责任度 <span displaypfx="inline-" class="mathjax-container">\(\gamma_{ik}\)</span> 则表示“样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 有多大程度属于第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 类”。这也是 GMM 比 K-Means 更灵活的原因：它允许边界模糊，也允许簇具有不同大小、方向和椭球形状。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>对样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，GMM 的概率密度写为：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{x})=\sum_{k=1}^{K} \pi_k \mathcal{N}(\boldsymbol{x}\mid \boldsymbol{\mu}_k,\boldsymbol{\Sigma}_k)\]</span>
<p>这条式子说明 GMM 的整体密度不是由单个高斯给出，而是 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个高斯成分的加权和。 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\)</span> 决定第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个成分在总体中的占比， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(\boldsymbol{x}\mid \boldsymbol{\mu}_k,\boldsymbol{\Sigma}_k)\)</span> 描述样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 在该成分下有多典型。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\)</span> 是混合系数，满足 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\ge 0\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\sum_k \pi_k=1\)</span>。给定样本后，其属于第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个成分的责任度（Responsibility）为：</p>
<span displaypfx="" class="mathjax-container">\[\gamma_{ik}=p(z_i=k|\boldsymbol{x}_i)=\frac{\pi_k \mathcal{N}(\boldsymbol{x}_i\mid \boldsymbol{\mu}_k,\boldsymbol{\Sigma}_k)}{\sum_{j=1}^{K}\pi_j \mathcal{N}(\boldsymbol{x}_i\mid \boldsymbol{\mu}_j,\boldsymbol{\Sigma}_j)}\]</span>
<p>责任度 <span displaypfx="inline-" class="mathjax-container">\(\gamma_{ik}\)</span> 是“样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 属于成分 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的后验概率”。分子表示“成分 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 解释这个样本的能力”，分母则把所有成分对该样本的解释能力加总起来做归一化，因此所有 <span displaypfx="inline-" class="mathjax-container">\(\gamma_{ik}\)</span> 加起来等于 1。</p>
<p>GMM 的参数通常通过期望最大化（Expectation-Maximization, EM）求解。E 步计算责任度，M 步更新参数：</p>
<span displaypfx="" class="mathjax-container">\[N_k=\sum_{i=1}^{N}\gamma_{ik},\qquad \pi_k=\frac{N_k}{N}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(N_k\)</span> 不是成分 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的硬计数，而是软计数：每个样本只按自己的责任度贡献一部分。于是混合系数更新为 <span displaypfx="inline-" class="mathjax-container">\(\pi_k=\frac{N_k}{N}\)</span>，表示第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个成分在总体中的相对权重。</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{\mu}_k=\frac{1}{N_k}\sum_{i=1}^{N}\gamma_{ik}\boldsymbol{x}_i,\qquad \boldsymbol{\Sigma}_k=\frac{1}{N_k}\sum_{i=1}^{N}\gamma_{ik}(\boldsymbol{x}_i-\boldsymbol{\mu}_k)(\boldsymbol{x}_i-\boldsymbol{\mu}_k)^\top\]</span>
<p>均值更新式说明：每个样本按责任度大小对中心 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}_k\)</span> 做加权贡献；协方差更新式则是在该加权中心周围统计离散程度。责任度越大，样本对该成分的中心和形状影响越大。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>初始化混合系数、均值和协方差。</li>
<li>E 步：计算每个样本对各高斯成分的责任度。</li>
<li>M 步：根据责任度更新参数。</li>
<li>重复 E / M，直到对数似然收敛。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在用户分群中，若人群天然分成多个椭球状子群体，那么 GMM 不仅能给出簇划分，还能输出“某个用户属于每个群体的概率”，这比 K-Means 的硬分配更细致。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：软聚类、概率解释清晰、能建模不同协方差形状。</li>
<li>局限：对初始化敏感；高维时协方差估计代价高。</li>
<li>适用场景：软聚类、密度估计、带概率解释的聚类分析。</li>
</ul>
<div class="blog_h3"><span class="graybg">隐马尔可夫模型（HMM）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>隐马尔可夫模型（Hidden Markov Model, HMM）用于序列建模，尤其适合处理序列标注（Sequence Labeling）问题。序列标注的任务形式是：给定一个按时间或位置排列的输入序列，为序列中的每个位置分配一个标签。例如，在词性标注中，句子“我爱北京天安门”的每个词或字都需要对应一个词性标签；在语音识别中，一段连续声学信号需要对应到一串离散文字或音素标签。</p>
<p>HMM 是序列模型中的经典早期方法，在语音识别等领域长期具有重要地位。它的优势在于结构清晰、推断高效，能够以较低的计算成本处理序列决策；与此同时，它主要依赖局部状态转移和局部发射关系，所使用的上下文信息相对有限。</p>
<p>HMM 的建模方式是：观测序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_{1:T}\)</span> 可以看到，但生成这些观测的状态序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{z}_{1:T}\)</span> 看不到。这里的隐藏状态通常对应词性、命名实体类别或发音状态等潜在标签。围绕这一模型，核心问题通常包括计算某段观测序列的概率、恢复最可能的隐藏状态路径，以及估计模型参数。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>HMM 假设隐藏状态满足一阶马尔可夫性（First-order Markov Property）：当前状态只依赖前一个状态；同时每个观测只依赖当前状态。一个直接的比喻是：无法直接看到远方朋友每天所处的天气，但可以持续看到他发布的活动，例如“去游泳”“去逛街”或“在家睡觉”。在这个比喻里，天气是隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span>，活动是观测 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span>。HMM 的生成逻辑正是<span style="background-color: #c0c0c0;">先生成隐藏状态序列，再由每个状态发射对应观测</span>。</p>
<p>如果今天是晴天，明天仍然晴天的概率通常较高；如果今天下雨，朋友在家休息的概率通常高于去游泳。这分别对应 HMM 中的状态转移概率 <span displaypfx="inline-" class="mathjax-container">\(p(z_t|z_{t-1})\)</span> 和发射概率 <span displaypfx="inline-" class="mathjax-container">\(p(x_t|z_t)\)</span>。因此，HMM 把序列建模为一个“剧本模拟器”：先按转移规律生成一条看不见的天气轨迹，再按照每一天的天气生成看得见的活动记录。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">HMM 概念</td>
<td style="text-align: center;">比喻中的对应物</td>
<td style="text-align: center;">含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span></td>
<td>每天的天气</td>
<td>真实存在，但观察者不能直接看到。</td>
</tr>
<tr>
<td>观测 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span></td>
<td>朋友圈里的活动</td>
<td>可以直接看到，用来反推隐藏状态。</td>
</tr>
<tr>
<td>初始分布 <span displaypfx="inline-" class="mathjax-container">\(p(z_1)\)</span></td>
<td>第一天各种天气出现的概率</td>
<td>序列从什么状态开始。</td>
</tr>
<tr>
<td>转移概率 <span displaypfx="inline-" class="mathjax-container">\(p(z_t|z_{t-1})\)</span></td>
<td>天气从今天到明天的变化规律</td>
<td>例如“晴天后仍是晴天”的概率较高。</td>
</tr>
<tr>
<td>发射概率 <span displaypfx="inline-" class="mathjax-container">\(p(x_t|z_t)\)</span></td>
<td>某种天气下出现某种活动的概率</td>
<td>例如下雨时更可能“在家睡觉”。</td>
</tr>
<tr>
<td>隐藏状态序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{z}_{1:T}\)</span></td>
<td>整段天气变化轨迹</td>
<td>模型希望恢复的潜在过程。</td>
</tr>
<tr>
<td>观测序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_{1:T}\)</span></td>
<td>整段活动记录</td>
<td>模型的输入数据。</td>
</tr>
</tbody>
</table>
<p>这个比喻也解释了 HMM 的优势与局限。它简单、快速、可解释，因为联合概率能够拆成局部转移和局部发射的乘积，并可用动态规划高效推断。但它的条件独立假设也很强：某一天的活动只由当天的天气决定，不直接依赖前后活动。在词性标注等任务中，一个词的标签往往同时受左右上下文影响，这种假设就会限制表达能力。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>HMM 的联合分布写为：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{x}_{1:T},\boldsymbol{z}_{1:T})=p(z_1)\prod_{t=2}^{T}p(z_t|z_{t-1})\prod_{t=1}^{T}p(x_t|z_t)\]</span>
<p>这条联合分布把一整条序列拆成三部分： <span displaypfx="inline-" class="mathjax-container">\(p(z_1)\)</span> 是初始状态概率， <span displaypfx="inline-" class="mathjax-container">\(\prod_{t=2}^{T}p(z_t|z_{t-1})\)</span> 是状态转移链， <span displaypfx="inline-" class="mathjax-container">\(\prod_{t=1}^{T}p(x_t|z_t)\)</span> 是每个状态发射观测的概率。正因为有这种乘积分解，HMM 才能用动态规划高效求解。</p>
<p>若记初始分布为 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span>，转移矩阵为 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，发射分布为 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>，则前向算法（Forward Algorithm）的核心量为：</p>
<span displaypfx="" class="mathjax-container">\[\alpha_t(j)=p(x_{1:t},z_t=j)\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(\alpha_t(j)\)</span> 的含义是：看到前 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个观测，同时当前时刻状态恰好是第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个状态的联合概率。它把“历史观测信息”和“当前落在哪个状态”压缩成一个可递推的中间量。</p>
<p>其递推为：</p>
<span displaypfx="" class="mathjax-container">\[\alpha_t(j)=\left[\sum_i \alpha_{t-1}(i)A_{ij}\right]B_j(x_t)\]</span>
<p>递推式可以分成两步看：先对所有上一步状态 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 求和，用 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}\)</span> 累积“从 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 转到 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的可能性”；再乘上 <span displaypfx="inline-" class="mathjax-container">\(B_j(x_t)\)</span>，表示当前状态 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 发射出观测 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 的概率。</p>
<p>最可能状态路径可由维特比算法（Viterbi Algorithm）求解；参数估计通常采用 Baum-Welch 算法，即 HMM 上的 EM。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>定义状态空间、转移概率和发射概率。</li>
<li>若参数已知，用前向 / 后向算法做概率计算，用维特比算法做解码。</li>
<li>若参数未知，用 Baum-Welch 在训练集上迭代估计参数。</li>
<li>根据任务输出状态路径、序列概率或边缘状态分布。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在词性标注中，观测是单词序列，隐藏状态是词性标签。HMM 会同时利用“某个词在某种词性下更常见”的发射规律和“词性之间如何转移”的序列规律完成整体解码。</p>
<p>例如，对序列“我 / 爱 / 北京 / 天安门”，模型看到的是词本身，看不到的是背后的标签序列。HMM 会比较多种候选路径，如“代词（Pronoun）→ 动词（Verb）→ 专有名词（Proper Noun）→ 专有名词（Proper Noun）”与其他不合理组合的概率。由于“我”更容易由代词状态发射，“爱”更容易由动词状态发射，而“代词后接动词、动词后接名词性成分”又符合常见转移规律，这条标签路径就会获得更高概率。这个过程可以理解为：模型一边根据词本身猜标签，一边检查整条词性路径是否顺畅。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/hmm.jpg"><img class="alignnone size-full wp-image-41251" src="https://blog.gmem.cc/wp-content/uploads/2026/03/hmm.jpg" alt="hmm" width="1408" height="768" /></a></p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：序列结构清晰，动态规划推断高效，可解释性强。</li>
<li>局限：一阶马尔可夫假设较强，表达能力有限。</li>
<li>适用场景：基础序列标注、时间状态切换建模、中小规模序列任务。</li>
</ul>
<div class="blog_h3"><span class="graybg">条件随机场（CRF）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>条件随机场（Conditional Random Field, CRF）主要用于结构化预测（Structured Prediction），尤其是序列标注。它处理的问题是：在给定输入序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的条件下，如何联合预测输出标签序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span>，并显式建模标签之间的依赖关系。</p>
<p>CRF 可以看作对 HMM 的进一步发展：它不再试图解释输入序列如何被生成，而是直接在给定输入的条件下预测最合理的标签结构。通过引入全局特征和更灵活的上下文依赖，CRF 缓解了 HMM 仅依赖局部独立假设所带来的信息不足。在深度学习广泛进入 NLP 之前，CRF 长期是各类标注任务中的核心方法。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>CRF 的核心是在给定观测序列的条件下，对整条标签序列进行全局评分，而不是显式描述数据生成过程。仍以天气和活动的比喻来理解：已知一整周的活动记录后，CRF 会把各种可能的天气序列都拿来比较，判断哪一种与这组活动整体最一致。它建模的是条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{y}|\boldsymbol{x})\)</span>，而不是 HMM 那样的联合分布 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x},\boldsymbol{y})\)</span>。</p>
<p>这种“全局把控”体现在两个层面。第一，CRF 的打分对象是完整的标签路径，而不是每个位置互相独立的局部决策；第二，打分依据可以是灵活定义的特征函数（Feature Function）。例如，若连续三天都出现“游泳”，对应“连续晴天”的标签组合就应得到更高分；若一周中出现“滑雪”这类活动，与“夏天”一致的天气标签组合就应被显著压低。特征函数可以同时利用当前位置、前后邻域以及相邻标签之间的组合关系。</p>
<p>因此，CRF 可以视为一个<span style="background-color: #c0c0c0;">面向整条序列的打分系统</span>：输入固定，模型比较不同标签序列的相对合理性，并选择得分最高的一条。它的优势是能够充分利用复杂上下文和标签依赖，在序列标注任务中通常比 HMM 更准确；代价是训练和推断都更重，需要计算整条序列上的归一化与动态规划。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>线性链 CRF 的条件分布通常写为：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{y}|\boldsymbol{x})=\frac{1}{Z(\boldsymbol{x})}\exp\left(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\right)\]</span>
<p>这个式子最好分三层看。先看最里面的 <span displaypfx="inline-" class="mathjax-container">\(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\)</span>：它表示对整条标签序列逐位置累积特征得分。再看外面的指数 <span displaypfx="inline-" class="mathjax-container">\(\exp(\cdot)\)</span>：它把“总得分”变成一个始终为正的数，而且总得分越大，这个数就越大。最后再除以 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span>，把这些正数正规化成概率。因此，CRF 的计算顺序可以理解为：<span style="background-color: #c0c0c0;">先打分，再变成正权重，最后归一化成概率</span>。</p>
<p>为了看清楚 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 在做什么，可以先临时把分子记成一个“未归一化分数”：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{p}(\boldsymbol{y}|\boldsymbol{x})=\exp\left(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\right)\]</span>
<p>这里特意加波浪号，是为了强调它<span style="background-color: #c0c0c0;">还不是概率</span>。原因很简单：把所有可能标签序列的 <span displaypfx="inline-" class="mathjax-container">\(\tilde{p}(\boldsymbol{y}|\boldsymbol{x})\)</span> 加起来，结果一般不会恰好等于 1。它只是每条路径的相对权重，表达“这条标签路径有多合理”。</p>
<p>这时配分函数（Partition Function）就出现了：</p>
<span displaypfx="" class="mathjax-container">\[Z(\boldsymbol{x})=\sum_{\boldsymbol{y}} \exp\left(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\right)\]</span>
<p>之所以你会觉得它“和上面那个公式一样”，是因为它确实就是<span style="background-color: #c0c0c0;">把上式分子对所有可能的标签序列整体求和</span>。上面的 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{y}|\boldsymbol{x})\)</span> 针对的是某一条固定标签序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span>；而这里的 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 不是看某一条路径，而是把所有可能路径都算一遍，再全部加起来。所以它不是“另一条几乎一样的公式”，而是“分子在全标签空间上的总和”。</p>
<p>于是条件概率就变成：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{y}|\boldsymbol{x})=\frac{\tilde{p}(\boldsymbol{y}|\boldsymbol{x})}{Z(\boldsymbol{x})}\]</span>
<p>现在这个式子就容易理解了：某条路径的概率，等于“这条路径自己的权重”除以“所有路径权重的总和”。这和 softmax 的归一化逻辑完全一致，只不过 softmax 是在有限个类别上归一化，而 CRF 是在所有可能的标签序列上归一化。</p>
<p>配分函数这个名字来自统计物理，但在这里不需要物理背景也能理解：它本质上就是一个<span style="background-color: #c0c0c0;">归一化常数</span>。没有它，模型只能说“路径 A 比路径 B 更合理”；有了它，模型才能进一步说“路径 A 的概率是多少”。也正因为 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 需要把所有可能标签路径都考虑进去，CRF 训练时才必须借助动态规划，而不能暴力枚举。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(f_k(y_{t-1},y_t,\boldsymbol{x},t)\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个特征函数，描述当前位置、相邻标签和输入之间的某种局部模式。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda_k\)</span>：该特征的权重；越大表示模型越重视这个模式。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>：固定输入序列；在讨论 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 时，它不变。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span>：某一条候选标签序列；计算 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 时要对所有可能的 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span> 求和。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_{t=1}^{T}\sum_k \lambda_k f_k(\cdot)\)</span>：整条标签序列的总得分。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\exp(\cdot)\)</span>：把总得分映射成正权重，并放大高分路径与低分路径之间的差异。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span>：所有候选标签路径未归一化权重的总和，用于把相对权重变成概率。</li>
</ul>
<p>训练时最大化条件对数似然，解码时用维特比算法寻找最优标签序列。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>定义特征函数，描述输入与标签、标签与标签之间的关系。</li>
<li>用前向后向算法计算配分函数与梯度。</li>
<li>通过梯度法优化参数 <span displaypfx="inline-" class="mathjax-container">\(\lambda_k\)</span>。</li>
<li>推断时用维特比算法输出最优标签序列。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在命名实体识别（NER）中，局部分类器可能会把某个词单独判成人名，但 CRF 会进一步考虑相邻标签是否合法，从而减少孤立的局部误判。这里“是否合法”指的不是语法合法，而是<span style="background-color: #c0c0c0;">标签序列是否符合该任务定义下允许出现的邻接模式</span>。例如，在常见的 BIO 标注体系中， <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 表示 Begin，即实体开始； <span displaypfx="inline-" class="mathjax-container">\(I\)</span> 表示 Inside，即实体内部； <span displaypfx="inline-" class="mathjax-container">\(O\)</span> 表示 Outside，即不属于任何实体。也有一些任务使用 BIOES 标注体系，其中 <span displaypfx="inline-" class="mathjax-container">\(E\)</span> 表示 End， <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 表示 Single。与 BIO 相比，BIOES 会把实体结束位置和单字实体显式标出来，因此边界信息更细。于是，标签 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER\)</span> 表示一个人名实体的开始， <span displaypfx="inline-" class="mathjax-container">\(I\text{-}PER\)</span> 表示该人名实体的内部， <span displaypfx="inline-" class="mathjax-container">\(O\)</span> 表示不属于任何实体。于是， <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER\rightarrow I\text{-}PER\)</span> 是常见且合法的相邻转移， <span displaypfx="inline-" class="mathjax-container">\(O\rightarrow B\text{-}PER\)</span> 也合法；但 <span displaypfx="inline-" class="mathjax-container">\(O\rightarrow I\text{-}PER\)</span> 通常不合法，因为一个实体内部标签不能无缘无故直接开始，前面必须先有对应的开始标签。同样， <span displaypfx="inline-" class="mathjax-container">\(B\text{-}LOC\rightarrow I\text{-}PER\)</span> 这类“实体类型突然不一致”的连接也通常应被压低分数。</p>
<p>例如，在句子“张三 在 北京 工作”中，若任务要识别人名（PER）和地点（LOC），一个合理的 BIO 标注序列可能是“张三 / 在 / 北京 / 工作”对应 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER, O, B\text{-}LOC, O\)</span>。这里首先需要区分两件事：<span style="background-color: #c0c0c0;">某个位置更像哪一种实体类型</span>，以及<span style="background-color: #c0c0c0;">这些局部判断连起来是否构成一条合理的标签序列</span>。前者主要来自输入本身提供的证据，例如“张三”在词形上很像中文人名，“北京”本身强烈像地点名词，而“在 北京 工作”这种上下文也会继续加强“北京是地点”的判断。传统 CRF 会把这些信息写成特征函数，例如“当前词是否常见于人名词表”“当前词是否带有地名后缀”“左邻词是否是介词‘在’”“右邻词是否是动作词‘工作’”等；每个特征都会给某个候选标签加分或减分。</p>
<p>CRF 的作用是在这些局部类型证据之上，再做一次全局一致性的联合解码。换言之，实体类型 A 还是 B，并不是和 CRF 完全无关；但也不是由 CRF 凭空决定的。更准确地说，<span style="background-color: #c0c0c0;">实体类型的语义判断主要来自输入特征，CRF 负责把这些局部判断放到整条序列里统一协调</span>。例如，如果“北京”这个位置单看局部证据时，对 LOC 的分数高于 PER，那么 CRF 会倾向保留“地点”这一判断；但它还会进一步检查，当前位置前后的标签连接是否自然。如果某条候选路径把“张三”切成 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER, O\)</span>，或者把“北京”接成 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER\rightarrow I\text{-}LOC\)</span>，即使某个局部位置的分数不低，整条路径仍会因为边界断裂或类型转移不一致而被整体压低。于是 CRF 做的不是单点分类，而是“局部类型打分 + 全局路径约束”的联合决策。</p>
<p>从工程实现上看，这个分工在不同年代的模型里表现形式不同。在传统 CRF 中，“局部类型打分”通常来自人工设计的特征模板，例如当前词、前后词、词性、字形、是否出现在人名词典或地名词典中；CRF 再把这些手工特征组合成整条序列的全局分数。在 BiLSTM-CRF 或 BERT-CRF 这类现代模型中，局部证据不再主要依赖手工模板，而是先由 BiLSTM 或 BERT 生成上下文化表示（Contextual Representation），再由线性层给出每个位置对各标签的局部分数，最后仍由 CRF 层负责建模标签转移和整条路径解码。也就是说，上游编码器主要回答“这个位置像什么类型”，CRF 层主要回答“这些位置判断拼在一起是否构成一条最合理的标签序列”。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-1.jpg"><img class="alignnone size-full wp-image-41263" src="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-1.jpg" alt="crf-1" width="1408" height="768" /></a></p>
<p>类似的全局打分思想也可以推广到依存句法分析（Dependency Parsing, DEP）这类结构化任务。句子中的每个词都需要找到自己的中心词（head），模型的目标不是孤立地决定“这个词连向谁”，而是评估整棵依存树是否合理。例如，在“她 喜欢 自然语言处理”中，“喜欢”通常更可能作为中心谓词，“她”依附到“喜欢”形成主谓关系，“自然语言处理”整体依附到“喜欢”形成宾语关系。若某个局部决策把“她”错误地连到“自然语言处理”，单看两个词的局部相似度未必很低，但放到整棵树的全局结构中就会显得不协调。CRF 的价值正体现在这里：它通过全局归一化和结构约束，偏好整体验证一致的输出结构，而不是一组彼此冲突的局部最优决策。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-2.jpg"><img class="alignnone size-full wp-image-41257" src="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-2.jpg" alt="crf-2" width="1408" height="768" /></a></p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：适合结构化输出，能显式编码标签依赖。</li>
<li>局限：训练与推断成本高于普通独立分类器。</li>
<li>适用场景：序列标注、分词、命名实体识别等条件结构化预测任务。</li>
</ul>
<div class="blog_h2"><span class="graybg">近邻模型</span></div>
<p>这一类方法处理的核心问题是：在缺少可靠全局函数形式时，是否可以直接依赖“局部相似样本通常有相似输出”这一假设完成预测。近邻模型不急于学习一个显式参数化函数，而是把相似性度量（Similarity Metric）本身作为建模中心：先找邻居，再由邻居投票、平均或加权得到结果。</p>
<div class="blog_h3"><span class="graybg">K 近邻（KNN）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>K 近邻（K-Nearest Neighbors, KNN）用于分类与回归。给定一个待预测样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，它要根据训练集中与其最相似的样本来决定输出。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>KNN 的基本假设是局部平滑性（Local Smoothness）：在特征空间中彼此接近的样本，往往具有相近的标签或数值。它不显式学习参数化模型，训练集本身就是局部比较的参照集。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定距离函数 <span displaypfx="inline-" class="mathjax-container">\(d(\boldsymbol{x},\boldsymbol{x}')\)</span> 与邻居数 <span displaypfx="inline-" class="mathjax-container">\(K\)</span>，若 <span displaypfx="inline-" class="mathjax-container">\(N_K(\boldsymbol{x})\)</span> 表示 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个最近邻，则分类时可写为：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\mathrm{mode}\left(\{y_i:(\boldsymbol{x}_i,y_i)\in N_K(\boldsymbol{x})\}\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(N_K(\boldsymbol{x})\)</span> 是样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个最近邻集合，花括号中收集的是这些邻居的标签， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{mode}(\cdot)\)</span> 则返回出现次数最多的类别。也就是说，KNN 分类本质上就是“看最近的邻居们大多数是谁”。</p>
<p>回归时常取邻居标签平均：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\frac{1}{K}\sum_{(\boldsymbol{x}_i,y_i)\in N_K(\boldsymbol{x})}y_i\]</span>
<p>回归版 KNN 只是把“多数投票”换成“数值平均”。 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{K}\sum\)</span> 表示把最近 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个邻居的输出做均值，因此预测值会受到局部邻域中所有数值样本的共同影响。</p>
<p>若距离使用欧氏距离，则默认所有特征尺度可比，因此标准化（Standardization）通常是必要前处理。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>训练阶段几乎不做参数学习，只保存全部训练样本。</li>
<li>推断时计算待测样本到训练样本的距离。</li>
<li>选出最近的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个样本。</li>
<li>分类取多数投票，回归取平均或距离加权平均。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在简单的手写数字识别中，如果一张新图片与训练集中大量“3”的图像都很接近，而与“8”的图像明显更远，那么 KNN 会依据局部邻域投票把它判为 3。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：概念简单、无需复杂训练、局部非线性表达能力强。</li>
<li>局限：推断成本高；对特征尺度和无关特征敏感；维度灾难会削弱距离判别力。</li>
<li>适用场景：中小规模数据、快速基线、基于相似度的简单分类与回归。</li>
</ul>
<div class="blog_h2"><span class="graybg">聚类</span></div>
<p>聚类处理的核心问题是：在没有标签的前提下，如何仅根据样本之间的几何关系、密度结构或层次关系，把数据自动分成若干组。这里不存在唯一正确的“簇”定义：K-Means 假设簇围绕中心分布，层次聚类强调多粒度组织结构，DBSCAN / HDBSCAN 则把簇理解为高密度连通区域。因此，聚类算法的选择本质上是在选择“什么样的结构应被视为同一类”。</p>
<div class="blog_h3"><span class="graybg">K-Means</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>K-Means 处理的是无监督聚类问题：给定一组没有标签的样本，希望把它们分成 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个簇，使同一簇内样本尽量接近，不同簇之间尽量分开。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>K-Means 用“每个簇由一个中心点代表”的方式近似数据分布。算法不断重复两件事：把样本分配给最近的中心；再用簇内样本均值更新中心。它本质上是在最小化簇内平方误差。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/k-means.jpg"><img class="alignnone size-full wp-image-41287" src="https://blog.gmem.cc/wp-content/uploads/2026/03/k-means.jpg" alt="k-means" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>目标函数为：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\{c_k\},\{z_i\}} \sum_{i=1}^{N} \|\boldsymbol{x}_i-c_{z_i}\|_2^2\]</span>
<p>这个目标里， <span displaypfx="inline-" class="mathjax-container">\(c_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个簇中心， <span displaypfx="inline-" class="mathjax-container">\(z_i\)</span> 表示样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 被分到哪个簇， <span displaypfx="inline-" class="mathjax-container">\(\|\boldsymbol{x}_i-c_{z_i}\|_2^2\)</span> 是样本到所属簇中心的平方距离。K-Means 想做的，就是让所有样本离各自中心都尽可能近。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(c_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个簇中心， <span displaypfx="inline-" class="mathjax-container">\(z_i\in\{1,\dots,K\}\)</span> 表示样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_i\)</span> 属于哪个簇。固定簇分配时，最优中心是该簇样本均值：</p>
<span displaypfx="" class="mathjax-container">\[c_k=\frac{1}{|S_k|}\sum_{\boldsymbol{x}_i\in S_k} \boldsymbol{x}_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(S_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个簇里所有样本的集合， <span displaypfx="inline-" class="mathjax-container">\(|S_k|\)</span> 是该簇样本数。这个更新式说明簇中心就是簇内样本的算术平均，因此 K-Means 的“中心”确实是均值意义上的代表点。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>初始化 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个簇中心。</li>
<li>分配步骤：把每个样本分到最近中心。</li>
<li>更新步骤：用各簇样本均值更新中心。</li>
<li>重复迭代直到簇分配稳定或目标下降很小。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在用户分群中，若特征是消费频次与客单价，K-Means 往往会自动形成“高频低客单”“低频高客单”“中频中客单”等若干均值中心明确的群体。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：实现简单、可扩展、训练速度快。</li>
<li>局限：需要预先指定 <span displaypfx="inline-" class="mathjax-container">\(K\)</span>；对初始化与离群点敏感；不适合非凸簇。</li>
<li>适用场景：簇近似球形、需要快速聚类或作为预处理的任务。</li>
</ul>
<div class="blog_h3"><span class="graybg">层次聚类</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>层次聚类（Hierarchical Clustering）输出的是一个由粗到细的聚类层次结构，因此簇数可以在观察树状图后再决定。它适合需要观察“簇是如何逐步合并或拆分”的任务。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>凝聚式层次聚类（Agglomerative Clustering）从每个样本单独成簇开始，每一步合并当前最相近的两个簇；分裂式层次聚类则从一个大簇开始不断拆分。实践中更常见的是凝聚式版本。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/h-clustering.jpg"><img class="alignnone size-full wp-image-41293" src="https://blog.gmem.cc/wp-content/uploads/2026/03/h-clustering.jpg" alt="h-clustering" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>关键在于定义簇间距离。常见联接准则（Linkage Criteria）包括：</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{single}}(A,B)=\min_{\boldsymbol{x}\in A,\boldsymbol{y}\in B} d(\boldsymbol{x},\boldsymbol{y})\]</span>
<p>单链距离只看两簇之间最近的那一对点，因此它很容易把“靠得最近的局部桥梁”连起来，适合发现链式结构，但也更容易被噪声点串联。</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{complete}}(A,B)=\max_{\boldsymbol{x}\in A,\boldsymbol{y}\in B} d(\boldsymbol{x},\boldsymbol{y})\]</span>
<p>完全链距离只看两簇之间最远的那一对点，因此它会避免把跨度过大的簇合并到一起，更偏好紧凑、直径较小的簇。</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{average}}(A,B)=\frac{1}{|A||B|}\sum_{\boldsymbol{x}\in A,\boldsymbol{y}\in B} d(\boldsymbol{x},\boldsymbol{y})\]</span>
<p>平均链则对两簇之间所有点对的距离取平均。这里 <span displaypfx="inline-" class="mathjax-container">\(|A||B|\)</span> 是点对总数，因此它不只看最近点或最远点，而是综合考虑两簇整体的平均接近程度。</p>
<p>不同联接方式对应不同簇形偏好：单链更容易形成链式簇，完全链更偏向紧凑簇，平均链则居中。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>初始化每个样本为单独一个簇。</li>
<li>计算簇间距离矩阵。</li>
<li>反复合并距离最近的两个簇，并更新距离矩阵。</li>
<li>得到树状图（Dendrogram）后，在某个高度切开即可获得聚类结果。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在文档聚类中，层次聚类不仅能区分“体育”“财经”“科技”等大类，还能进一步展示每一大类内部的细粒度层级关系。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：无需预先指定簇数；可以输出多层次聚类结构。</li>
<li>局限：计算与存储成本较高；早期合并错误通常无法回退。</li>
<li>适用场景：中小规模数据、需要树状关系解释的聚类分析。</li>
</ul>
<div class="blog_h3"><span class="graybg">DBSCAN</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>DBSCAN（Density-Based Spatial Clustering of Applications with Noise）用于识别任意形状的高密度簇，并显式发现噪声点。它尤其适合处理非球形簇与含离群点数据。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>DBSCAN 通过局部密度定义簇。若一个点周围半径 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 内有足够多的邻居，则它是核心点（Core Point）；核心点可以把周围密度可达（Density-Reachable）的样本不断扩展成一个簇。既不够密、又不属于任何核心点邻域的样本被视为噪声。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/dbscan.jpg"><img class="alignnone size-full wp-image-41305" src="https://blog.gmem.cc/wp-content/uploads/2026/03/dbscan.jpg" alt="dbscan" width="1024" height="559" /></a></p>
<p>&nbsp;</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>记 <span displaypfx="inline-" class="mathjax-container">\(N_{\epsilon}(\boldsymbol{x})\)</span> 为点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 邻域。若：</p>
<span displaypfx="" class="mathjax-container">\[|N_{\epsilon}(\boldsymbol{x})|\ge \text{minPts}\]</span>
<p>判断核心点只需看两件事： <span displaypfx="inline-" class="mathjax-container">\(N_{\epsilon}(\boldsymbol{x})\)</span> 是点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 在半径 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 内的邻域集合， <span displaypfx="inline-" class="mathjax-container">\(|N_{\epsilon}(\boldsymbol{x})|\)</span> 是邻域里有多少点。只要这个数量不小于 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span>，就说明该点周围密度足够高，可以作为簇扩张的核心。</p>
<p>则 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 是核心点。若某点落在某个核心点邻域中但自身并非核心点，则为边界点（Border Point）；不属于任何簇的点为噪声（Noise）。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>遍历未访问样本，计算其 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 邻域。</li>
<li>若邻居数少于 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span>，则暂记为噪声或边界候选。</li>
<li>若为核心点，则以它为起点递归扩展所有密度可达的点。</li>
<li>重复直到所有样本都被标记。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在地理位置聚类中，餐馆、商圈、交通枢纽附近的点位通常形成形状复杂的密集区域。DBSCAN 可以识别这些非凸热点，同时把零散孤立点保留为噪声。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：无需预设簇数；可识别任意形状簇；对离群点鲁棒。</li>
<li>局限：对 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span> 敏感；难以同时适配不同密度簇。</li>
<li>适用场景：空间聚类、热点区域发现、非凸簇与带噪声数据。</li>
</ul>
<div class="blog_h3"><span class="graybg">HDBSCAN</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>HDBSCAN（Hierarchical DBSCAN）用于缓解 DBSCAN 的单一密度阈值问题。它面对的核心困难是：真实数据中的簇密度常常并不一致，用一组固定的 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span> 很难同时兼顾所有簇。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>HDBSCAN 会在多个密度尺度上构建层次结构，再从中选出稳定簇（Stable Clusters）作为结果。这让它比 DBSCAN 更适合处理密度差异显著的数据。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/hdbscan-intuition.png"><img class="alignnone size-full wp-image-41327" src="https://blog.gmem.cc/wp-content/uploads/2026/03/hdbscan-intuition.png" alt="hdbscan-intuition" width="3045" height="12714" /></a></p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定参数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，先定义核心距离（Core Distance）为点到其第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 近邻的距离，再定义互可达距离（Mutual Reachability Distance）：</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{mreach},k}(\boldsymbol{x},\boldsymbol{y})=\max\big(\text{core}_k(\boldsymbol{x}),\text{core}_k(\boldsymbol{y}),d(\boldsymbol{x},\boldsymbol{y})\big)\]</span>
<p>互可达距离把三个量取最大值：点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的核心距离、点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span> 的核心距离，以及它们之间的原始距离。这样做的效果是：低密度区域的点会被“拉远”，从而更清楚地暴露不同密度簇之间的结构边界。</p>
<p>随后算法在互可达图上构建最小生成树（Minimum Spanning Tree），再转成聚类层次，并依据簇稳定性选择最终输出。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>计算所有点的核心距离。</li>
<li>基于互可达距离构造图并求最小生成树。</li>
<li>从图中得到随密度变化的层次聚类结构。</li>
<li>在压缩树（Condensed Tree）上选择稳定簇作为结果。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在用户行为嵌入空间中，有些兴趣群体很紧密，有些较松散。HDBSCAN 可以同时保留这两类簇，而不需要强行用同一个密度阈值描述它们。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：对多密度簇更稳健，仍保留噪声识别能力。</li>
<li>局限：实现更复杂，计算与内存开销通常高于 DBSCAN。</li>
<li>适用场景：嵌入聚类、用户分群、不同密度簇共存的数据。</li>
</ul>
<div class="blog_h2"><span class="graybg">升维</span></div>
<p>升维（Feature Expansion / Lifting）处理的问题，与降维正好互补。降维试图把高维表示压缩到更低维空间，以减少冗余、噪声与计算量；升维则试图把原始特征映射到一个更高维的表示空间，使原本难以表达、难以分离或难以拟合的结构，在新空间里变得更容易处理。它的目标不是“把维度变大本身”，而是<span style="background-color: #c0c0c0;">通过增加表示自由度，把非线性关系改写为更容易由简单模型处理的形式</span>。</p>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>许多经典机器学习模型本体是线性的，例如线性回归、逻辑回归、线性支持向量机（Support Vector Machine, SVM）。它们直接在原始输入空间中学习一个线性决策函数或线性预测函数。若数据关系本身高度非线性，那么模型能力可能不足。升维的思路因此是：先把输入映射为一个更高维的新表示，再在新表示上使用线性模型。模型形式仍然简单，但由于工作空间改变了，整体表达能力会显著提升。</p>
<p>更一般地，若原始输入为 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\in \mathbb{R}^d\)</span>，则升维映射可写成：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\boldsymbol{x}}=\phi(\boldsymbol{x}),\qquad \phi: \mathbb{R}^d\to \mathbb{R}^D,\qquad D&gt;d\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 是原始输入； <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span> 是升维后的新特征； <span displaypfx="inline-" class="mathjax-container">\(\phi(\boldsymbol{x})\)</span> 是特征映射； <span displaypfx="inline-" class="mathjax-container">\(D&gt;d\)</span> 表示新的表示空间维度高于原空间维度。若后续模型写成 <span displaypfx="inline-" class="mathjax-container">\(f(\boldsymbol{x})=\boldsymbol{w}^\top \tilde{\boldsymbol{x}}+b\)</span>，那么它虽然在新空间中仍然是线性的，但在原始输入 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 上通常已经对应一个非线性函数。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>升维的直观含义，是把原来“纠缠在一起”的关系展开。例如在二维平面里，某些数据可能无法被一条直线分开；但若把 <span displaypfx="inline-" class="mathjax-container">\((x_1,x_2)\)</span> 映射成 <span displaypfx="inline-" class="mathjax-container">\((x_1,x_2,x_1^2,x_2^2,x_1x_2)\)</span> 这样的更高维特征，原本的弯曲边界就可能对应高维空间中的一个超平面。于是复杂性并没有消失，而是被转移到了特征映射 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}=\phi(\boldsymbol{x})\)</span> 上。</p>
<p>这也是经典机器学习里一个非常常见的套路：<span style="background-color: #c0c0c0;">先做特征构造或特征展开，再用结构简单、优化稳定的线性模型</span>。因此，升维常常并不以“升维”这个名字出现，而是以多项式特征、基函数展开、核方法、one-hot 编码、N-gram 稀疏特征、随机特征等形式出现。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>升维后的线性模型通常写成：</p>
<span displaypfx="" class="mathjax-container">\[f(\boldsymbol{x})=\boldsymbol{w}^\top \tilde{\boldsymbol{x}}+b\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span> 是由原始输入 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 构造出来的高维特征向量； <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\in \mathbb{R}^D\)</span> 是新空间中的参数； <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是偏置。关键点在于：模型在 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span> 空间里仍然是线性的，但如果 <span displaypfx="inline-" class="mathjax-container">\(\phi(\boldsymbol{x})\)</span> 含有平方项、交叉项、基函数或核映射，那么 <span displaypfx="inline-" class="mathjax-container">\(f(\boldsymbol{x})\)</span> 相对于原始输入 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 就会表现出非线性。</p>
<p>以二次多项式特征为例，若原始输入为 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}=(x_1,x_2)\)</span>，则可构造：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\boldsymbol{x}}=(x_1,x_2,x_1^2,x_2^2,x_1x_2)\]</span>
<p>此时线性模型</p>
<span displaypfx="" class="mathjax-container">\[f(\boldsymbol{x})=w_1x_1+w_2x_2+w_3x_1^2+w_4x_2^2+w_5x_1x_2+b\]</span>
<p>在参数上仍然是线性的，但在输入上已经能够表达二次曲面或二次决策边界。这正是升维最核心的数学作用：<span style="background-color: #c0c0c0;">把原空间中的非线性关系，改写成高维空间中的线性关系</span>。</p>
<div class="blog_h3"><span class="graybg">常见升维方式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">方式</td>
<td style="text-align: center;">升维形式</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">典型场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>多项式特征</td>
<td>加入平方项、立方项、交叉项</td>
<td>把低阶线性模型扩展为可拟合非线性关系</td>
<td>线性回归、逻辑回归、线性分类器</td>
</tr>
<tr>
<td>基函数展开</td>
<td>高斯基、样条基、傅里叶基等</td>
<td>用一组预定义函数把局部或周期结构显式展开</td>
<td>回归、广义加性模型、核近似</td>
</tr>
<tr>
<td>核方法</td>
<td>通过核函数隐式映射到高维甚至无限维空间</td>
<td>在不显式展开特征的情况下提升可分性</td>
<td>SVM、核岭回归、核 PCA</td>
</tr>
<tr>
<td>One-hot 编码</td>
<td>把离散类别映射为高维稀疏向量</td>
<td>让类别变量进入线性模型并保持类别独立性</td>
<td>表格特征、推荐、广告、点击率预估</td>
</tr>
<tr>
<td>N-gram / 词袋</td>
<td>把文本映射为高维稀疏词项空间</td>
<td>显式展开局部共现与组合模式</td>
<td>传统文本分类、检索、朴素贝叶斯、线性 SVM</td>
</tr>
<tr>
<td>随机特征</td>
<td>用随机映射近似某些核空间</td>
<td>在显式高维表示与核方法之间做折中</td>
<td>Random Fourier Features、核近似</td>
</tr>
</tbody>
</table>
<p>其中，核方法是经典机器学习里最有代表性的升维思想。以 SVM 为例，若定义特征映射 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}=\phi(\boldsymbol{x})\)</span>，则线性分类器可写成 <span displaypfx="inline-" class="mathjax-container">\(f(\boldsymbol{x})=\boldsymbol{w}^\top \tilde{\boldsymbol{x}}+b\)</span>。核技巧（Kernel Trick）并不显式构造 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span>，而是直接通过核函数</p>
<span displaypfx="" class="mathjax-container">\[K(\boldsymbol{x},\boldsymbol{x}')=\phi(\boldsymbol{x})^\top \phi(\boldsymbol{x}')\]</span>
<p>计算升维后特征空间中的内积。这样既保留了高维映射的表达力，又避免了显式展开到巨大维度的代价。也正因为如此，SVM 是很多人最先感受到“升维威力”的经典模型。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在圆形可分但线性不可分的数据中，原始二维平面上一条直线无法把内圈与外圈分开；但若加入半径相关的二次特征，例如 <span displaypfx="inline-" class="mathjax-container">\(x_1^2+x_2^2\)</span>，则问题可以转写为更高维空间中的线性分离。文本任务里，N-gram 稀疏特征同样是一种典型升维：原始句子是离散序列，经过词袋或 N-gram 展开后，就变成了数万维甚至更高维的稀疏向量，随后再交给逻辑回归、朴素贝叶斯或线性 SVM 处理。</p>
<p>表格任务中的类别变量处理也体现了同样思想。一个城市字段看起来只是一个离散取值，但 one-hot 编码后，它会被展开成一个高维稀疏向量，使模型能够为每个类别学习独立参数。推荐系统、广告点击率预估、工业风控中的大规模离散特征，长期都高度依赖这种升维方式。</p>
<div class="blog_h3"><span class="graybg">为什么经典机器学习更谨慎地升维</span></div>
<p>经典机器学习也会使用升维，但通常更谨慎。原因在于，维度一旦上去，过拟合风险、存储开销与计算开销都会迅速增加，这就是维度灾难（Curse of Dimensionality）的典型体现。于是经典方法常把“升维”与“控制复杂度”配套使用：一边通过特征展开提升表达能力，一边用正则化（Regularization）、特征选择（Feature Selection）或后续降维来抑制过拟合。</p>
<p>因此，在经典机器学习里，常见工作流并不是“只升维”或“只降维”，而是两者交替配合：先做有针对性的特征展开，让结构变得更容易表达；再通过正则化、筛选或压缩保留真正有效的部分。升维是在展开表达能力，降维是在压缩冗余信息，它们并不是对立操作，而是围绕表示空间做的两种互补控制。</p>
<div class="blog_h3"><span class="graybg">维度灾难</span></div>
<p>维度灾难（Curse of Dimensionality）指的是：当特征维度不断升高时，许多在低维空间中直观、有效的统计与几何规律会迅速恶化，导致数据需求、计算成本与建模难度同时上升。它不是某一个单独问题，而是一组高维效应的统称，包括样本空间体积指数级膨胀、样本变得极其稀疏、局部邻域难以稳定估计、距离与密度统计的判别力下降，以及模型更容易用复杂边界去记忆训练集。</p>
<p>其中一个最重要的现象确实是：<span style="background-color: #c0c0c0;">高维里距离往往会变得不再像低维那样有区分力</span>。直观地说，当维度很多时，样本之间的最近距离和最远距离可能越来越接近，导致“谁是真正近邻”这件事变得没那么清晰。依赖距离或局部邻域的方法，例如 KNN、聚类、核密度估计、局部异常检测等，往往会因此退化。这也是为什么高维数据上，距离度量、标准化、特征筛选和嵌入学习会变得格外重要。</p>
<p>但“高维更容易过拟合”并不只因为距离失去意义。更根本的原因是：当维度升高后，表示空间的自由度与可容纳的划分方式急剧增加，而训练样本相对于整个空间显得越来越稀疏。模型于是更容易找到一些只对训练集成立、却不能推广到新样本的偶然边界或偶然相关性。换句话说，<span style="background-color: #c0c0c0;">距离退化主要伤害的是邻域、相似度与密度估计；过拟合风险上升则更直接地来自空间稀疏化、参数自由度增加和有效样本覆盖不足</span>。</p>
<p>这也是为什么经典机器学习在做升维时往往必须同步引入约束：一方面用特征展开提升表达能力，另一方面通过正则化（Regularization）、特征选择（Feature Selection）、降维（Dimensionality Reduction）或更强的数据先验，限制模型不要把高维空间当成“背题空间”。因此，维度灾难并不是在说“高维一定不好”，而是在提醒：<span style="background-color: #c0c0c0;">维度每增加一层，模型就需要更多数据、更强归纳偏置和更谨慎的复杂度控制，才能让新增维度真正转化为有效表达能力</span>。</p>
<div class="blog_h3"><span class="graybg">和深度学习中的升维关系</span></div>
<p>深度学习中的很多操作，本质上也在做升维。Transformer 的前馈网络（Feed-Forward Network, FFN / MLP）常把 <span displaypfx="inline-" class="mathjax-container">\(d_{\text{model}}\)</span> 升到更大的 <span displaypfx="inline-" class="mathjax-container">\(d_{\text{ff}}\)</span>，再降回原维度；这与经典机器学习里“先展开、再用简单变换处理”的思想是一脉相承的。差别在于，经典方法里的升维往往是人工设计或固定形式的，而深度学习中的升维通常是可学习的线性投影与非线性组合。</p>
<p>因此，若从表示学习角度看，升维在机器学习中并不罕见。SVM 的核空间、逻辑回归的多项式特征、文本的 N-gram 稀疏向量、推荐系统的 one-hot 离散展开，以及 Transformer MLP 中的中间维度扩张，本质上都属于同一条主线：<span style="background-color: #c0c0c0;">把原本难以处理的关系，展开到一个更容易表达与分离的表示空间里</span>。</p>
<div class="blog_h2"><span class="graybg">降维</span></div>
<p>降维处理的核心问题是：高维数据往往包含冗余、相关性与噪声，既增加计算成本，也削弱可视化与建模稳定性。目标不是简单“删维度”，而是在压缩表示的同时尽量保留有用结构——这个“有用”可以是方差、类别可分性、局部邻域、全局流形，具体取决于所采用的方法。</p>
<div class="blog_h3"><span class="graybg">主成分分析（PCA）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>主成分分析（Principal Component Analysis, PCA）处理的是线性降维问题：在尽量保留数据主要变化信息的前提下，把高维样本映射到更低维空间。它常用于压缩维度、去相关、可视化与噪声抑制。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/pca.jpg"><img class="alignnone size-full wp-image-41347" src="https://blog.gmem.cc/wp-content/uploads/2026/03/pca.jpg" alt="pca" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>PCA 用方差（Variance）近似衡量信息量。若数据在某个方向上的投影变化很大，说明该方向承载了更多结构；于是 PCA 选择能最大化投影方差的一组正交方向作为新的表示基底。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定中心化后的数据矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{X}_c\in\mathbb{R}^{N\times d}\)</span>，协方差矩阵为：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{\Sigma}=\frac{1}{N}\boldsymbol{X}_c^\top \boldsymbol{X}_c\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{X}_c\)</span> 是已经减去均值后的数据矩阵， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{X}_c^\top \boldsymbol{X}_c\)</span> 汇总了各个特征之间如何共同变化；再除以 <span displaypfx="inline-" class="mathjax-container">\(N\)</span>，就得到平均意义下的协方差矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\Sigma}\)</span>。PCA 正是从这个矩阵里找“变化最大的方向”。</p>
<p>第一主成分对应的优化问题为：</p>
<span displaypfx="" class="mathjax-container">\[\max_{\boldsymbol{u}} \quad \boldsymbol{u}^\top \boldsymbol{\Sigma}\boldsymbol{u} \quad \text{s.t.} \quad \|\boldsymbol{u}\|_2=1\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{u}\)</span> 是候选投影方向， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{u}^\top \boldsymbol{\Sigma}\boldsymbol{u}\)</span> 表示数据投影到该方向后的方差，约束 <span displaypfx="inline-" class="mathjax-container">\(\|\boldsymbol{u}\|_2=1\)</span> 则防止通过把向量无限放大来虚增方差。于是这个优化问题真正寻找的是“单位长度下最能保留变化信息的方向”。</p>
<p>其解是协方差矩阵最大特征值对应的特征向量。取前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个主成分组成矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{U}_k\)</span> 后，样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的低维表示为：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{z}=\boldsymbol{U}_k^\top(\boldsymbol{x}-\boldsymbol{\mu})\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}\)</span> 是原始数据均值， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}-\boldsymbol{\mu}\)</span> 先把样本中心化， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{U}_k\)</span> 由前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个主成分方向组成，最终 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{z}\)</span> 就是样本在这组主方向上的低维坐标。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>对数据做中心化，必要时再做标准化。</li>
<li>计算协方差矩阵或直接对数据矩阵做奇异值分解（SVD）。</li>
<li>取前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个主成分方向。</li>
<li>把数据投影到这些方向上得到低维表示。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在人脸图像压缩中，大量像素变化往往由少数全局因素驱动。PCA 可以用少量主成分保留大部分有效变化，从而显著降低特征维度。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：线性、稳定、可解释，常用作预处理和可视化。</li>
<li>局限：只能捕捉线性结构；对异常值敏感。</li>
<li>适用场景：线性降维、特征压缩、去相关与噪声过滤。</li>
</ul>
<div class="blog_h3"><span class="graybg">线性判别分析（LDA）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>线性判别分析（Linear Discriminant Analysis, LDA）用于监督降维与分类。与 PCA 只看输入分布不同，LDA 利用类别标签寻找一个投影空间，使同类样本尽量聚集、异类样本尽量分开。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/lda.jpg"><img class="alignnone size-full wp-image-41349" src="https://blog.gmem.cc/wp-content/uploads/2026/03/lda.jpg" alt="lda" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>LDA 同时考虑类内散度（Within-class Scatter）和类间散度（Between-class Scatter）。好的投影方向应当让类间距离大、类内波动小，因此它优化的核心是判别性。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>定义类内散度矩阵：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{S}_W=\sum_{k=1}^{C}\sum_{\boldsymbol{x}_i\in \mathcal{C}_k}(\boldsymbol{x}_i-\boldsymbol{\mu}_k)(\boldsymbol{x}_i-\boldsymbol{\mu}_k)^\top\]</span>
<p>类内散度矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_W\)</span> 统计的是“同一类内部有多分散”。 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{C}_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 类的样本集合， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}_k\)</span> 是该类均值。若类内样本围绕各自均值分布得很紧， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_W\)</span> 就会较小。</p>
<p>类间散度矩阵：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{S}_B=\sum_{k=1}^{C}N_k(\boldsymbol{\mu}_k-\boldsymbol{\mu})(\boldsymbol{\mu}_k-\boldsymbol{\mu})^\top\]</span>
<p>类间散度矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_B\)</span> 统计的是“各类中心彼此有多分开”。其中 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}\)</span> 是全局均值， <span displaypfx="inline-" class="mathjax-container">\(N_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 类样本数，因此样本多的大类会对类间结构贡献更大权重。</p>
<p>LDA 寻找投影向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 使 Fisher 判别准则最大：</p>
<span displaypfx="" class="mathjax-container">\[J(\boldsymbol{w})=\frac{\boldsymbol{w}^\top \boldsymbol{S}_B \boldsymbol{w}}{\boldsymbol{w}^\top \boldsymbol{S}_W \boldsymbol{w}}\]</span>
<p>Fisher 准则的分子衡量“投影后类间有多分开”，分母衡量“投影后类内有多混在一起”。因此 <span displaypfx="inline-" class="mathjax-container">\(J(\boldsymbol{w})\)</span> 越大，说明方向 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 越有助于把不同类别拉开、同时保持同类紧凑。</p>
<p>该问题可转化为广义特征值问题 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_B\boldsymbol{w}=\lambda \boldsymbol{S}_W\boldsymbol{w}\)</span>。对 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 类问题，最多能得到 <span displaypfx="inline-" class="mathjax-container">\(C-1\)</span> 个有效判别方向。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>根据标签统计每一类的均值与全局均值。</li>
<li>构造类内散度矩阵和类间散度矩阵。</li>
<li>求解广义特征值问题，选择主要判别方向。</li>
<li>将样本投影到低维判别子空间中做分类或可视化。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在手写数字分类中，LDA 关注的是“哪些方向最有助于把不同数字分开”，因此在有标签监督时，它往往比只最大化总体方差的 PCA 更适合分类前降维。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：利用标签信息，投影方向更有判别性。</li>
<li>局限：线性假设较强；类内散度矩阵可能奇异。</li>
<li>适用场景：有标签监督的降维、分类前特征压缩、判别性可视化。</li>
</ul>
<div class="blog_h3"><span class="graybg">t-SNE</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>t-SNE（t-distributed Stochastic Neighbor Embedding）主要用于二维或三维可视化。它主要关注如何在低维图上尽量保留高维空间中的局部邻域关系。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>t-SNE 把“样本彼此接近”转写为概率相似度：在高维空间中，接近的样本应当有较大概率互为邻居；在低维嵌入中，也希望这种邻近关系继续成立。优化的目标是让两种邻域概率分布尽可能接近。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>在高维空间中，先定义邻域概率 <span displaypfx="inline-" class="mathjax-container">\(p_{ij}\)</span>；在低维空间中，对嵌入点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}_i\)</span> 定义：</p>
<span displaypfx="" class="mathjax-container">\[q_{ij}=\frac{(1+\|\boldsymbol{y}_i-\boldsymbol{y}_j\|_2^2)^{-1}}{\sum_{a\ne b}(1+\|\boldsymbol{y}_a-\boldsymbol{y}_b\|_2^2)^{-1}}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}_i\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}_j\)</span> 是低维嵌入坐标，分子把距离越近的点赋予越大的相似度，分母则把所有点对的相似度加总起来做归一化，因此 <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span> 可以被解释成低维空间里的邻域概率。</p>
<p>t-SNE 最小化高维邻域分布与低维邻域分布之间的 KL 散度：</p>
<span displaypfx="" class="mathjax-container">\[\text{KL}(P\|Q)=\sum_{i\ne j} p_{ij} \log \frac{p_{ij}}{q_{ij}}\]</span>
<p>KL 散度衡量的是“低维邻域分布 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 与高维邻域分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 相差多少”。若某对样本在高维里很近，即 <span displaypfx="inline-" class="mathjax-container">\(p_{ij}\)</span> 很大，但在低维里被拉得太开， <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span> 就会过小，从而产生较大惩罚。</p>
<p>低维中采用重尾 Student-t 分布，主要是为了缓解拥挤问题（Crowding Problem），使远处点更容易被拉开。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>根据高维距离计算样本间的邻域概率。</li>
<li>随机初始化低维坐标。</li>
<li>计算低维相似度 <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span>。</li>
<li>通过梯度下降最小化 KL 散度并更新低维坐标。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在词向量或图像嵌入可视化中，t-SNE 常把语义相近的样本压到局部团簇中，从而帮助研究者直观看到表示空间里是否出现了合理分群。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：二维可视化效果强，局部邻域保持能力好。</li>
<li>局限：全局距离与簇间相对位置不稳定；对超参数和随机初始化敏感。</li>
<li>适用场景：嵌入可视化、表示质量诊断、探索性数据分析。</li>
</ul>
<div class="blog_h3"><span class="graybg">UMAP</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>UMAP（Uniform Manifold Approximation and Projection）同样用于低维可视化与非线性降维。它面对的任务与 t-SNE 类似，但更强调在保留局部结构的同时，尽量维持一定的全局几何关系，并提升速度与可扩展性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/umap.jpg"><img class="alignnone size-full wp-image-41351" src="https://blog.gmem.cc/wp-content/uploads/2026/03/umap.jpg" alt="umap" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>UMAP 先在高维空间构建一个加权近邻图，把数据视为流形（Manifold）上的离散采样；随后在低维空间中寻找一张新图，使低维图与高维图的模糊连通结构尽量一致。它本质上是在匹配两张图的连通结构，而不只是在匹配两组欧氏距离。</p>
<p>这里的流形可以先从一个标准定义理解：设样本位于高维空间 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^D\)</span> 中，若它们实际上集中在某个低维集合 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{M}\subseteq\mathbb{R}^D\)</span> 附近，并且对 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{M}\)</span> 上任一点，都能在一个足够小的邻域内用 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维坐标平滑描述，其中 <span displaypfx="inline-" class="mathjax-container">\(d\ll D\)</span>，那么这个集合就可以看作一个 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维流形。流形的关键性质不是“它弯不弯”，而是<span style="background-color: #c0c0c0;">它在局部看起来像低维欧氏空间，在全局上则可以嵌入到更高维空间并发生弯曲、卷曲或拉伸</span>。</p>
<p>在数据分析里，这个概念表示：虽然原始特征有很多维，但样本并没有真正填满整个高维空间，而是分布在某种低维结构附近。例如一组人脸图像在像素空间里维度极高，但由姿态、光照、表情等少数潜在因素变化时，样本往往只覆盖其中一个低维子区域。UMAP 的出发点正是：若数据主要几何结构由这个低维流形决定，那么局部近邻关系比“全局欧氏直线距离”更能反映真实结构。</p>
<p>“离散采样”则表示：真实流形 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{M}\)</span> 本身是连续对象，但我们手里只有有限个样本点 <span displaypfx="inline-" class="mathjax-container">\(\{x_i\}_{i=1}^N\)</span>。UMAP 通过近邻图近似这些点在流形上的局部连通关系，再在低维空间中寻找一组坐标 <span displaypfx="inline-" class="mathjax-container">\(\{y_i\}_{i=1}^N\)</span>，使这种局部连通关系尽量被保留下来。因此它不是直接恢复整张流形，而是在有限样本层面恢复流形的邻域结构。</p>
<p>与 t-SNE 相比，UMAP 更强调“样本来自某个低维流形，并且近邻图是在近似这个流形的局部连通结构”；t-SNE 的核心对象则更偏向“高维邻域概率分布与低维邻域概率分布的匹配”。两者都重视局部关系，但 UMAP 的表述更几何化，t-SNE 的表述更概率化。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>若高维图边权为 <span displaypfx="inline-" class="mathjax-container">\(p_{ij}\)</span>，低维图边权常写为：</p>
<span displaypfx="" class="mathjax-container">\[q_{ij}=\frac{1}{1+a\|\boldsymbol{y}_i-\boldsymbol{y}_j\|_2^{2b}}\]</span>
<p>UMAP 用这个函数把低维距离转换成连通强度。 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 控制曲线形状：距离很近时 <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span> 接近 1，距离变远时迅速衰减到接近 0，因此它可以把“近邻关系”转写成平滑的图边权。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(a,b\)</span> 决定低维距离与连接强度的映射形状。UMAP 的优化目标通常是高维图与低维图之间的交叉熵（Cross-Entropy）：既鼓励高维中相连的点在低维中也靠近，也鼓励高维中不相连的点在低维中适当分开。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>计算近邻图，并为边赋予模糊连通权重。</li>
<li>初始化低维坐标。</li>
<li>通过随机优化最小化高维图与低维图之间的交叉熵。</li>
<li>得到二维或三维嵌入用于可视化或下游分析。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在单细胞测序、文本嵌入或推荐向量分析中，UMAP 常被用来把高维表示映射到二维平面，从而观察群体结构、类别分布与异常点。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：通常比 t-SNE 更快，较好兼顾局部与部分全局结构。</li>
<li>局限：嵌入结果仍依赖超参数与随机种子；二维距离不能机械等同于原空间距离。</li>
<li>适用场景：大规模嵌入可视化、非线性降维、聚类前的低维表示。</li>
</ul>
<div class="blog_h2"><span class="graybg">异常检测</span></div>
<p>异常检测处理的核心问题是：正常样本通常大量存在，而异常样本稀少、形态多变、甚至在训练阶段根本拿不到完整标签。模型因此不再主要学习“类别之间如何区分”，而是学习“什么算正常”以及“偏离正常结构有多严重”。不同方法对“异常”的定义并不相同：有的依赖隔离难易度，有的依赖局部密度，有的学习正常区域边界。</p>
<div class="blog_h3"><span class="graybg">什么叫异常</span></div>
<p>在业务语境里，“异常”并不等于“数值特别大”或“离均值很远”。更准确地说，异常是<span style="background-color: #c0c0c0;">相对于当前业务规则、历史模式或同类群体而言，不应当出现、很少出现、或者一旦出现就值得额外关注的样本</span>。因此异常是一个“相对概念”，必须依赖参照系：相对于谁、在哪个时间段、在什么上下文里、以什么代价衡量。</p>
<p>例如，单笔消费 5000 元在全国范围内不一定异常，但若这位用户平时只在本地便利店做几十元交易，而这次交易突然发生在异地、高风险设备、深夜时段，并伴随支付习惯突变，那么它在风控上就可能是异常。类似地，服务器 CPU 使用率 85% 在大促期间可能是正常负载，在凌晨低峰却可能意味着任务堆积；工厂传感器温度轻微升高若同时伴随振动模式变化，也可能预示故障正在形成。</p>
<p>这说明业务上的异常通常至少包含三类含义。第一类是<span style="background-color: #c0c0c0;">统计稀有</span>：样本落在低概率区域。第二类是<span style="background-color: #c0c0c0;">行为失配</span>：它与该对象自己的历史模式不一致。第三类是<span style="background-color: #c0c0c0;">群体失配</span>：它在全局上不一定极端，但相对于同类群体显著不同。异常检测算法的差异，本质上就在于它们分别更擅长刻画哪一种“失配”。</p>
<p>因此，做异常检测时首先要回答的不是“用哪种模型”，而是“业务到底把什么视为异常”。若异常意味着“明显稀少且容易与大部队分开”，隔离式方法更合适；若异常意味着“在本地邻域里显得稀疏”，应优先考虑密度比较；若正常样本边界清楚、异常样本类型杂乱，则更适合只学习正常区域。算法不是在定义业务，而是在实现业务已经确定的异常标准。</p>
<div class="blog_h3"><span class="graybg">孤立森林（Isolation Forest）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>孤立森林（Isolation Forest）用于无监督异常检测：在没有可靠异常标签时，仅根据数据分布本身识别“容易被孤立”的异常样本。它尤其适合高维表格数据与大规模检测场景。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>孤立森林利用一个非常直接的直觉：异常点通常更稀少、更孤立，因此在随机切分下更容易被提早隔离。路径越短，越可能是异常；路径越长，越像处于正常群体内部。若把正常样本理解为“扎堆生活在高密度区域里的大群体”，那么它们往往需要经过很多次随机切割才会被单独分离出来；而那些落在边缘、稀疏区域、或与主体分布明显脱节的样本，只需少量切分就会被单独留在某个叶节点里。</p>
<p>这种思路和距离或密度方法非常不同。K 近邻（K-Nearest Neighbors, KNN）或局部异常因子（Local Outlier Factor, LOF）会显式比较“离别人有多远”或“局部密度有多低”；孤立森林则直接把异常检测改写成一个更具操作性的判据：<span style="background-color: #c0c0c0;">一个样本在随机树里平均多快会被单独隔开</span>。因此它并不先估计复杂的概率分布，也不依赖全局距离结构，而是用“隔离难易度”来近似刻画异常性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.png"><img class="alignnone size-full wp-image-41363" src="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.png" alt="random-forest" width="1600" height="1308" /></a></p>
<div class="blog_h4"><span class="graybg">图示解读</span></div>
<p>图中的示意数据由两个主要正常簇和若干离散分布的异常点构成。背景等高线展示的是孤立森林学到的“异常分数地形”：颜色越深，表示该区域样本平均路径长度越长，更接近模型眼中的正常高密度区域；颜色越浅，表示样本更容易在随机切分中被提早隔离，因此更接近潜在异常区域。</p>
<p>图中圆形点对应被模型判为正常的样本，它们主要聚集在两个深色中心附近；叉号对应被模型判为异常的样本，它们更多分布在边缘、簇间空白带或浅色区域。这种可视化非常适合帮助理解孤立森林的工作方式：它并不是在画一条严格的几何边界，而是在表达“哪里更容易被随机树迅速切出来”。因此，等高线的深浅更接近一种“隔离难度地图”，而不是传统分类器意义上的硬分类边界。</p>
<div class="blog_h4"><span class="graybg">隔离树是什么</span></div>
<p>隔离树（Isolation Tree, iTree）可以理解成一棵专门为了“把样本逐步切开”而构造的随机二叉树。它与决策树（Decision Tree）在外形上相似，但目标完全不同：决策树是在找最有区分力的切分规则，隔离树则故意随机选择一个特征，再在该特征当前取值范围内随机选择一个切分点，把样本递归分到左右子节点。</p>
<p>设当前节点包含样本集合 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，随机选到的特征为第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 维，其切分阈值为 <span displaypfx="inline-" class="mathjax-container">\(\tau\)</span>，则一次切分可写成</p>
<span displaypfx="" class="mathjax-container">\[S_{\mathrm{left}}=\{\boldsymbol{x}\in S\mid x_j&lt;\tau\},\qquad S_{\mathrm{right}}=\{\boldsymbol{x}\in S\mid x_j\ge \tau\}\]</span>
<p>递归继续进行，直到某个节点只剩下 1 个样本，或所有样本在当前节点上已经无法再被有效区分。对一个本来就远离主体、所在区域又很稀疏的样本而言，随机切分往往只需要很少几步就能把它单独留在某个叶节点中；而对处在高密度正常群体内部的样本，通常要经过更多次切分才能被单独隔离。隔离树因此并不显式估计概率密度，而是把“异常”转写为“被随机切分提早单独分离”的难易度。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>对样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，记它在一棵隔离树中的路径长度为 <span displaypfx="inline-" class="mathjax-container">\(h(\boldsymbol{x})\)</span>。在多棵树上取平均路径长度 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[h(\boldsymbol{x})]\)</span> 后，异常分数定义为：</p>
<span displaypfx="" class="mathjax-container">\[s(\boldsymbol{x},n)=2^{-\mathbb{E}[h(\boldsymbol{x})]/c(n)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(h(\boldsymbol{x})\)</span> 是样本在一棵树里被隔离所需的路径长度， <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[h(\boldsymbol{x})]\)</span> 是在整片森林中的平均值， <span displaypfx="inline-" class="mathjax-container">\(c(n)\)</span> 是针对样本规模 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 的归一化常数。路径越短，指数里的值越小，异常分数 <span displaypfx="inline-" class="mathjax-container">\(s(\boldsymbol{x},n)\)</span> 就越接近 1。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(c(n)\)</span> 是平均路径长度的归一化常数，用来把不同样本规模下的路径长度放到可比较的尺度上。它常写为 <span displaypfx="inline-" class="mathjax-container">\(c(n)=2H_{n-1}-2(n-1)/n\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(H_{n-1}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(n-1\)</span> 个调和数（Harmonic Number）。若某点明显比普通样本更早被切分隔离，则其平均路径长度更小，异常分数更接近 1。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>从训练集中随机抽取多个子样本。</li>
<li>为每个子样本构建随机隔离树。</li>
<li>对待测点计算其在所有树中的平均路径长度。</li>
<li>将平均路径长度映射为异常分数。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在交易风控中，若某条交易在金额、时间、地点、设备等多个维度上都明显偏离正常模式，它往往能在随机切分下被较早隔离出来，因此会获得更高异常分数。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：无需显式估计密度，也不依赖两两距离矩阵；因此在大规模数据上通常比基于邻域或密度的方法更高效。</li>
<li>优点：对高维表格数据较友好，对随机噪声通常也有较强鲁棒性，因为最终判断来自多棵随机树上的平均隔离行为，而不是某一次局部切分。</li>
<li>局限：对特征编码和特征尺度的业务含义仍然敏感；若异常与正常高度混叠、或者异常本身并不更容易被切开，隔离优势会下降。</li>
<li>适用场景：风控、日志异常、设备故障、指标监控等无监督异常检测，尤其适合作为高维表格场景中的强基线。</li>
</ul>
<div class="blog_h3"><span class="graybg">局部异常因子（LOF）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>局部异常因子（Local Outlier Factor, LOF）主要解决“全局上不远，但在局部邻域中显著稀疏”的异常检测问题。当数据不同区域密度差异很大时，只看全局距离通常不够稳定。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>LOF 比较的是“这个点相对于其邻居是否更稀疏”。若一个点周围的局部密度显著低于邻居自己的局部密度，则它更像局部异常。它识别的不是“全局上最远的点”，而是<span style="background-color: #c0c0c0;">相对于自己所在局部环境显得不协调的点</span>。因此，当不同区域本来就有不同密度时，LOF 往往比只看全局距离的方法更稳。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/lof.png"><img class="alignnone size-full wp-image-41371" src="https://blog.gmem.cc/wp-content/uploads/2026/03/lof.png" alt="lof" width="1000" height="800" /></a></p>
<div class="blog_h4"><span class="graybg">图示解读</span></div>
<p>图中的实心圆点表示样本点，围绕样本点的空心圆圈大小表示该点的 LOF 分数大小。圆圈越小，说明该点的局部密度与其邻居相近，更像正常簇中的内部样本；圆圈越大，说明该点所在位置相对于邻居显得更稀疏，因此更可能是局部异常点。</p>
<p>这张图直观展示了 LOF 的核心判断方式：它并不先问“这个点离全局中心有多远”，而是先问“这个点和自己周围那一圈邻居相比，是不是显得过于稀疏”。因此，大圆圈对应的未必是全局最远的点，而更可能是那些<span style="background-color: #c0c0c0;">周围邻居仍然较密、但它自己明显脱离了局部密度水平</span>的点。</p>
<p>若把这张示意图看作两个高密度簇与少量随机噪声点的组合，则邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 决定了算法观察“局部环境”的尺度。 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 较小时，模型更敏感于非常局部的扰动； <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 较大时，密度比较更平滑，但也可能削弱对细粒度异常的敏感性。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，设 <span displaypfx="inline-" class="mathjax-container">\(N_k(\boldsymbol{p})\)</span> 表示点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{p}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个最近邻集合。LOF 的计算可以拆成三层：先算可达距离（Reachability Distance），再算局部可达密度（Local Reachability Density, LRD），最后比较邻居密度与自身密度，得到局部异常因子（Local Outlier Factor, LOF）。</p>
<p>先定义可达距离（Reachability Distance）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{rd}_k(\boldsymbol{p},\boldsymbol{o})=\max\big(\text{k-distance}(\boldsymbol{o}),d(\boldsymbol{p},\boldsymbol{o})\big)\]</span>
<p>可达距离会把两个量取最大值：邻居点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{o}\)</span> 自己的第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 邻距离，以及点对之间的真实距离。这样做可以防止极近的点对把局部密度估得过于夸张，使密度估计更稳。</p>
<p>再定义局部可达密度（Local Reachability Density, LRD）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{lrd}_k(\boldsymbol{p})=\left(\frac{1}{|N_k(\boldsymbol{p})|}\sum_{\boldsymbol{o}\in N_k(\boldsymbol{p})} \mathrm{rd}_k(\boldsymbol{p},\boldsymbol{o})\right)^{-1}\]</span>
<p>局部可达密度 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{lrd}_k(\boldsymbol{p})\)</span> 本质上是“平均可达距离”的倒数：平均距离越小，周围越拥挤，密度越大；平均距离越大，周围越稀疏，密度越小。</p>
<p>最终的 LOF 分数为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{LOF}_k(\boldsymbol{p})=\frac{1}{|N_k(\boldsymbol{p})|}\sum_{\boldsymbol{o}\in N_k(\boldsymbol{p})} \frac{\mathrm{lrd}_k(\boldsymbol{o})}{\mathrm{lrd}_k(\boldsymbol{p})}\]</span>
<p>LOF 分数比较的是“邻居的局部密度”和“自己本身的局部密度”。若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{lrd}_k(\boldsymbol{p})\)</span> 明显小于邻居的密度，分数就会大于 1，说明该点相对周围环境显得更孤立。更具体地说，分子是邻居局部密度的平均水平，分母是点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{p}\)</span> 自己的局部密度，因此这个比值本质上是在问：<span style="background-color: #c0c0c0;">你周围的人都很挤，而你自己是不是站得太空</span>。</p>
<p>结果通常可以这样解读：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{LOF}_k(\boldsymbol{p})\approx 1\)</span>：该点的局部密度与邻居相近，通常属于正常样本。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{LOF}_k(\boldsymbol{p})&lt;1\)</span>：该点甚至比邻居更密集，往往处于簇的核心区域。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{LOF}_k(\boldsymbol{p})&gt;1\)</span>：该点局部密度低于邻居，越大越像异常点。</li>
</ul>
<p>因此，LOF 回答的是“这个点在自己的局部邻域里是不是显得过于稀疏”，而不是“这个点离全局中心远不远”。这也是它能识别局部异常、却对距离度量与邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 较敏感的根本原因。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>为每个样本找到 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个近邻。</li>
<li>计算可达距离与局部可达密度。</li>
<li>比较样本与邻居的局部密度，得到 LOF 分数。</li>
<li>按分数排序或设置阈值输出异常点。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在消费行为数据中，某一线城市用户的高消费在全国范围内未必异常，但在“同年龄、同区域、同收入”的邻域里可能明显偏离。LOF 正是通过这种局部密度比较识别这类异常。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：适合不同密度区域共存的数据，能识别“全局上不极端、但局部上明显失配”的异常。</li>
<li>优点：解释性较强，因为 LOF 分数直接来自“邻居密度 / 自身密度”的局部比较，便于回答异常是相对于谁显得异常。</li>
<li>局限：对距离度量、标准化和邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 很敏感； <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 太小会放大噪声，太大又可能抹平真正的局部异常。</li>
<li>局限：大规模近邻搜索成本较高，因此在超大数据上常需要索引加速或近似近邻方法配合。</li>
<li>适用场景：局部离群检测、消费异常、群体内部行为异常、同类用户或设备群体中的行为失配识别。</li>
</ul>
<div class="blog_h3"><span class="graybg">单类支持向量机（One-Class SVM）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>单类支持向量机（One-Class Support Vector Machine, One-Class SVM）用于“只有正常样本、缺少可靠异常样本”的异常检测任务。它的目标是学习一个描述正常样本区域的边界。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>One-Class SVM 通过核映射把样本送到高维特征空间，再寻找一个把大多数正常样本与原点分开的超平面。等价地看，它学习的是一个“包住正常样本”的高维边界，边界外的点更可能是异常。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>一种标准形式为：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\boldsymbol{w},\rho,\boldsymbol{\xi}} \frac{1}{2}\|\boldsymbol{w}\|_2^2+\frac{1}{\nu N}\sum_{i=1}^{N} \xi_i-\rho\]</span>
<p>One-Class SVM 的目标由三部分组成： <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\boldsymbol{w}\|_2^2\)</span> 控制边界不要太复杂， <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{\nu N}\sum_i \xi_i\)</span> 惩罚落在边界外或靠得太近的样本， <span displaypfx="inline-" class="mathjax-container">\(-\rho\)</span> 则鼓励把正常区域尽量向外推开。 <span displaypfx="inline-" class="mathjax-container">\(\nu\)</span> 越大，对违约样本的容忍度越高。</p>
<span displaypfx="" class="mathjax-container">\[\text{s.t.} \quad \boldsymbol{w}^\top \phi(\boldsymbol{x}_i) \ge \rho-\xi_i,\qquad \xi_i \ge 0\]</span>
<p>这些约束表示：样本映射到特征空间后，其投影值至少要达到阈值 <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span>；若做不到，就用非负松弛变量 <span displaypfx="inline-" class="mathjax-container">\(\xi_i\)</span> 记录违约程度。于是模型允许少量样本越界，但必须为此付出代价。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 是超平面法向量， <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span> 是阈值， <span displaypfx="inline-" class="mathjax-container">\(\xi_i\)</span> 是松弛变量， <span displaypfx="inline-" class="mathjax-container">\(\nu\in(0,1]\)</span> 控制允许落在边界外的比例与支持向量比例。判别函数为：</p>
<span displaypfx="" class="mathjax-container">\[f(\boldsymbol{x})=\text{sign}\left(\boldsymbol{w}^\top \phi(\boldsymbol{x})-\rho\right)\]</span>
<p>判别函数先计算样本在特征空间里相对边界的位置：若 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}^\top \phi(\boldsymbol{x})-\rho\)</span> 为正，说明样本位于学习到的正常区域一侧；若为负，则更可能落在边界之外，被视为异常。</p>
<p>若使用核函数 <span displaypfx="inline-" class="mathjax-container">\(K(\boldsymbol{x},\boldsymbol{x}')\)</span>，则该边界可以是非线性的，因此能表达复杂的正常区域。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>只用正常样本训练模型，选择核函数与超参数 <span displaypfx="inline-" class="mathjax-container">\(\nu\)</span>。</li>
<li>在特征空间中学习分离超平面。</li>
<li>对新样本计算判别函数值。</li>
<li>若分数低于边界阈值，则判为异常。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在设备健康监控中，异常故障类型往往变化很大，难以完整收集，但正常运行数据很多。One-Class SVM 可以只基于正常样本学习边界，一旦新样本落出该区域，就触发异常告警。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：只依赖正常样本；核方法可表达复杂边界。</li>
<li>局限：对特征缩放和核参数敏感；大规模训练较重。</li>
<li>适用场景：设备监控、入侵检测、质量控制等“正常样本丰富、异常样本稀缺”的任务。</li>
</ul>
<div class="blog_h1"><span class="graybg">神经网络</span></div>
<div class="blog_h2"><span class="graybg">前馈神经网络</span></div>
<div class="blog_h3"><span class="graybg">感知机</span></div>
<p>感知机（Perceptron）是最早的神经元模型之一，也是现代神经网络最基本的计算原型。无论是 MLP、CNN、RNN，还是 Transformer，本质上都由大量“线性变换 + 非线性变换”的单元堆叠而成；从这个意义上说，理解感知机，就是理解大型模型最小的功能部件。</p>
<p>最原始的感知机先做线性组合，再经过一个阈值函数给出二分类输出：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\mathrm{sign}(\mathbf{w}^\top \mathbf{x}+b)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 是输入特征， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 是权重， <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是偏置。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top \mathbf{x}+b\)</span> 的含义是“沿着权重指定的方向对输入做加权打分”，而 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{sign}(\cdot)\)</span> 则把连续分数变成离散决策：高于阈值判为正类，低于阈值判为负类。</p>
<p>需要特别区分的是：最早的感知机确实直接做分类，但现代神经网络里的大多数“感知机式单元”并不直接输出最终类别。隐藏层单元更常见的形式是 <span displaypfx="inline-" class="mathjax-container">\(h=\phi(\mathbf{w}^\top \mathbf{x}+b)\)</span>，它们输出的是中间表示（Intermediate Representation），职责是检测局部模式、重组特征并为后续层提供更有用的表示；只有最后的任务头（Task Head）才把这些中间表示转成分类、回归或生成输出。</p>
<p>这里的中间表示（Intermediate Representation）本质上就是一组<span style="background-color: #c0c0c0;">可被后续层继续计算的数值特征</span>。它既可以是向量（Vector），也可以是矩阵（Matrix），更一般地说，它通常是张量（Tensor）。向量和矩阵都只是张量的特殊情形：若只看单个样本的 MLP 隐层输出，最常见的是向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{h}\in\mathbb{R}^{d}\)</span>；若看 Transformer 对整段序列的隐藏状态，常写成矩阵 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span>；若再把 batch 维也带上，则会变成三维张量 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\in\mathbb{R}^{B\times L\times d}\)</span>。在卷积网络（Convolutional Neural Network, CNN）里，中间表示则常是特征图张量（Feature Map Tensor） <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\in\mathbb{R}^{C\times H\times W}\)</span>，或带 batch 的 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^{B\times C\times H\times W}\)</span>。</p>
<p>因此，“中间表示”不是某种神秘对象，它就是网络在某一层对输入所形成的内部编码。它不像最终标签那样直接面向人类语义，而是把输入重写成更适合下一层处理的坐标系。例如，文本模型中的某一层隐藏状态可能同时编码词义、上下文关系、句法位置和任务相关线索；图像模型中的某一层特征图则可能突出边缘、纹理、局部部件或更高层形状。后续层与任务头读到的，正是这些内部编码。</p>
<p>感知机的重要性不止在于历史地位，更在于今天大型模型中的知识，本质上仍然是通过这类权重结构逐层编码进去的。训练过程并不会把知识像数据库那样逐条写成显式记录，而是不断调整参数，使某些输入模式被放大、某些输入模式被抑制。于是，模型在数据中反复见到的统计规律——词与词的共现、图像局部纹理、特征之间的组合关系——都会被压缩进参数矩阵的数值结构中。</p>
<p>更准确地说，大模型中的知识通常不是“某一个感知机单独存储一条事实”，而是以<span style="background-color: #c0c0c0;">分布式表示（Distributed Representation）</span>的形式分散在大量参数里。单个单元更像一个局部特征探测器（Feature Detector）：它只对某种模式敏感；许多单元级联后，网络才能把低层简单模式组合成高层抽象概念。模型规模越大、层数越深、参数越多，可被编码的模式组合也越丰富，这正是大模型具备强表达能力与“知识容量”的原因之一。</p>
<p>感知机能学会线性可分任务，但无法处理 XOR 这类线性不可分问题，这正是多层网络出现的动机：当一个超平面不够时，就需要通过多层组合把输入空间逐步重写成更容易分开的表示。</p>
<div class="blog_h3"><span class="graybg">多层感知机（MLP）</span></div>
<p>多层感知机（Multi-Layer Perceptron, MLP）处理的核心问题是：当输入与输出之间的关系不是一个超平面就能表达时，如何通过多层可学习变换，把原始特征逐步改写成更容易完成任务的表示。它由多层线性变换与逐元素非线性激活交替组成，是最基本也最通用的前馈网络结构。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp.png"><img class="alignnone size-full wp-image-41415" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp.png" alt="mlp" width="1024" height="1024" /></a></p>
<p>单层可写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{h}=\phi(W\mathbf{x}+\mathbf{b})\]</span>
<p>多层堆叠后，可写为：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{h}^{(1)}=\phi(W^{(1)}\mathbf{x}+\mathbf{b}^{(1)}),\quad \mathbf{h}^{(2)}=\phi(W^{(2)}\mathbf{h}^{(1)}+\mathbf{b}^{(2)}),\quad \hat{\mathbf{y}}=W^{(3)}\mathbf{h}^{(2)}+\mathbf{b}^{(3)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(W^{(l)}\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}^{(l)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层参数， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{h}^{(l)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层隐藏表示。所谓“逐层重写表示”，就是每一层都在回答一个更具体的问题：哪些原始模式值得保留、哪些组合值得放大、哪些方向更利于后续任务。于是，早期层往往捕捉较局部、较简单的模式，后期层则把这些模式组合成更抽象的语义结构。</p>
<p>MLP 比单层感知机强，关键不在“层数更多”本身，而在于层与层之间插入了非线性激活函数 <span displaypfx="inline-" class="mathjax-container">\(\phi\)</span>。如果没有非线性，多层线性变换满足</p>
<span displaypfx="" class="mathjax-container">\[W^{(2)}(W^{(1)}\mathbf{x}+\mathbf{b}^{(1)})+\mathbf{b}^{(2)}=\tilde W\mathbf{x}+\tilde{\mathbf{b}}\]</span>
<p>最终仍然等价于一层线性变换，表达能力不会因为堆叠而提升。只有加入非线性后，网络才能把输入空间切分、折叠、拉伸并重新组合，形成复杂的分段线性或平滑非线性决策边界。</p>
<p>从几何角度看，单层模型像“用一个超平面切一次”；而多层 MLP 则是在表示空间中反复做坐标变换和非线性折叠，把原本难分的数据逐步变成线性头也能分开的形状。文中的激活函数对比图展示的正是这个过程：不同激活函数会把同一个三层网络变成完全不同的几何变换器。</p>
<p>在现代大模型中，MLP 的作用远不只是“附属模块”。在 Transformer 里，注意力层负责在 token 之间路由信息，而 MLP / Feed-Forward Network（FFN）则负责在每个位置上做通道维度的非线性特征变换，把路由来的信息重新编码进更强的表示。因此，很多语义模式、组合规则与任务相关知识，最终都会沉淀到这些大规模参数化的 MLP 权重中。</p>
<p>更进一步说，在 Transformer 的常见解释框架里，MLP / FFN 往往被看作<span style="background-color: #c0c0c0;">事实性知识的重要载体之一</span>。一个常见直觉是把 FFN 看成参数化的“键值存储器（Key-Value Memory）”：第一层线性变换更像在检测当前输入是否匹配某种模式或概念，第二层线性变换则把与该模式相关的语义方向重新写回残差流（Residual Stream）。这里的残差流，可以理解为 Transformer 里那条贯穿各层的主表示通道：每一层注意力与 MLP 的输出，都会通过残差相加的方式写回这条主通道，再交给后续层继续处理。因此，诸如“实体—属性”“术语—定义”“模式—响应”这类较稳定的关联，常常更容易在 MLP 权重里留下痕迹。</p>
<p>但这并不意味着“一个神经元就存一条事实”。更准确的描述是：<span style="background-color: #c0c0c0;">知识通常以分布式方式存在于许多层、许多通道和许多参数方向里</span>。单个神经元有时会对某种关系或概念特别敏感，因而出现所谓“知识神经元（Knowledge Neurons）”现象；但更稳定的事实表示，通常仍然依赖一组共同激活的单元和跨层传递的表示。可以把两类子层的分工概括为：注意力更擅长“去哪里找、和谁建立联系”，MLP 更擅长“把匹配到的模式变成可供后续层使用的语义内容”。因此，说 MLP 是知识的主要载体之一是合理的；说知识只存在于 MLP、完全不在注意力里，则过于简单。</p>
<div class="blog_h2"><span class="graybg">激活函数</span></div>
<p>激活函数（Activation Function）的作用是给线性层引入非线性。如果没有激活函数，多层线性层叠起来仍然等价于一层线性变换，深度就失去了意义。</p>
<p>先给出一个实用的选型总表。它不代替后文的机制分析，但可以先回答工程上最常见的问题：某个激活函数通常应该放在输出层、隐藏层，还是特定结构里。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">激活函数</td>
<td style="text-align: center;">输出范围</td>
<td style="text-align: center;">典型适用场景</td>
<td style="text-align: center;">主要原因</td>
</tr>
</thead>
<tbody>
<tr>
<td>Sigmoid</td>
<td><span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span></td>
<td>二分类输出层；LSTM / GRU 门控</td>
<td>天然给出概率或开关强度，但隐藏层易饱和</td>
</tr>
<tr>
<td>Tanh</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span></td>
<td>较浅网络；需要零中心有界激活的结构</td>
<td>比 sigmoid 更利于优化，但深层仍会饱和</td>
</tr>
<tr>
<td>ReLU</td>
<td><span displaypfx="inline-" class="mathjax-container">\([0,+\infty)\)</span></td>
<td>深层前馈网络、CNN 的默认隐藏层</td>
<td>不饱和、计算便宜、分段线性、优化稳定</td>
</tr>
<tr>
<td>Leaky ReLU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\infty,+\infty)\)</span></td>
<td>担心 Dying ReLU 的深层隐藏层</td>
<td>保留 ReLU 优点，同时避免负半轴完全断梯度</td>
</tr>
<tr>
<td>ELU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\alpha,+\infty)\)</span></td>
<td>希望激活更平滑、且均值更接近 0 的隐藏层</td>
<td>负区间平滑并可取负值，但计算开销高于 ReLU</td>
</tr>
<tr>
<td>GELU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\infty,+\infty)\)</span></td>
<td>Transformer、BERT 类模型的隐藏层</td>
<td>选择性强且过渡平滑，兼顾表达能力与优化平滑性</td>
</tr>
<tr>
<td>Swish / SiLU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\infty,+\infty)\)</span></td>
<td>现代卷积网络；部分大模型隐藏层</td>
<td>软门控、平滑、比硬截断更柔和</td>
</tr>
<tr>
<td>Softmax</td>
<td>各分量在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 且总和为 1</td>
<td>多分类输出层；语言模型词表分布输出</td>
<td>把 logits 归一化为概率分布，不用于普通隐藏层</td>
</tr>
</tbody>
</table>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp-activation-summary.png"><img class="alignnone size-full wp-image-41289" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp-activation-summary.png" alt="mlp-activation-summary" width="1920" height="957" /></a></p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/activation-functions-grid.png"><img class="alignnone size-full wp-image-41461" src="https://blog.gmem.cc/wp-content/uploads/2026/03/activation-functions-grid.png" alt="activation-functions-grid" width="1920" height="1079" /></a></p>
<div class="blog_h3"><span class="graybg">Sigmoid</span></div>
<p>Sigmoid 把实数压到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\sigma(x)=\frac{1}{1+e^{-x}}\]</span>
<p>Sigmoid 的优势在于输出天然落在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>，因此非常适合表示概率，尤其常用于二分类输出层或门控结构中。但它在隐藏层里的主要问题是饱和（saturation）：当输入绝对值较大时，函数会迅速贴近 0 或 1，此时导数接近 0，梯度在反向传播时会被不断压缩，深层网络因此容易出现梯度消失（Vanishing Gradient）。从优化角度看，这意味着前面层参数即使有误，也很难收到足够强的更新信号。</p>
<p>此外，sigmoid 的输出始终为正，不以 0 为中心，这会使后续层接收到带偏移的激活分布，通常不利于优化动态的稳定性。因此，sigmoid 今天更多保留在“需要概率解释”的输出层，或在 LSTM / GRU 等门控结构中充当开关函数，而不再是深层前馈隐藏层的默认选择。</p>
<div class="blog_h3"><span class="graybg">Tanh</span></div>
<p>Tanh 的输出范围是 <span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}\]</span>
<p>Tanh 可以看作“零中心版 sigmoid”：它同样会在大幅度输入时饱和，但输出分布位于 <span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span> 且以 0 为中心，这通常比 sigmoid 更利于优化，因为后续层接收到的激活不再整体偏向正侧。对于需要表达“正负方向”差异的隐藏表示，tanh 往往也比 sigmoid 更自然。</p>
<p>不过，tanh 并没有解决饱和带来的根本问题：当 <span displaypfx="inline-" class="mathjax-container">\(|x|\)</span> 很大时，导数仍接近 0，深层网络中的梯度传播依然会变弱。因此，在较深的前馈网络里，tanh 通常不如 ReLU 家族稳定；它更多出现在较浅网络、早期神经网络设计，或某些希望激活有界且零中心的结构中。</p>
<div class="blog_h3"><span class="graybg">ReLU</span></div>
<p>ReLU（Rectified Linear Unit）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ReLU}(x)=\max(0,x)\]</span>
<p>ReLU 看起来几乎“过于简单”，但它之所以长期有效，关键不在公式花哨，而在于它同时满足了深层优化最需要的几条性质。第一，ReLU 在正半轴不饱和（non-saturating）：当 <span displaypfx="inline-" class="mathjax-container">\(x&gt;0\)</span> 时导数恒为 1，梯度穿过这一单元时不会像 sigmoid / tanh 那样被持续压小，因此更有利于深层网络中的梯度传播。第二，ReLU 保留了非线性，但正半轴仍是线性的，这使整个网络变成<span style="background-color: #c0c0c0;">分段线性（piecewise linear）</span>系统：表达能力足够强，同时局部优化形状又比高度弯曲的饱和函数更“规整”。第三，负半轴直接截断会带来自然的稀疏激活（sparse activation）：不是所有单元都会在每个样本上同时活跃，这通常有助于提升表示分解能力，并降低无效共适应（co-adaptation）。这里的共适应，指多个神经元在训练中形成了过强的相互依赖：某个单元之所以有效，不是因为它单独学到了稳定、可迁移的模式，而是因为它总是和另外几个特定单元“成套工作”。一旦输入分布变化，或其中某些单元没有按训练时那样响应，这种脆弱的协同关系就容易失效，从而削弱泛化能力。</p>
<p>从工程角度看，ReLU 的优势还包括计算代价极低，只需一次比较运算；这在大规模训练中会被成千上万层和数十亿次前向/反向传播放大。更深层的原因是：深度网络真正需要的不是“平滑得很漂亮”的激活函数，而是一个既能打破线性、又不会在大范围内把梯度压扁、还能让优化器容易工作的非线性。ReLU 恰好在这三点之间取得了非常实用的平衡，这就是为什么一个形式上极其朴素的函数，反而成为现代深度学习最成功的默认选择之一。它的代价也很明确：负区间梯度为 0，单元可能长期失活，这就是后面 Leaky ReLU、ELU、GELU 等变体继续改进的出发点。</p>
<div class="blog_h3"><span class="graybg">Leaky ReLU</span></div>
<p>Leaky ReLU 给负半轴保留一个很小的斜率：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{LeakyReLU}(x)=\max(\alpha x,x),\quad \alpha\ll 1\]</span>
<p>这样做是为了缓解“死亡 ReLU（Dying ReLU）”问题：若某神经元长期落在负区间，普通 ReLU 的梯度可能一直为 0，而 Leaky ReLU 仍保留一点更新信号。</p>
<p>从机制上看，Leaky ReLU 的核心改动很小，但很有针对性：它保留了 ReLU 在正半轴的不饱和与分段线性优点，同时避免把负半轴完全切断。这样即使某个单元暂时落入负区间，参数仍有机会通过非零梯度被重新拉回活跃状态。因此，Leaky ReLU 可以看作对 ReLU 的保守修正：表达风格几乎不变，但训练风险更低。</p>
<div class="blog_h3"><span class="graybg">ELU</span></div>
<p>ELU（Exponential Linear Unit）在负半轴使用指数平滑：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ELU}(x)=\begin{cases}x,&amp;x&gt;0\\ \alpha(e^x-1),&amp;x\le 0\end{cases}\]</span>
<p>它的目标是兼顾 ReLU 的优化优势和负区间的平滑性，使激活均值更接近 0。</p>
<p>与 Leaky ReLU 相比，ELU 在负半轴不再是简单直线，而是用指数曲线平滑衰减到负饱和值。这带来两个效果：一是避免了 ReLU 在 0 点附近过于生硬的折线结构；二是允许激活出现稳定的负值，从而减轻隐藏表示整体偏正的问题。代价是计算比 ReLU 更重，且负区间在极小值处同样会逐渐饱和，因此它通常被视为“更平滑、更零中心”的 ReLU 变体，而不是彻底不同的一类激活。</p>
<div class="blog_h3"><span class="graybg">GELU</span></div>
<p>GELU（Gaussian Error Linear Unit）可理解为“按输入大小平滑地决定保留多少信号”。它不像 ReLU 那样硬截断，而是对小正值和小负值做连续、概率化的保留，因此在 0 附近更平滑。</p>
<p>这种设计的价值在于：它仍然保留了 ReLU 家族的选择性——不是所有信号都被同等对待——但又避免了硬截断带来的尖锐折点和完全失活区。于是，GELU 往往能在“表达选择性”和“优化平滑性”之间取得更好的折中，这也是它在 Transformer、BERT 及其后续大量变体中被广泛采用的重要原因。</p>
<div class="blog_h3"><span class="graybg">Swish / SiLU</span></div>
<p>Swish / SiLU 定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{SiLU}(x)=x\,\sigma(x)\]</span>
<p>它是平滑、非单调的激活函数，在某些深层网络里表现优于 ReLU。其结构可以直接读成“输入值 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 乘上一个 sigmoid 门控 <span displaypfx="inline-" class="mathjax-container">\(\sigma(x)\)</span>”：当 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 很大时， <span displaypfx="inline-" class="mathjax-container">\(\sigma(x)\approx 1\)</span>，信号几乎原样通过；当 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 很小时， <span displaypfx="inline-" class="mathjax-container">\(\sigma(x)\approx 0\)</span>，信号被显著压低；在 0 附近则是连续、平滑的软过渡。</p>
<p>这种形式的价值在于：它既不像 ReLU 那样做硬截断，也不像 sigmoid 那样把输出彻底压进固定区间，而是让网络学到一种“按输入强度自适应通过多少”的软门控机制。结果是，SiLU / Swish 往往能在保持优化平滑性的同时，保留较强的表达灵活性，因此在一些现代卷积网络与大模型变体中表现良好。它可以看作介于 ReLU 家族与门控激活之间的一种折中设计。</p>
<div class="blog_h3"><span class="graybg">Softmax</span></div>
<p>Softmax 把一组实数分数（scores）映射为概率分布（Probability Distribution）。在分类与语言模型里，这组分数通常称为 logit（Logits）：它们是 softmax 之前的未归一化输出。</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{softmax}(z)_i=\frac{e^{z_i}}{\sum_{j=1}^{V} e^{z_j}}\]</span>
<p>logits 的两个关键性质：</p>
<ul>
<li>logits 不需要是概率，可以是任意实数；softmax 才把它变成 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span> 且和为 1 的分布。</li>
<li>softmax 对整体平移不敏感：对任意常数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{softmax}(z)=\mathrm{softmax}(z+c\mathbf{1})\)</span>。因此实现里常用 <span displaypfx="inline-" class="mathjax-container">\(z\leftarrow z-\max_i z_i\)</span> 做数值稳定（Numerical Stability）。</li>
</ul>
<p>这说明 softmax 真正关心的是各个 logit 之间的相对差值，而不是某个 logit 的绝对数值。若把所有分数同时加 10，模型对“哪一类更占优”的判断不会改变，因为指数项会在分子和分母里同时乘上 <span displaypfx="inline-" class="mathjax-container">\(e^{10}\)</span>，最终完全约掉。改变 softmax 输出的，是某个类别相对其他类别高了多少，而不是整体抬高或压低所有分数。</p>
<p>也正因为如此，logit 更适合理解为“未归一化偏好分数（unnormalized preference scores）”，而不是“概率雏形”。例如两类 logits 从 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\((101,102)\)</span>，softmax 输出完全相同；但若从 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\((1,5)\)</span>，第二类相对第一类的优势被显著拉大，概率才会明显变化。</p>
<p>在语言模型（Language Model）中，给定最后一层隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d_{\text{model}}}\)</span>，线性输出头产生词表大小 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 的 logits： <span displaypfx="inline-" class="mathjax-container">\(z=hW_{\text{vocab}}+b\)</span>，再经 softmax 得到下一个 token 的分布。若采用权重共享（Weight Tying），则输入嵌入表 <span displaypfx="inline-" class="mathjax-container">\(E\in\mathbb{R}^{V\;\times d_{\text{model}}}\)</span> 与输出头满足 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab}}=E^\top\)</span>，于是可直接写成 <span displaypfx="inline-" class="mathjax-container">\(z=hE^\top+b\)</span>。</p>
<div class="blog_h4"><span class="graybg">Softmax和分类任务</span></div>
<p>在多分类任务里，这几个概念实际上是一条连续的计算链：任务头先输出 logits <span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}^{C}\)</span>，softmax 把它们变成条件概率 <span displaypfx="inline-" class="mathjax-container">\(p(y=i|x)\)</span>，再取真实类别 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 的负对数概率作为单样本损失，也就是负对数似然（Negative Log-Likelihood, NLL）。</p>
<span displaypfx="" class="mathjax-container">\[p(y=i|x)=\mathrm{softmax}(z)_i=\frac{e^{z_i}}{\sum_{j=1}^{C}e^{z_j}},\qquad \ell_{\mathrm{NLL}}(z,c)=-\log p(y=c|x)\]</span>
<p>把两步合起来，NLL 可以直接写成 logits 的函数：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{NLL}}(z,c)=-\log\frac{e^{z_c}}{\sum_{j=1}^{C}e^{z_j}}=-z_c+\log\sum_{j=1}^{C}e^{z_j}\]</span>
<p>这个式子把训练目标拆成了两部分：第一项 <span displaypfx="inline-" class="mathjax-container">\(-z_c\)</span> 要求真实类别的 logit 足够大；第二项 <span displaypfx="inline-" class="mathjax-container">\(\log\sum_j e^{z_j}\)</span> 是归一化项（log-sum-exp），它把所有类别的竞争都算进去。因此训练并不是单独把正确类别分数抬高，而是要让它相对其他类别更占优势。</p>
<p>softmax 的平移不变性（Translation Invariance）在这里也能直接看见。对任意常数 <span displaypfx="inline-" class="mathjax-container">\(a\)</span>，若把所有 logits 同时改为 <span displaypfx="inline-" class="mathjax-container">\(z+a\mathbf{1}\)</span>，则 softmax 概率不变，NLL 也不变：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{softmax}(z+a\mathbf{1})_i=\mathrm{softmax}(z)_i,\qquad \ell_{\mathrm{NLL}}(z+a\mathbf{1},c)=\ell_{\mathrm{NLL}}(z,c)\]</span>
<p>因此 logits 的绝对零点没有意义，真正有意义的是类别之间的相对差值。工程实现里常把 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 先整体减去 <span displaypfx="inline-" class="mathjax-container">\(\max_i z_i\)</span>，再计算 softmax 或 log-sum-exp。这样不会改变概率与损失，但能显著降低指数溢出的风险。</p>
<div class="blog_h2"><span class="graybg">损失函数</span></div>
<p>术语区分：假设函数（Hypothesis Function）/模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 定义“模型在做什么映射”；样本损失（Loss Function）定义在单样本上；代价函数/成本函数（Cost Function）是把样本损失在全数据集上做平均或求和后的经验风险；目标函数（Objective Function）是优化器真正要优化的函数，最常见写法是 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)=L(\theta)+\lambda\Omega(\theta)\)</span>。</p>
<div class="blog_h3"><span class="graybg">回归损失</span></div>
<div class="blog_h4"><span class="graybg">MSE</span></div>
<p>均方误差（Mean Squared Error, MSE）定义为</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{MSE}}(y,\hat y)=(\hat y-y)^2\]</span>
<p>平方的作用是让大误差被放大处罚。若一个样本错 10，另一个样本错 1，那么前者在 MSE 里不是“重 10 倍”，而是“重 100 倍”。因此 MSE 很适合你明确希望重罚大错的场景，也对应前面讲过的高斯噪声假设。</p>
<div class="blog_h4"><span class="graybg">MAE</span></div>
<p>平均绝对误差（Mean Absolute Error, MAE）对应单样本形式</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{MAE}}(y,\hat y)=|\hat y-y|\]</span>
<p>它直接度量偏差大小，对离群点更鲁棒，因为不会像平方那样把大误差急剧放大。若做房价预测，数据中存在一批价格远高于主体分布的豪宅样本时，MAE 往往比 MSE 更稳；这里的“主体分布”指的是样本中占大多数的普通住宅价格区间，而豪宅样本相对它明显偏高。这样一来，哪怕豪宅样本数量不多，它们也会在 MSE 下因为误差被平方而获得过大的影响力。</p>
<div class="blog_h4"><span class="graybg">Huber Loss</span></div>
<p>Huber Loss 结合了 MSE 与 MAE：误差小时像平方误差，误差大时像绝对误差。设阈值 <span displaypfx="inline-" class="mathjax-container">\(\delta\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\ell_\delta(r)=\begin{cases}\frac{1}{2}r^2,&amp;|r|\le \delta\\ \delta(|r|-\frac{1}{2}\delta),&amp;|r|&gt;\delta\end{cases},\quad r=\hat y-y\]</span>
<p>这条式子的直觉是：小误差区间内保持平滑、便于优化；大误差区间内降低对离群点的过度敏感。它像“正常误差严肃处理，极端异常别让它一票否决整个模型”。</p>
<div class="blog_h3"><span class="graybg">分类损失</span></div>
<div class="blog_h4"><span class="graybg">交叉熵损失（Binary）</span></div>
<p>二分类交叉熵（Binary Cross-Entropy, BCE）用来训练输出概率 <span displaypfx="inline-" class="mathjax-container">\(p\in(0,1)\)</span> 的二分类器。这里 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 通常表示模型预测“正类”的概率， <span displaypfx="inline-" class="mathjax-container">\(y\in\{0,1\}\)</span> 是真实标签： <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 表示正类， <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span> 表示负类。</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{BCE}}(y,p)=-\Big(y\log p+(1-y)\log(1-p)\Big)\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(\ell_{\mathrm{BCE}}(y,p)\)</span> 表示“单个样本在真实标签为 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>、模型预测正类概率为 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 时的 BCE 损失值”。也就是说，这不是新的概率，而是一个标量惩罚：预测越符合真实标签，它越小；预测越违背真实标签，它越大。</p>
<p>这条公式会根据 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的取值自动选择应当惩罚哪一项。当 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 时，式中的 <span displaypfx="inline-" class="mathjax-container">\((1-y)=0\)</span>，因此第二项消失，损失化简为 <span displaypfx="inline-" class="mathjax-container">\(-\log p\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span> 时，第一项中的 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span>，因此第一项消失，损失化简为 <span displaypfx="inline-" class="mathjax-container">\(-\log(1-p)\)</span>。于是它惩罚的本质就是：让真实类别对应的概率尽可能高。</p>
<p>数值例子（自然对数）：若 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(p=0.9\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\ell\approx 0.105\)</span>；若 <span displaypfx="inline-" class="mathjax-container">\(p=0.1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\ell\approx 2.303\)</span>。正确但不自信会被罚，错误且自信会被重罚。</p>
<div class="blog_h4"><span class="graybg">交叉熵损失（Categorical）</span></div>
<p>多分类交叉熵（Categorical Cross-Entropy, CE）与 softmax 通常配套使用。这里 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实分布（Ground-Truth Distribution）在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类上的概率质量， <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 是模型预测分布（Predicted Distribution）在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类上的概率。若类别总数为 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>，则单样本损失写成：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{CE}}(y,p)=-\sum_{i=1}^{C} y_i\log p_i\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(\ell_{\mathrm{CE}}(y,p)\)</span> 表示“当真实标签分布为 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>、模型预测分布为 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 时，这个样本对应的交叉熵损失值”。其本质是：<span style="background-color: #c0c0c0;">用真实标签分布 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 作为权重，对预测分布的负对数概率 <span displaypfx="inline-" class="mathjax-container">\(-\log p_i\)</span> 做加权平均</span>。</p>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是 one-hot 分布时，只有真实类别 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 那一维的权重为 1，其余维度权重都为 0，因此求和会自动塌缩成单项：</p>
<span displaypfx="" class="mathjax-container">\[\ell=-\log p_c\]</span>
<p>这正是分类任务里最常见的形式。它与最大似然估计（Maximum Likelihood Estimation, MLE）完全一致：最小化交叉熵等价于最大化真实类别的对数似然。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 不再是 one-hot，而是一个在多个类别上分配了非零概率质量的分布，那么交叉熵就不再只读取单个类别 <span displaypfx="inline-" class="mathjax-container">\(p_c\)</span>，而是会对整条标签分布做加权。常见来源有两类。</p>
<p>第一类是软标签（Soft Label）。它指真实监督信号本身就是一个概率分布，而不是“只有一个绝对正确类别”的硬标签（Hard Label）。例如一张图像可能被标注为“70% 像猫、30% 像狐狸”，或一个样本本身就带有多标注者投票汇总后的类别分布。在这种情况下， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 直接表示第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类的目标概率，交叉熵自然要对所有类别一起计算。</p>
<p>第二类是教师分布蒸馏（Knowledge Distillation from Teacher Distribution）。知识蒸馏（Knowledge Distillation）的做法是：不用人工标签单独监督学生模型（Student Model），而是让学生去拟合教师模型（Teacher Model）给出的类别分布。若教师在某个样本上输出 <span displaypfx="inline-" class="mathjax-container">\(q\)</span>，学生输出 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，则训练目标常包含 <span displaypfx="inline-" class="mathjax-container">\(-\sum_i q_i\log p_i\)</span> 这样的交叉熵或等价的 KL 散度项。它传递的不只是“哪一类是对的”，还传递“其余类别分别有多像”，因此常被称为暗知识（Dark Knowledge）。</p>
<p>这两种情况的共同点是：标签本身已经不是单点答案，而是一条分布。于是交叉熵的计算对象就不再只是“真实类别那一项”，而是整个目标分布与预测分布之间的匹配程度。Label Smoothing 也会把 one-hot 改成软分布，但它承担的是训练目标修正的角色，因此放在后文“分类任务正则化”里更清晰。</p>
<p>从信息论角度看，交叉熵的标准定义是两个分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 之间的</p>
<span displaypfx="" class="mathjax-container">\[H(P,Q)=-\sum_x P(x)\log Q(x)\]</span>
<p>分类里的 <span displaypfx="inline-" class="mathjax-container">\(\ell_{\mathrm{CE}}(y,p)\)</span> 与这个定义并不矛盾，它只是把信息论中的 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 分别具体化成了“单个样本对应的真实标签分布 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>”与“模型在这个样本上的预测分布 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>”。若标签是 one-hot，那么 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 退化成一个只在真实类别处取值为 1 的离散分布，于是信息论里的交叉熵自然退化成 <span displaypfx="inline-" class="mathjax-container">\(-\log p_c\)</span>。</p>
<p>进一步地，交叉熵与 KL 散度（Kullback–Leibler Divergence）的关系可以直接写成：</p>
<span displaypfx="" class="mathjax-container">\[H(P,Q)=H(P)+D_{\mathrm{KL}}(P\|Q)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(H(P)=-\sum_x P(x)\log P(x)\)</span> 是真实分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 自身的熵（Entropy），只由数据分布决定； <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(P\|Q)=\sum_x P(x)\log\frac{P(x)}{Q(x)}\)</span> 是 KL 散度，用来衡量预测分布 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 相对真实分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 的偏离程度。于是，在训练数据固定时， <span displaypfx="inline-" class="mathjax-container">\(H(P)\)</span> 是常数，最小化交叉熵 <span displaypfx="inline-" class="mathjax-container">\(H(P,Q)\)</span> 就等价于最小化 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(P\|Q)\)</span>。</p>
<p>因此，“最小化交叉熵就是最小化 KL 散度”这句话在监督学习里通常成立，但更准确的表述是：<span style="background-color: #c0c0c0;">在真实分布固定不变时，最小化交叉熵与最小化预测分布对真实分布的 KL 偏离是等价的</span>。KL 散度确实可以理解为“与真实分布的差异或偏离”，但它不是对称距离（Symmetric Distance）：一般有 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(P\|Q) \neq D_{\mathrm{KL}}(Q\|P)\)</span>，也不满足严格距离函数的三角不等式，因此更准确的名称是分布失配（distribution mismatch）或相对熵（relative entropy）。</p>
<p>若进一步把 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 写成 softmax 作用在 logits <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 上，则多分类交叉熵可直接写成：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{CE}}(z,c)=-\log\mathrm{softmax}(z)_c=-z_c+\log\sum_{j=1}^{C}e^{z_j}\]</span>
<p>这条式子把任务头和损失函数直接接起来：线性头先输出 logits，softmax 把它们归一化为概率，交叉熵再读取真实类别的负对数概率。由于 softmax 具有平移不变性，给所有 logits 同时加上同一个常数不会改变这个损失，因此实现中通常直接从 logits 计算交叉熵（log-sum-exp 形式），并先减去 <span displaypfx="inline-" class="mathjax-container">\(\max_j z_j\)</span> 做数值稳定，而不是显式先算 softmax 再取 log。</p>
<div class="blog_h4"><span class="graybg">Focal Loss</span></div>
<p>Focal Loss 常用于类别极不平衡的分类任务，尤其是目标检测（Object Detection）这类“负样本远多于正样本”的场景。它不是简单换掉交叉熵，而是在交叉熵前再乘一个与样本难度相关的调制因子，使已经分得很对的容易样本贡献变小，把梯度预算更多留给困难样本：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{focal}}(p_t)=-(1-p_t)^\gamma\log p_t\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(p_t\)</span> 表示“真实类别对应的预测概率”：若真实标签 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(p_t=p\)</span>；若 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(p_t=1-p\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(-\log p_t\)</span> 就是普通交叉熵，而前面的 <span displaypfx="inline-" class="mathjax-container">\((1-p_t)^\gamma\)</span> 是额外加上的难度调制项。参数 <span displaypfx="inline-" class="mathjax-container">\(\gamma\ge 0\)</span> 控制聚焦强度： <span displaypfx="inline-" class="mathjax-container">\(\gamma=0\)</span> 时，Focal Loss 退化回普通交叉熵； <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 越大，对容易样本的压低越明显。</p>
<p>这个机制的关键在于样本难度如何反映到权重上。若某个样本已经分得很对，例如真实类别概率 <span displaypfx="inline-" class="mathjax-container">\(p_t=0.99\)</span>，则调制因子 <span displaypfx="inline-" class="mathjax-container">\((1-p_t)^\gamma\)</span> 会非常小；以 <span displaypfx="inline-" class="mathjax-container">\(\gamma=2\)</span> 为例，权重大约只有 <span displaypfx="inline-" class="mathjax-container">\((0.01)^2=10^{-4}\)</span>，这意味着它对总损失和梯度的影响被大幅削弱。相反，若某个样本很难，例如 <span displaypfx="inline-" class="mathjax-container">\(p_t=0.2\)</span>，则权重约为 <span displaypfx="inline-" class="mathjax-container">\((0.8)^2=0.64\)</span>，其损失会被较大程度保留。于是训练过程不再被海量“早就分对的简单样本”主导，而会持续关注误分样本、边界样本和少数类样本。</p>
<p>目标检测是最典型的应用例子。以单阶段检测器（One-stage Detector）为例，一张图像上往往有成千上万个候选框（Anchors），但真正包含目标的正样本只占极少数；绝大多数候选框都是背景。若直接使用普通交叉熵，训练会被这些“背景且容易判断”的负样本淹没：它们单个损失虽小，但数量太多，累积后仍然主导梯度。Focal Loss 的作用正是把这批容易背景样本的权重压下去，让模型把更多注意力放在少数正样本、遮挡目标、边界模糊目标，以及那些看起来像目标但其实是背景的困难负样本上。这样做通常会显著改善长尾检测与前景-背景极不平衡时的训练效果。</p>
<div class="blog_h4"><span class="graybg">Hinge Loss</span></div>
<p>Hinge Loss 是 SVM 常用的分类损失：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{hinge}}(y,f)=\max(0,1-yf),\quad y\in\{-1,+1\}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 是模型分数而不是概率。当 <span displaypfx="inline-" class="mathjax-container">\(yf\ge 1\)</span> 时，说明不仅分类正确，而且留出了足够间隔，损失为 0；当 <span displaypfx="inline-" class="mathjax-container">\(yf&lt;1\)</span> 时，就要受罚。它强调的不只是“分对”，而是“分对且留有安全距离”。</p>
<div class="blog_h3"><span class="graybg">度量学习损失</span></div>
<p>度量学习（Metric Learning）不直接预测类别，而是学习一个表示空间，让“应该相似的样本靠近，不该相似的样本拉远”。这类损失在检索、人脸识别、推荐召回和 embedding 学习中非常常见。</p>
<div class="blog_h4"><span class="graybg">Contrastive Loss</span></div>
<p>Contrastive Loss 处理样本对（pair）。若一对样本应相似，则拉近它们；若应不同，则至少推开到某个间隔 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 之外：</p>
<span displaypfx="" class="mathjax-container">\[\ell=y\,d^2+(1-y)\max(0,m-d)^2\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 是两者在嵌入空间中的距离， <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 表示正对， <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span> 表示负对。它像“朋友要坐得近，陌生人至少别挤在一起”。</p>
<div class="blog_h4"><span class="graybg">Triplet Loss</span></div>
<p>Triplet Loss 使用三元组：锚点（Anchor）、正样本（Positive）、负样本（Negative）。目标是让锚点离正样本比离负样本更近至少一个 margin：</p>
<span displaypfx="" class="mathjax-container">\[\ell=\max\big(0,\ d(a,p)-d(a,n)+m\big)\]</span>
<p>这条式子表达的是一种相对排序约束，而不是绝对相似度分数。它非常适合“谁比谁更像”的任务，例如人脸验证：同一个人的两张照片应比不同人的照片更近。</p>
<div class="blog_h4"><span class="graybg">InfoNCE</span></div>
<p>InfoNCE 是现代对比学习最常见的损失之一。对一个锚点来说，它把正样本放进一堆候选里，要求模型把正样本打分最高：</p>
<span displaypfx="" class="mathjax-container">\[-\log \frac{\exp(\mathrm{sim}(z_i,z_i^+)/\tau)}{\sum_j \exp(\mathrm{sim}(z_i,z_j)/\tau)}\]</span>
<p>分子是正确配对，分母是所有候选。这个结构和 softmax 分类非常像，只不过类别不再是固定标签，而是“在一堆候选里，谁才是真正匹配的那个”。在大语言模型 embedding、图像表征学习和多模态对齐里，它几乎是标准配置。</p>
<div class="blog_h2"><span class="graybg">任务头（Task Head）</span></div>
<p>任务头（Task Head）是把主干网络（Backbone）产出的隐藏表示（Hidden Representation）映射到具体任务输出空间的模块。主干负责抽取通用特征，任务头负责把特征“读出来”并对齐到目标形式（类别、数值、序列标签、跨度、关系等）。在工程上，绝大多数“用 Transformer 做下游任务”都可以写成：<span style="background-color: #c0c0c0;">Transformer backbone + task head + task loss</span>。</p>
<div class="blog_h3"><span class="graybg">中间表示、logits 与任务头的关系</span></div>
<p>主干网络输出的隐藏表示（Hidden Representation）是任务头的输入，任务头则是把这种内部表示读成具体任务输出的最后一层或最后几层变换。若把主干输出记为 <span displaypfx="inline-" class="mathjax-container">\(h\)</span>、<span displaypfx="inline-" class="mathjax-container">\(H\)</span> 或更一般的张量 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span>，则任务头通常先做一次线性读出（Linear Readout）得到分数，再视任务类型决定是否接 sigmoid、softmax、CRF 解码或直接保留实数输出。</p>
<p>logits 就是在这个读出阶段最常见的中间产物。它们是<span style="background-color: #c0c0c0;">任务头输出、但尚未归一化或尚未解码的原始分数</span>。例如，多分类头常先产生 <span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}^{C}\)</span>，这里 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 是类别数， <span displaypfx="inline-" class="mathjax-container">\(z_c\)</span> 表示模型对第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 类的偏好分数；softmax 之后这些分数才变成概率。对 token 分类任务，task head 产生的不是单个向量，而是一整张 logits 矩阵 <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times C}\)</span>；对语言模型，输出则是词表 logits <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times V}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 是词表大小。</p>
<p>因此，关系可以概括为：<span style="background-color: #c0c0c0;">输入先被 backbone 编码成中间表示，中间表示再被 task head 读成 logits 或其他任务分数，最后再由概率映射、解码器或损失函数把这些分数变成最终预测</span>。logits 不是所有中间表示的统称，而是“距离最终任务输出只差一步”的那类任务分数；它们通常由任务头生成，而不是由主干网络中间每一层都显式生成。</p>
<div class="blog_h3"><span class="graybg">任务头输出对照表</span></div>
<p>不同任务头的差异，最核心地体现在“直接输出什么张量、这些张量后面还要经过什么处理”这两个问题上。下面这张表把常见任务头的输入形状、直接输出和后续处理并列起来，便于从工程实现角度快速对照。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">任务类型</td>
<td style="text-align: center;">任务头常见输入</td>
<td style="text-align: center;">任务头直接输出</td>
<td style="text-align: center;">后续处理</td>
</tr>
</thead>
<tbody>
<tr>
<td>二分类</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>标量 logit <span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}\)</span>，或二维 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{2}\)</span></td>
<td>sigmoid 或 softmax，得到类别概率</td>
</tr>
<tr>
<td>多分类</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>类别 logits 向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{C}\)</span></td>
<td>softmax 后得到 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 类概率</td>
</tr>
<tr>
<td>多标签分类</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>每个标签一个 logit，组成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{C}\)</span></td>
<td>对每一维独立做 sigmoid，而不是在类别间做 softmax</td>
</tr>
<tr>
<td>回归</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>实数或向量 <span displaypfx="inline-" class="mathjax-container">\(\hat{\mathbf{y}}\in\mathbb{R}^{m}\)</span></td>
<td>通常不做概率归一化，直接配合回归损失</td>
</tr>
<tr>
<td>Token 分类 / NER</td>
<td>序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span></td>
<td>token-level logits 矩阵 <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times C}\)</span></td>
<td>逐 token softmax，或接 CRF 做全局解码</td>
</tr>
<tr>
<td>语言模型 / 文本生成</td>
<td>序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span></td>
<td>词表 logits <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times V}\)</span></td>
<td>对每个位置在词表维做 softmax，得到 next-token 分布</td>
</tr>
<tr>
<td>跨度抽取（Span Extraction）</td>
<td>序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span></td>
<td>起点 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\in\mathbb{R}^{L}\)</span> 与终点 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\in\mathbb{R}^{L}\)</span>，或 span 分数矩阵</td>
<td>在起止位置上做 softmax 或联合评分，输出片段边界</td>
</tr>
<tr>
<td>依存句法 / 关系抽取</td>
<td>成对表示 <span displaypfx="inline-" class="mathjax-container">\(h_i,h_j\)</span> 或序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\)</span></td>
<td>边分数矩阵 <span displaypfx="inline-" class="mathjax-container">\(S\in\mathbb{R}^{L\times L}\)</span>，或关系分数张量 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{S}\in\mathbb{R}^{L\times L\times C}\)</span></td>
<td>argmax、biaffine 解码或图结构约束解码</td>
</tr>
<tr>
<td>度量学习 / 检索</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>embedding 向量 <span displaypfx="inline-" class="mathjax-container">\(e\in\mathbb{R}^{d'}\)</span></td>
<td>不直接输出 logits；后续用相似度函数或对比损失比较</td>
</tr>
</tbody>
</table>
<p>这张表的关键在于区分“任务头直接输出什么”和“用户最终看到什么”。很多任务头直接输出的并不是概率，也不是标签，而是 logits、边分数、起止位置分数或 embedding。概率、标签、生成 token、依存边、异常分数等最终结果，通常还需要经过归一化、解码、阈值化或搜索过程才能得到。</p>
<div class="blog_h3"><span class="graybg">分类头</span></div>
<p>分类头（Classification Head）的核心职责，是把主干网络输出的表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span> 变成类别分数（Class Scores）或 logits。最常见的做法是一层线性映射：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{z}=W\mathbf{h}+\mathbf{b}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{h}\in\mathbb{R}^{d}\)</span> 是单个样本的隐藏表示， <span displaypfx="inline-" class="mathjax-container">\(W\)</span> 是任务头权重矩阵， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 是偏置向量， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 是未归一化类别分数。分类任务的关键区别不在于“有没有线性层”，而在于：<span style="background-color: #c0c0c0;">输出空间是否互斥、每个样本允许几个标签成立、以及这些分数之后接什么归一化与损失</span>。</p>
<p>直觉上，这个线性头就是一个“可学习的读出（Readout）”：在高维表示空间里用超平面（Hyperplane）切分区域，或用线性映射把表示投影到目标坐标系。这里的“读出”指的是：主干网络先把输入编码成内部表示，而任务头再把这种内部表示转换成模型真正需要输出的量，例如类别 logits、词表 logits、回归值、span 分数或关系分数。换言之，读出不是“再提特征”，而是<span style="background-color: #c0c0c0;">把已经形成的表示翻译成任务空间中的可判定分数</span>。</p>
<div class="blog_h4"><span class="graybg">二分类</span></div>
<p>二分类（Binary Classification）要求每个样本只在两个互斥类别中选一个，例如“垃圾 / 非垃圾”“欺诈 / 正常”“阳性 / 阴性”。最常见的写法是输出一个标量 logit：</p>
<span displaypfx="" class="mathjax-container">\[z=\mathbf{w}^\top \mathbf{h}+b\]</span>
<p>然后通过 sigmoid 得到正类概率：</p>
<span displaypfx="" class="mathjax-container">\[p=P(y=1\mid x)=\sigma(z)=\frac{1}{1+e^{-z}}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是模型对正类的原始偏好分数， <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是正类概率，负类概率则是 <span displaypfx="inline-" class="mathjax-container">\(1-p\)</span>。训练时通常配合二分类交叉熵（Binary Cross-Entropy, BCE）。工程上也可以输出二维 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{2}\)</span>，再接 softmax；但若任务确实只有两个互斥类别，单 logit + sigmoid 更常见，也更经济。</p>
<div class="blog_h4"><span class="graybg">多分类</span></div>
<p>多分类（Multi-class Classification）要求每个样本在 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 个互斥类别中恰好选一个类别，例如“猫 / 狗 / 鸟”“体育 / 财经 / 科技 / 娱乐”。此时任务头不会只输出一个标量，而是输出长度为 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 的 logits 向量：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{z}=W\mathbf{h}+\mathbf{b},\qquad W\in\mathbb{R}^{C\times d},\ \mathbf{z}\in\mathbb{R}^{C}\]</span>
<p>其中第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 维 <span displaypfx="inline-" class="mathjax-container">\(z_c\)</span> 表示模型对第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 个类别的原始偏好分数。由于这些类别互斥，后续通常接 softmax，把整组分数归一化成概率分布：</p>
<span displaypfx="" class="mathjax-container">\[p(y=c\mid x)=\frac{e^{z_c}}{\sum_{j=1}^{C}e^{z_j}}\]</span>
<p>这条式子的含义很直接：分子 <span displaypfx="inline-" class="mathjax-container">\(e^{z_c}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 类的相对强度，分母 <span displaypfx="inline-" class="mathjax-container">\(\sum_{j=1}^{C}e^{z_j}\)</span> 把所有类别一起归一化，因此最终得到的 <span displaypfx="inline-" class="mathjax-container">\(p(y=c\mid x)\)</span> 落在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 之间，且所有类别概率和为 1。也正因为总和必须为 1，多分类头天然表达的是“类间竞争”：某一类概率上升，其他类的总概率就必须下降。</p>
<p>训练时通常直接把 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 输入多分类交叉熵（Cross-Entropy）损失，而不是手动先算 softmax 再取对数。这样做的原因是数值稳定：损失函数内部会把 softmax 与对数合并成 log-sum-exp 形式，避免指数溢出。推理时若只关心类别标签，直接取 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_c z_c\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_c p(y=c\mid x)\)</span> 即可；若需要概率、阈值或校准，再显式使用 softmax。</p>
<div class="blog_h4"><span class="graybg">多标签分类</span></div>
<p>多标签分类（Multi-label Classification）与多分类名字相近，但任务结构完全不同。它不是“从多个类里选一个”，而是<span style="background-color: #c0c0c0;">同一个样本可以同时拥有多个标签</span>。例如一篇文章可以同时属于“AI、NLP、Transformer”，一张图片可以同时打上“室内、人物、宠物”三个标签。</p>
<p>这种任务里，标签之间不再互斥，因此不能使用 softmax。若仍用 softmax，所有标签概率会被强制归一化为和 1，相当于错误地假设“只能有一个标签成立”。多标签头通常输出 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 个 logits：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{z}=W\mathbf{h}+\mathbf{b},\qquad \mathbf{z}\in\mathbb{R}^{C}\]</span>
<p>但后续不是做一次整体 softmax，而是对每一维独立做 sigmoid：</p>
<span displaypfx="" class="mathjax-container">\[p_i=\sigma(z_i),\qquad i=1,\dots,C\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 表示“第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个标签是否成立”的独立概率。训练时通常使用逐维二分类交叉熵，即把每个标签都当作一个独立的二分类问题，再在标签维上求和或取平均：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{multi\mbox{-}label}}=-\sum_{i=1}^{C}\Big(y_i\log p_i+(1-y_i)\log(1-p_i)\Big)\]</span>
<p>因此，多标签头的关键不是“输出维度也叫 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>”，而是<span style="background-color: #c0c0c0;">这 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 维之间不竞争，每一维都在独立回答一个 yes/no 问题</span>。推理时也不是取单个 argmax，而是对每一维做阈值判断，例如输出所有满足 <span displaypfx="inline-" class="mathjax-container">\(p_i&gt;0.5\)</span> 的标签，或按业务分别设定不同标签阈值。</p>
<div class="blog_h3"><span class="graybg">回归头</span></div>
<p>回归头（Regression Head）不负责在离散类别之间做判别，而是直接输出连续数值（Continuous Value）或连续向量。最常见的形式仍然是一层线性映射：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\mathbf{y}}=W\mathbf{h}+\mathbf{b},\qquad \hat{\mathbf{y}}\in\mathbb{R}^{m}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 是回归目标维度。若 <span displaypfx="inline-" class="mathjax-container">\(m=1\)</span>，就是标量回归，例如房价预测、评分预测、温度预测；若 <span displaypfx="inline-" class="mathjax-container">\(m&gt;1\)</span>，则是多维回归，例如边界框坐标回归、姿态参数回归或多目标数值预测。</p>
<p>回归头通常不接 softmax，也不强制输出落在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>。原因很简单：回归任务关心的是数值大小本身，而不是类别概率竞争。训练时常配合均方误差（MSE）、平均绝对误差（MAE）或 Huber Loss。只有当目标值本身有明确范围约束时，才会额外接 sigmoid、tanh 或其他变换，把输出压到指定区间。</p>
<div class="blog_h3"><span class="graybg">语言模型头（LM Head）</span></div>
<p>语言模型头（Language Modeling Head, LM Head）是把隐藏表示映射回词表空间的输出头。只要任务目标是“在若干位置上对词表中的 token 做预测”，就会出现这一类头；因此它不只存在于 Decoder-only 大模型，也存在于 Encoder-only 的掩码语言模型（Masked Language Model, MLM）以及 Encoder-Decoder 的生成端。它读取主干网络在每个位置输出的隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\;\times d_{\text{model}}}\)</span>，并把每个位置的表示投影到整张词表空间，得到词表 logits：</p>
<span displaypfx="" class="mathjax-container">\[Z = HW_{\text{vocab}} + \mathbf{1}b^\top,\quad W_{\text{vocab}}\in\mathbb{R}^{d_{\text{model}}\;\times {V}},\ Z\in\mathbb{R}^{L\;\times {V}}\]</span>
<p>这条式子先描述整体矩阵，再自然落到单个位置。这里 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 是序列长度，表示当前一共有多少个位置； <span displaypfx="inline-" class="mathjax-container">\(d_{\text{model}}\)</span> 是隐藏维度； <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 是词表大小（Vocabulary Size）。因此，隐藏状态矩阵 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\;\times d_{\text{model}}}\)</span> 的每一行对应一个位置的表示，输出权重矩阵 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab}}\in\mathbb{R}^{d_{\text{model}}\;\times {V}}\)</span> 的每一列对应“词表中某个 token 作为候选答案时的读出方向”，最终得到的 <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\;\times {V}}\)</span> 就是一个“位置 <span displaypfx="inline-" class="mathjax-container">\(\times\)</span> 词表”的打分表：行表示位置，列表示候选 token。</p>
<p>把这张打分表聚焦到第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 行，就得到 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\in\mathbb{R}^{V}\)</span>，也就是“第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置对整张词表所有 token 的一整行 logits”。这里 <span displaypfx="inline-" class="mathjax-container">\(H_t\in\mathbb{R}^{d_{\text{model}}}\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏状态，冒号 <span displaypfx="inline-" class="mathjax-container">\(:\)</span> 表示该行的全部列；若写成 <span displaypfx="inline-" class="mathjax-container">\(Z_{:,i}\)</span>，则表示第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 列，也就是“所有位置对第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个 token 的分数”。继续缩小到单个元素 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,i}\)</span>，它表示“在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置，把词表中第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个 token 作为下一个输出时的原始分数”。</p>
<p>把矩阵形式按单个位置、单个候选 token 展开后，打分可写成：</p>
<span displaypfx="" class="mathjax-container">\[Z_{t,i}=H_t\cdot W_{\text{vocab},:,i}+b_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab},:,i}\)</span> 表示输出权重矩阵的第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 列，也就是与词表第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个 token 对应的参数向量； <span displaypfx="inline-" class="mathjax-container">\(b_i\)</span> 是该 token 的偏置项。这个公式的读法是：拿第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏表示 <span displaypfx="inline-" class="mathjax-container">\(H_t\)</span>，与“token <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 的读出向量”做一次点积，再加偏置，就得到该 token 在该位置的 logit。</p>
<p>LM Head 与分类头的根本区别在于输出空间。普通分类头通常只需输出 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 个类别分数；LM Head 则要在每个位置输出 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 个分数，而 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 往往达到几万甚至几十万。因此，LM Head 本质上是“逐位置的大规模多分类器”：每一步都在问“下一个 token 应该是词表中的哪一个”。</p>
<p>同一个 LM Head 公式，在不同 Transformer 架构里的使用方式并不相同。对 Encoder-only 模型，LM Head 通常服务于掩码语言建模：模型先用双向注意力得到各位置隐藏状态，再只在被遮蔽的位置上读取 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\)</span> 来预测原 token；这一过程通常是一次性编码，不涉及自回归生成，也没有 KV Cache 逐步增长的问题。对 Decoder-only 模型，LM Head 用于 next-token 预测：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏状态对应预测 <span displaypfx="inline-" class="mathjax-container">\(x_{t+1}\)</span>，推理时会逐步生成，因此会配合因果注意力（Causal Self-Attention）和 KV Cache。对 Encoder-Decoder 模型，LM Head 位于解码器一侧：编码器先产出源序列表示，解码器再在因果自注意力与交叉注意力（Cross-Attention）的共同作用下生成目标侧隐藏状态，最后由 LM Head 映射到词表。</p>
<p>训练时，自回归语言模型（Autoregressive Language Model）通常采用 next-token 目标：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(H_t\)</span> 用来预测真实的下一个 token <span displaypfx="inline-" class="mathjax-container">\(x_{t+1}\)</span>。对应损失可写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{LM}}=-\sum_{t=1}^{L-1}\log \frac{\exp(Z_{t,x_{t+1}})}{\sum_{i=1}^{V}\exp(Z_{t,i})}\]</span>
<p>这个损失公式也可以逐项拆开理解。左边的 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}_{\mathrm{LM}}\)</span> 是整段序列的语言模型损失；求和下标 <span displaypfx="inline-" class="mathjax-container">\(t=1,\dots,L-1\)</span> 表示：前 <span displaypfx="inline-" class="mathjax-container">\(L-1\)</span> 个位置都要各自预测一次下一个 token。分子里的 <span displaypfx="inline-" class="mathjax-container">\(\exp(Z_{t,x_{t+1}})\)</span> 表示“真实下一个 token 在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置对应的指数化分数”；这里 <span displaypfx="inline-" class="mathjax-container">\(x_{t+1}\)</span> 是真实的下一个 token id，所以 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,x_{t+1}}\)</span> 表示在位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 对这个真实 token 的 logit。分母 <span displaypfx="inline-" class="mathjax-container">\(\sum_{i=1}^{V}\exp(Z_{t,i})\)</span> 则把整张词表所有候选 token 的分数全部加起来做归一化。因此整个分式就是“在位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 预测真实下一个 token 的概率”，外面的负对数再把它变成训练损失。</p>
<p>推理时则不是直接输出标签，而是先对 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\)</span> 做 softmax 得到下一个 token 的条件分布。这里的 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\)</span> 不是一个标量，而是长度为 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 的向量，包含位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 对整张词表每个 token 的分数。对这整行做 softmax 后，得到的是：</p>
<span displaypfx="" class="mathjax-container">\[p(x_{t+1}=i\mid x_{\le t})=\frac{e^{Z_{t,i}}}{\sum_{j=1}^{V}e^{Z_{t,j}}},\qquad i=1,\dots,V\]</span>
<p>这条式子表示：在已经看到前缀 <span displaypfx="inline-" class="mathjax-container">\(x_{\le t}\)</span> 的条件下，下一个 token 取词表中第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个词的概率是多少。随后再配合贪心搜索（Greedy Decoding）、束搜索（Beam Search）、温度采样（Temperature Sampling）、top-k / top-p 等策略，从这组概率中选出真正生成的 token。也就是说，LM Head 负责把隐藏表示变成“词表级候选分数”，真正的文本生成还要再经过一层解码（Decoding）策略。</p>
<p>很多 LLM 还会使用权重共享（Weight Tying）：把输入嵌入表 <span displaypfx="inline-" class="mathjax-container">\(E\in\mathbb{R}^{V\;\times d_{\text{model}}}\)</span> 与输出头绑定，使 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab}}=E^\top\)</span>。这样一来，输入端“一个 token 的向量表示”与输出端“一个 token 作为候选答案时的原型向量”共用同一套参数空间。它通常既能减少参数量，也让输入和输出语义空间保持更强一致性。</p>
<p>从工程角度看，LM Head 往往比分类头更贵，因为它直接与词表大小 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 成正比：词表越大，最后一层的矩阵乘法、softmax 和采样都越重。因此，大模型的推理优化常会专门围绕 LM Head 展开，例如 fused softmax、采样优化、logits processor、词表裁剪或 speculative decoding 等。本质上，这些优化都在解决同一个问题：<span style="background-color: #c0c0c0;">如何更高效地从词表 logits 走到最终生成 token</span>。</p>
<div class="blog_h3"><span class="graybg">序列标注头（Token Classification）</span></div>
<p>序列标注（Sequence Labeling）/Token 分类（Token Classification）要求对每个 token 预测一个标签（例如 NER）。设主干输出 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span>（长度 <span displaypfx="inline-" class="mathjax-container">\(L\)</span>，隐藏维 <span displaypfx="inline-" class="mathjax-container">\(d\)</span>），则逐 token 线性头为：</p>
<span displaypfx="" class="mathjax-container">\[Z=HW^\top+\mathbf{1}b^\top,\quad W\in\mathbb{R}^{C\times d},\ Z\in\mathbb{R}^{L\times C}\]</span>
<p>对第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个 token，用 softmax 得到 <span displaypfx="inline-" class="mathjax-container">\(p(y_t|x)\)</span> 并用逐 token 交叉熵训练。该做法把每个位置的标签看作条件独立，能工作，但会忽略标签之间的结构约束（例如 BIO 体系中不允许 <pre class="crayon-plain-tag">I-ORG</pre> 紧跟 <pre class="crayon-plain-tag">O</pre>）。</p>
<p>例：对 “Apple Inc. is in California” 的 NER（Named Entity Recognition），合理标签序列可能是 <pre class="crayon-plain-tag">B-ORG I-ORG O O B-LOC</pre>。若逐 token softmax 独立预测，模型可能输出不合法的组合；这类“结构错误”通常需要结构化任务头（Structured Head）来显式建模。</p>
<div class="blog_h4"><span class="graybg">条件随机场（CRF）</span></div>
<p>线性链条件随机场（Linear-chain Conditional Random Field, CRF）在 token 分类头上增加一个转移矩阵（Transition Matrix）<span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{C\times C}\)</span>，对整段标签序列做归一化建模。令发射分数（Emission Score）为 <span displaypfx="inline-" class="mathjax-container">\(s_t(y)=Z_{t,y}\)</span>，则序列 <span displaypfx="inline-" class="mathjax-container">\(y_{1:L}\)</span> 的总分为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{score}(x,y)=\sum_{t=1}^{L}\Big(A_{y_{t-1},y_t}+s_t(y_t)\Big)\]</span>
<p>并定义条件概率：</p>
<span displaypfx="" class="mathjax-container">\[p(y|x)=\frac{\exp(\mathrm{score}(x,y))}{\sum_{y'}\exp(\mathrm{score}(x,y'))}\]</span>
<p>训练最小化负对数似然（Negative Log-Likelihood）；解码用 Viterbi 算法（动态规划）求 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_y \mathrm{score}(x,y)\)</span>。CRF 的收益是把“标签合法性/连贯性”学进转移项，从而显著减少结构错误。</p>
<div class="blog_h3"><span class="graybg">双仿射头（Biaffine）</span></div>
<p>双仿射（Biaffine）任务头常用于“成对打分”（Pairwise Scoring），例如依存句法（Dependency Parsing）里的“当前词应依附到哪个词”，或关系抽取（Relation Extraction）里的“两个实体之间是否存在某种关系”。它的名字可以按层级直接理解：对一个变量， <span displaypfx="inline-" class="mathjax-container">\(Wx\)</span> 是线性， <span displaypfx="inline-" class="mathjax-container">\(Wx+b\)</span> 是仿射；对两个变量， <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span> 是双线性（Bilinear）；在这个双线性项之外再加上线性项和偏置项，就得到双仿射（Biaffine）。因此，双仿射的本质是：<span style="background-color: #c0c0c0;">既建模两个表示之间的交互，又保留各自单独的角色偏好</span>。</p>
<span displaypfx="" class="mathjax-container">\[s(i,j)=h_i^\top U h_j + w^\top [h_i;h_j] + b\]</span>
<p>这条式子可以逐项拆开读。 <span displaypfx="inline-" class="mathjax-container">\(h_i,h_j\in\mathbb{R}^{d}\)</span> 是两个待配对对象的表示向量；在依存句法里，它们可分别表示“当前词”和“候选 head”；在关系抽取里，它们可分别表示“实体 1”和“实体 2”。 <span displaypfx="inline-" class="mathjax-container">\(U\in\mathbb{R}^{d\times d}\)</span> 是双线性参数矩阵，因此 <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span> 建模的是二者之间的交互强度：不是只看 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 自己，也不是只看 <span displaypfx="inline-" class="mathjax-container">\(h_j\)</span> 自己，而是看“这两个向量放在一起是否匹配”。把它展开后就是 <span displaypfx="inline-" class="mathjax-container">\(\sum_{p=1}^{d}\sum_{q=1}^{d}(h_i)_p\,U_{pq}\,(h_j)_q\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(U_{pq}\)</span> 可以理解为“第 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 个 dependent 特征与第 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 个 head 特征同时出现时，该给多少分”。</p>
<p>若任务需要一次输出多类关系分数，工程实现通常会为每个类别各放一张矩阵 <span displaypfx="inline-" class="mathjax-container">\(U^{(k)}\)</span>，或直接使用三阶参数张量。这样不同关系类型就能拥有不同的交互模式，而不必共用同一套 <span displaypfx="inline-" class="mathjax-container">\(U\)</span>。</p>
<p><span displaypfx="inline-" class="mathjax-container">\([h_i;h_j]\in\mathbb{R}^{2d}\)</span> 表示把两个向量直接拼接； <span displaypfx="inline-" class="mathjax-container">\(w^\top [h_i;h_j]\)</span> 是普通仿射项，可以进一步拆成“只依赖 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 的线性项 + 只依赖 <span displaypfx="inline-" class="mathjax-container">\(h_j\)</span> 的线性项”。它的作用是补充单边信息：即使暂时不看两者之间的乘性交互，某些 token 或实体本身也可能更像 head、更像 dependent，或更像某类关系的一端。最后的 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是全局偏置，给整类配对提供一个基线分数。</p>
<p>具象地看， <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span> 像在问“这两个人之间是否搭得上”， <span displaypfx="inline-" class="mathjax-container">\(w^\top [h_i;h_j]\)</span> 像在问“这两个人各自单独看，是否本来就带某种角色倾向”。把两部分合起来，模型既能利用配对关系，也不会丢掉单个对象自身的类型线索。这正是双仿射通常比纯双线性更稳、更有表达力的原因。</p>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(s(i,j)\)</span> 对所有 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 组合都算出来，就能形成一个 <span displaypfx="inline-" class="mathjax-container">\(L\times L\)</span> 的分数矩阵；对每个位置 <span displaypfx="inline-" class="mathjax-container">\(i\)</span>，再在所有候选 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 上做 <span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 或 softmax 分类，就能得到“它最可能依附到谁”或“它与谁最可能存在某种关系”。这种头的关键点在于：任务监督信号作用在“对”的层面，而不是单点分类。</p>
<div class="blog_h3"><span class="graybg">解析式 NLP（Analytical NLP）任务选型</span></div>
<p>解析式 NLP（Analytical NLP）指一类“结构化输出”的语言任务：输出不是一段自由文本，而是标签序列、树或图。这类任务的工程难点通常不在 backbone，而在<span style="background-color: #c0c0c0;">输出结构约束、标注成本、以及错误后果</span>（例如信息抽取用于合规/风控）。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">任务</td>
<td style="text-align: center;">输出结构</td>
<td style="text-align: center;">常见任务头</td>
<td style="text-align: center;">数据/标注成本</td>
<td style="text-align: center;">蒸馏难度</td>
</tr>
</thead>
<tbody>
<tr>
<td>NER</td>
<td>BIO 标签序列</td>
<td>Token 分类；CRF（可选）</td>
<td>中（需要一致的标注规范）</td>
<td>低~中（教师模型能稳定给出 token-level 或 span-level 标签）</td>
</tr>
<tr>
<td>DEP（Dependency Parsing）</td>
<td>树（每 token 一个 head）</td>
<td>Biaffine / 图解析器（Parser）</td>
<td>高（标注复杂；语言差异大）</td>
<td>中~高（结构约束更强；蒸馏需覆盖长尾句式）</td>
</tr>
<tr>
<td>SDP（Semantic Dependency Parsing）</td>
<td>有向图（多 head / 多边）</td>
<td>图结构头（Graph Head）</td>
<td>很高（语义标注成本高）</td>
<td>高（输出空间更大；错误更难用局部规则修正）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">多任务与多头</span></div>
<p>同一个 backbone 可以挂多个任务头（Multi-head / Multi-task）：共享表示学习带来数据效率，但不同任务的梯度可能冲突，需要权衡损失权重（Loss Weighting）与采样策略。工程上也常见“同任务多头”：例如同时输出 token 标签与 span 边界，或同时输出检索 embedding 与分类 logits，用不同头对齐不同指标。</p>
<div class="blog_h2"><span class="graybg">反向传播</span></div>
<p>反向传播（Backpropagation）描述的是训练阶段中“如何把损失函数对输出的误差信号传回网络内部，并进一步求出各层参数梯度”的过程。理解这一章时，可以把它拆成三件事：前向传播先把值算出来，反向传播再把梯度传回去，而链式法则与 Jacobian 则给出这件事的数学形式。</p>
<div class="blog_h3"><span class="graybg">前向传播</span></div>
<p>前向传播（Forward Propagation）是训练与推理中首先发生的计算过程：给定输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 和当前参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，按照网络定义从前到后依次计算各层输出、最终预测 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span>，以及训练时对应的损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span>。若把网络写成函数复合</p>
<span displaypfx="" class="mathjax-container">\[h_1=f_0(x),\quad h_2=f_1(h_1),\quad \cdots,\quad h_L=f_{L-1}(h_{L-1}),\quad \hat y=g(h_L),\quad L=\ell(\hat y,y)\]</span>
<p>那么前向传播做的就是按 <span displaypfx="inline-" class="mathjax-container">\(x\to h_1\to h_2\to \cdots \to h_L\to \hat y \to L\)</span> 的顺序把这些值算出来。它回答的问题是：<span style="background-color: #c0c0c0;">在当前参数下，这个样本会被模型算成什么结果，以及这个结果与真实标签相差多大</span>。</p>
<p>前向传播的产物不仅是最终预测，还包括中间激活值（Activation）。这些中间量一方面构成模型当前这次推理的内部表示，另一方面也是后续反向传播计算梯度时必须依赖的节点。因此，训练过程总是先有前向传播，再有反向传播：前者负责算值，后者负责算这些值对参数的敏感度。</p>
<div class="blog_h3"><span class="graybg">反向传播</span></div>
<p>反向传播（Backpropagation）是深度学习里用于<span style="background-color: #c0c0c0;">高效计算梯度（Gradient）</span>的算法。训练神经网络的目标，是最小化损失函数（Loss Function）<span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span>；而要用梯度下降（Gradient Descent）或 Adam 之类的优化器更新参数，就必须先知道每个参数对损失的影响，也就是 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial \theta}\)</span>。</p>
<p>这个需求在深层网络里会立刻变得庞大。一个模型往往包含从数万到数十亿个参数；如果对每个参数都单独做一次数值扰动，逐个估计“它变一点时损失会怎么变”，计算代价会高得无法训练。反向传播出现的直接原因，就是要把这个原本几乎不可做的问题，变成一次前向传播（Forward Pass）加一次反向传播（Backward Pass）即可完成的梯度计算流程。</p>
<p>它的来源并不神秘：神经网络本质上是许多简单函数的复合。线性层、激活函数、归一化、注意力、损失函数，都会把前一层输出当作后一层输入。只要这些局部变换可导（Differentiable）或几乎处处可导，就可以对整条复合链应用链式法则（Chain Rule）。反向传播就是把链式法则改写成一种适合计算图（Computational Graph）执行的程序：<span style="background-color: #c0c0c0;">前向时保存必要的中间结果，反向时从最终损失出发，把局部导数一层层乘回去，并把梯度分发给每个中间变量和参数</span>。</p>
<p>因此，反向传播不是另一条独立于微积分的“神经网络专用规则”，而是链式法则在复合函数上的工程化实现。它做的事情可以概括为两步：第一步，前向传播先算出各层激活值与最终损失；第二步，从 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial L}=1\)</span> 出发，把损失敏感度沿着依赖边反向传回去，依次得到 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_l}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial W_l}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial b_l}\)</span> 等量。到这一步为止，反向传播完成的是<span style="background-color: #c0c0c0;">梯度计算</span>；参数真正如何更新，则属于优化器（Optimizer）的职责。</p>
<p>最简单的梯度下降（Gradient Descent）更新写成</p>
<span displaypfx="" class="mathjax-container">\[\theta \leftarrow \theta - \eta \nabla_{\theta} L\]</span>
<p>这里更适合写梯度符号 <span displaypfx="inline-" class="mathjax-container">\(\nabla_{\theta}L\)</span>，因为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 往往是整个参数向量或参数集合，而不是单个标量。若只讨论某一个标量参数，才可写成 <span displaypfx="inline-" class="mathjax-container">\(\theta \leftarrow \theta - \eta \frac{\partial L}{\partial \theta}\)</span>。从计算方式看，反向传播对应自动微分（Automatic Differentiation）中的反向模式（Reverse Mode AD）。它特别适合“输入参数很多、输出损失很少”的场景；而神经网络训练恰好就是这种结构：参数维度极大，但最终通常只关心一个标量损失。因此，反向传播成为现代深度学习训练的标准机制。</p>
<div class="blog_h4"><span class="graybg">一个最小手算例子</span></div>
<p>先看一个只多加一层激活函数（Activation）的最小网络。设输入 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>，参数为权重 <span displaypfx="inline-" class="mathjax-container">\(w=1\)</span> 与偏置 <span displaypfx="inline-" class="mathjax-container">\(b=-1\)</span>，先做仿射变换（Affine Transform）</p>
<span displaypfx="" class="mathjax-container">\[z=wx+b=1\cdot 2-1=1\]</span>
<p>再经过 ReLU 激活函数</p>
<span displaypfx="" class="mathjax-container">\[a=\mathrm{ReLU}(z)=\max(0,z)=1\]</span>
<p>把激活值当作最终预测，即 <span displaypfx="inline-" class="mathjax-container">\(\hat y=a\)</span>。若真实标签 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span>，损失取平方损失（Squared Loss）</p>
<span displaypfx="" class="mathjax-container">\[L=\frac12(\hat y-y)^2=\frac12(1-0)^2=0.5\]</span>
<p>前向传播链条现在是</p>
<span displaypfx="" class="mathjax-container">\[x\ \longrightarrow\ z\ \longrightarrow\ a=\hat y\ \longrightarrow\ L\]</span>
<p>反向传播先从损失对输出的导数开始：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial \hat y}=\hat y-y=1\]</span>
<p>由于这里 <span displaypfx="inline-" class="mathjax-container">\(\hat y=a\)</span>，所以</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial a}=1\]</span>
<p>再经过激活函数这一层。因为当前 <span displaypfx="inline-" class="mathjax-container">\(z=1&gt;0\)</span>，ReLU 的导数为</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial a}{\partial z}=\mathrm{ReLU}'(z)=1\]</span>
<p>于是梯度传回仿射层输出：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial z}=\frac{\partial L}{\partial a}\cdot\frac{\partial a}{\partial z}=1\cdot 1=1\]</span>
<p>最后再看仿射层对参数的局部导数：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial z}{\partial w}=x=2,\qquad \frac{\partial z}{\partial b}=1\]</span>
<p>因此</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w}=\frac{\partial L}{\partial z}\cdot\frac{\partial z}{\partial w}=1\cdot 2=2\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial b}=\frac{\partial L}{\partial z}\cdot\frac{\partial z}{\partial b}=1\cdot 1=1\]</span>
<p>若学习率 <span displaypfx="inline-" class="mathjax-container">\(\eta=0.1\)</span>，梯度下降更新后</p>
<span displaypfx="" class="mathjax-container">\[w\leftarrow 1-0.1\cdot 2=0.8,\qquad b\leftarrow -1-0.1\cdot 1=-1.1\]</span>
<p>这个版本比纯线性例子多了一站 <span displaypfx="inline-" class="mathjax-container">\(z\to a\)</span>，因此链式法则也多乘了一个局部导数 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial a}{\partial z}\)</span>。这正是反向传播的一般形式：<span style="background-color: #c0c0c0;">误差信号每穿过一层，就乘上这一层自己的局部导数</span>。若这里的 <span displaypfx="inline-" class="mathjax-container">\(z&lt;0\)</span>，则 ReLU 导数为 0，上游梯度也会在这一层被截断。</p>
<div class="blog_h3"><span class="graybg">链式法则与雅可比连乘</span></div>
<p>反向传播（Backpropagation）本质是链式法则（Chain Rule）在计算图（Computational Graph）上的系统化应用：把最终损失对中间变量的导数沿依赖关系向后传。</p>
<p>固定一个训练样本 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 后，损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 最终当然可以看成参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的函数；训练时真正要更新的也确实是权重（Weight）。但在计算图里， <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 并不是一步直接从 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 跳到损失，而是先经过各层线性变换、激活值（Activation）和输出再到损失。因此，反向传播会先求 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_l}\)</span> 这类“损失对中间激活值的敏感度”，再由这些中间梯度继续求出 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial \theta_l}\)</span>。</p>
<p>Jacobian 一节已经给出局部线性化：若 <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}=f_l(h_l)\)</span>，则在当前点附近有 <span displaypfx="inline-" class="mathjax-container">\(dh_{l+1}\approx J_l\,dh_l\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(J_l=\frac{\partial h_{l+1}}{\partial h_l}\)</span>。这条式子描述的是<span style="background-color: #c0c0c0;">输入扰动如何向前传到输出扰动</span>；反向传播关心的是相反方向的问题：输出端的损失敏感度如何传回输入端。</p>
<p>先看一层。由于损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 是标量，若采用与前文 Jacobian 一致的分子布局（Numerator Layout），把 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_l}\)</span> 记成 <span displaypfx="inline-" class="mathjax-container">\(1\;\times d_l\)</span> 的行向量，则向量链式法则写成：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_l}=\frac{\partial L}{\partial h_{l+1}}\frac{\partial h_{l+1}}{\partial h_l}=\frac{\partial L}{\partial h_{l+1}}J_l\]</span>
<p>这条式子最好按符号逐个读。先看 <span displaypfx="inline-" class="mathjax-container">\(h_l\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}\)</span>：它们分别表示第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层的输入表示和该层输出表示。若 <span displaypfx="inline-" class="mathjax-container">\(h_l\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(d_l\)</span> 个分量， <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(d_{l+1}\)</span> 个分量，那么</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_{l+1}}=\begin{bmatrix}\frac{\partial L}{\partial h_{l+1,1}} &amp; \frac{\partial L}{\partial h_{l+1,2}} &amp; \cdots &amp; \frac{\partial L}{\partial h_{l+1,d_{l+1}}}\end{bmatrix}\in\mathbb{R}^{1\;\times d_{l+1}}\]</span>
<p>它表示：损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 对第 <span displaypfx="inline-" class="mathjax-container">\(l+1\)</span> 层每个输出分量分别有多敏感。这里把它写成行向量，是因为前文采用的是分子布局（Numerator Layout）。</p>
<p>再看 Jacobian：</p>
<span displaypfx="" class="mathjax-container">\[J_l=\frac{\partial h_{l+1}}{\partial h_l}\in\mathbb{R}^{d_{l+1}\;\times d_l}\]</span>
<p>其中第 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 个元素是</p>
<span displaypfx="" class="mathjax-container">\[(J_l)_{ij}=\frac{\partial h_{l+1,i}}{\partial h_{l,j}}\]</span>
<p>意思是：第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个输入分量变化一点，会让第 <span displaypfx="inline-" class="mathjax-container">\(l+1\)</span> 层第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个输出分量变化多少。因此，Jacobian 记录的是“输入各分量 <span displaypfx="inline-" class="mathjax-container">\(\to\)</span> 输出各分量”的局部影响表。</p>
<p>现在看乘法</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_l}=\frac{\partial L}{\partial h_{l+1}}J_l\]</span>
<p>左边最终应该是一个关于 <span displaypfx="inline-" class="mathjax-container">\(h_l\)</span> 各分量的梯度，所以它必须有 <span displaypfx="inline-" class="mathjax-container">\(d_l\)</span> 个分量。维度上， <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_{l+1}}\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(1\;\times d_{l+1}\)</span>， <span displaypfx="inline-" class="mathjax-container">\(J_l\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(d_{l+1}\;\times d_l\)</span>，两者相乘后确实得到 <span displaypfx="inline-" class="mathjax-container">\(1\;\times d_l\)</span>。</p>
<p>如果把第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个分量单独展开，这个乘法其实就是</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_{l,j}}=\sum_{i=1}^{d_{l+1}}\frac{\partial L}{\partial h_{l+1,i}}\frac{\partial h_{l+1,i}}{\partial h_{l,j}}\]</span>
<p>这时含义就完全显出来了：第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个输入分量，会通过第 <span displaypfx="inline-" class="mathjax-container">\(l+1\)</span> 层的所有输出分量共同影响损失；因此要把“损失对每个输出分量的敏感度”乘上“该输出分量对当前输入分量的局部导数”，再对所有输出分量求和。矩阵乘法只是把这组求和一次性写成了紧凑形式。</p>
<p>把这条一层公式沿网络递推展开，对深度网络 <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}=f_l(h_l)\)</span> 可得：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_0}=\frac{\partial L}{\partial h_L}J_{L-1}J_{L-2}\cdots J_0,\qquad J_l=\frac{\partial h_{l+1}}{\partial h_l}\]</span>
<p>这条公式的读法非常具体：前向传播按 <span displaypfx="inline-" class="mathjax-container">\(h_0\to h_1\to\cdots\to h_L\)</span> 计算；反向传播则从最终梯度 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_L}\)</span> 出发，依次乘上最后一层到第一层的 Jacobian，把敏感度一层层传回去。矩阵乘法满足结合律（Associativity），因此可以逐层累积；但不满足交换律，乘法顺序不能改，因为每个 Jacobian 都对应不同层的局部坐标变换。</p>
<p>一个最小可算例可以把这件事完全写开。设输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是标量，第一层有两个参数 <span displaypfx="inline-" class="mathjax-container">\(w_{11},w_{12}\)</span>，输出二维隐藏表示：</p>
<span displaypfx="" class="mathjax-container">\[h_1=\begin{bmatrix}h_{1,1}\\h_{1,2}\end{bmatrix}=\begin{bmatrix}w_{11}x\\w_{12}x\end{bmatrix}\]</span>
<p>第二层有两个参数 <span displaypfx="inline-" class="mathjax-container">\(w_{21},w_{22}\)</span>，把二维隐藏表示读成一个标量预测：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=w_{21}h_{1,1}+w_{22}h_{1,2}\]</span>
<p>损失取最简单的平方损失（Squared Loss）：</p>
<span displaypfx="" class="mathjax-container">\[L=\frac12(\hat y-y)^2\]</span>
<p>这里总参数一共四个，但反向传播不会直接去“猜” <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial w_{11}}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial w_{12}}\)</span> 等结果，而是先沿着计算图写出中间量：</p>
<span displaypfx="" class="mathjax-container">\[x\ \longrightarrow\ h_1\ \longrightarrow\ \hat y\ \longrightarrow\ L\]</span>
<p>先看第二层的局部 Jacobian。因为 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span> 是标量、 <span displaypfx="inline-" class="mathjax-container">\(h_1\in\mathbb{R}^2\)</span>，所以</p>
<span displaypfx="" class="mathjax-container">\[J_1=\frac{\partial \hat y}{\partial h_1}=\begin{bmatrix}w_{21}&amp;w_{22}\end{bmatrix}\]</span>
<p>再看第一层的局部 Jacobian。因为 <span displaypfx="inline-" class="mathjax-container">\(h_1\in\mathbb{R}^2\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是标量，所以</p>
<span displaypfx="" class="mathjax-container">\[J_0=\frac{\partial h_1}{\partial x}=\begin{bmatrix}w_{11}\\w_{12}\end{bmatrix}\]</span>
<p>损失对最终输出的导数最容易先算：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial \hat y}=\hat y-y\]</span>
<p>于是，损失对隐藏表示的梯度由链式法则得到：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_1}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial h_1}=(\hat y-y)\begin{bmatrix}w_{21}&amp;w_{22}\end{bmatrix}\]</span>
<p>这一步已经把“损失对输出的敏感度”传回到了中间激活值 <span displaypfx="inline-" class="mathjax-container">\(h_1\)</span>。继续往回乘第一层 Jacobian，就得到损失对输入的梯度：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial x}=\frac{\partial L}{\partial \hat y}J_1J_0\]</span>
<span displaypfx="" class="mathjax-container">\[=(\hat y-y)\begin{bmatrix}w_{21}&amp;w_{22}\end{bmatrix}\begin{bmatrix}w_{11}\\w_{12}\end{bmatrix}\]</span>
<span displaypfx="" class="mathjax-container">\[=(\hat y-y)(w_{21}w_{11}+w_{22}w_{12})\]</span>
<p>对参数的梯度也是同样的思路。第二层参数直接作用在 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span> 上，因此</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{21}}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial w_{21}}=(\hat y-y)h_{1,1}\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{22}}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial w_{22}}=(\hat y-y)h_{1,2}\]</span>
<p>第一层参数不直接连到损失，而是先影响 <span displaypfx="inline-" class="mathjax-container">\(h_1\)</span>，再影响 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(L\)</span>，所以必须经过中间激活值这一站：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{11}}=\frac{\partial L}{\partial h_{1,1}}\frac{\partial h_{1,1}}{\partial w_{11}}=[(\hat y-y)w_{21}]\,x\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{12}}=\frac{\partial L}{\partial h_{1,2}}\frac{\partial h_{1,2}}{\partial w_{12}}=[(\hat y-y)w_{22}]\,x\]</span>
<p>若取一组具体数值 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{11}=1\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{12}=-1\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{21}=0.5\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{22}=2\)</span>，则前向传播先得到</p>
<span displaypfx="" class="mathjax-container">\[h_1=\begin{bmatrix}2\\-2\end{bmatrix},\qquad \hat y=0.5\cdot 2+2\cdot(-2)=-3,\qquad \frac{\partial L}{\partial \hat y}=\hat y-y=-4\]</span>
<p>于是反向传播依次得到</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_1}=-4\begin{bmatrix}0.5&amp;2\end{bmatrix}=\begin{bmatrix}-2&amp;-8\end{bmatrix}\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial x}=\begin{bmatrix}-2&amp;-8\end{bmatrix}\begin{bmatrix}1\\-1\end{bmatrix}=6\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{21}}=-4\cdot 2=-8,\qquad \frac{\partial L}{\partial w_{22}}=-4\cdot(-2)=8\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{11}}=(-2)\cdot 2=-4,\qquad \frac{\partial L}{\partial w_{12}}=(-8)\cdot 2=-16\]</span>
<p>这个最小例子把反向传播的结构完整展示了出来：损失函数最终是参数的函数，但梯度并不是“绕开中间层直接对权重求”。它必须先通过 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial \hat y}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_1}\)</span> 这样的中间敏感度逐步回传。激活值因此不是“额外引入的变量”，而是计算图中必经的节点。</p>
<p>若改用深度学习里更常见的列向量梯度记号 <span displaypfx="inline-" class="mathjax-container">\(\nabla_{h_l}L\in\mathbb{R}^{d_l}\)</span>，同一件事会写成</p>
<span displaypfx="" class="mathjax-container">\[\nabla_{h_l}L=J_l^\top \nabla_{h_{l+1}}L\]</span>
<p>这与上面的公式没有本质区别，只是把“左乘 Jacobian 的行向量记号”改写成了“右侧乘 Jacobian 转置的列向量记号”。工程实现里常见的 Vector-Jacobian Product，本质上就是这一步。</p>
<p>对参数的梯度也是同一个模式。若第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层含参数 <span displaypfx="inline-" class="mathjax-container">\(\theta_l\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial \theta_l}=\frac{\partial L}{\partial h_{l+1}}\frac{\partial h_{l+1}}{\partial \theta_l}\]</span>
<p>因此 backward 的核心不是“把前向再算一遍”，而是复用每一层的局部 Jacobian，把同一个上游梯度分别传给输入变量和参数。</p>
<div class="blog_h3"><span class="graybg">梯度消失与爆炸</span></div>
<p>梯度消失（Vanishing Gradients）与梯度爆炸（Exploding Gradients）通常来自连乘的数值尺度：若这些雅可比的有效增益（Effective Gain）的奇异值（Singular Values）长期小于 1，则梯度指数级衰减；长期大于 1 则指数级放大。</p>
<p>在 RNN 的 BPTT 中，这种现象尤其明显：同一递归矩阵在时间轴上重复相乘。粗略地看，若 <span displaypfx="inline-" class="mathjax-container">\(W_{hh}\)</span> 的谱半径（Spectral Radius）<span displaypfx="inline-" class="mathjax-container">\(\rho(W_{hh})\)</span> 明显大于 1，梯度更易爆炸；明显小于 1 更易消失。但这不是“只要 <span displaypfx="inline-" class="mathjax-container">\(\lambda&gt;1\)</span> 就一定爆炸”的二选一结论，因为实际还受到激活函数导数、归一化、残差/门控结构、以及数据分布的共同影响。</p>
<p>“梯度随训练慢慢变小是否正常？”在接近最优点时，梯度范数下降是预期现象；需要警惕的是训练早期就出现系统性消失（例如大量饱和激活导致导数接近 0）或出现不稳定爆炸（loss/梯度频繁变成 NaN/Inf）。</p>
<p>常见应对：</p>
<ul>
<li>梯度裁剪（Gradient Clipping）：抑制爆炸。</li>
<li>合理初始化（Xavier/He）、归一化（BatchNorm/LayerNorm）、残差连接（Residual）。</li>
<li>门控结构（Gated Units）：LSTM/GRU 通过门控缓解长程梯度问题。</li>
</ul>
<div class="blog_h2"><span class="graybg">权重初始化</span></div>
<p>权重初始化（Weight Initialization）的目标不是“随机给一个小数”，而是控制信号在层间传播的数值尺度。设第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层的线性部分为 <span displaypfx="inline-" class="mathjax-container">\(z^{(l)}=W^{(l)}h^{(l-1)}+b^{(l)}\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(z^{(l)}\)</span> 的二阶原点矩（Second Raw Moment）在层间持续放大，前向激活与反向梯度都会倾向爆炸；若持续衰减，则会出现梯度消失。</p>
<p>对单个神经元，写成 <span displaypfx="inline-" class="mathjax-container">\(z=\sum_{i=1}^{n}w_i x_i+b\)</span>。初始化分析里通常进一步假设 <span displaypfx="inline-" class="mathjax-container">\(w_i\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 相互独立，且权重满足 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[w_i]=0\)</span>。这里的 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[w_i]=0\)</span> 不是在声称“训练后的权重天然应当均值为 0”，而是在分析初始化时，把权重看作从一个以 0 为中心的随机分布中抽样得到；这样做的目的，是避免网络在一开始就对某一方向产生系统性偏置，并让前向输出的均值计算更干净。基于这一初始化假设，在线性部分有</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[z]=0,\quad \mathbb{E}[z^2]=n\,\mathrm{Var}(w)\,\mathbb{E}[x^2]\]</span>
<p>当输入也近似零均值时， <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[x^2]\approx \mathrm{Var}(x)\)</span>，于是常写成 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(z)\approx n\,\mathrm{Var}(w)\,\mathrm{Var}(x)\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 就是 fan_in。因此初始化的核心约束可以概括为：让 <span displaypfx="inline-" class="mathjax-container">\(n\,\mathrm{Var}(w)\)</span> 保持在 1 附近。</p>
<p>更严格地说，深度初始化分析常跟踪的是 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[h^2]\)</span>，而不是所有层都精确使用方差；因为经过 ReLU 之后，激活不再零均值，此时二阶原点矩与方差不再完全相同。但在线性层与零均值近似下，两者是一致的，因此“保持方差稳定”仍是准确的工程表述。</p>
<p>两种朴素做法都会失败：若所有权重都初始化为 0，则各神经元保持完全对称，反向传播得到相同更新，网络退化为“许多拷贝的同一个单元”；若直接令 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)=1\)</span>，则线性输出的尺度会随层数按 fan_in 连乘，深层网络极易数值失稳。偏置（Bias）通常初始化为 0 或很小的常数；真正决定尺度的是权重矩阵的方差结构。</p>
<p>这里的 fan_in 与 fan_out 可以直接按“这层连接了多少路信号”来理解。对某个线性层中的一个输出神经元，fan_in 是流入它的输入通道数；输入通道越多，很多随机小贡献叠加后，输出就越容易变大。对某个输入神经元，fan_out 是它连接到下一层多少个输出通道；fan_out 越大，这个输入方向上的梯度在反向传播时就会被分发到更多支路。因此，fan_in 主要约束前向激活的放大量级，fan_out 主要约束反向梯度的放大量级。</p>
<p>具象地看，一个神经元像一个汇流节点：fan_in 决定有多少根水管把信号同时灌进来，fan_out 决定这股信号会被分流到多少个下游节点。初始化如果不考虑这两个连接数，网络就会在“汇流过猛”与“分流过弱”之间失衡。Xavier 正是在前向与反向之间做折中，He 则进一步把激活函数本身带来的能量损失也纳入补偿。</p>
<div class="blog_h3"><span class="graybg">Xavier 初始化</span></div>
<p>Xavier 初始化（Xavier Initialization）也称 Glorot 初始化（Glorot Initialization），针对近似线性的激活工作区间设计，例如 tanh 与 sigmoid 在原点附近的局部线性区域。它的目标是同时控制前向激活与反向梯度的尺度，使它们在穿过每一层时不发生系统性放大或衰减。</p>
<p>若暂时忽略激活函数对二阶原点矩的额外缩放，则前向保持尺度不变要求 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)\approx 1/\mathrm{fan\_in}\)</span>；反向对应地要求 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)\approx 1/\mathrm{fan\_out}\)</span>。Glorot 给出的折中形式因此写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(w)=\frac{2}{\mathrm{fan\_in}+\mathrm{fan\_out}}\]</span>
<p>常见实现包括正态版 <span displaypfx="inline-" class="mathjax-container">\(w\sim\mathcal{N}\!\left(0,\frac{2}{\mathrm{fan\_in}+\mathrm{fan\_out}}\right)\)</span>，以及均匀版 <span displaypfx="inline-" class="mathjax-container">\(w\sim U\!\left[-\sqrt{\frac{6}{\mathrm{fan\_in}+\mathrm{fan\_out}}},\sqrt{\frac{6}{\mathrm{fan\_in}+\mathrm{fan\_out}}}\right]\)</span>。</p>
<p>从直觉上看，Xavier 初始化像是在给每一层设置一个“不过度放大、也不过度压缩”的中性增益（neutral gain）。可以把一层线性变换想成一组并联的混音器：输入信号从上一层流入，经过许多权重通道混合后送到下一层。Xavier 的目标就是让这组混音器在初始状态下近似保持“总音量”不变，使信号既不会层层变得越来越吵，也不会层层变得越来越弱。</p>
<p>Xavier 的局限在于：它默认激活函数不会系统性丢失太多能量。对 ReLU 这类半波整流（Half-wave Rectification）激活，这个假设不再成立；即使线性层前后的尺度匹配，经过激活后信号的二阶原点矩仍会明显下降。</p>
<div class="blog_h3"><span class="graybg">He 初始化</span></div>
<p>He 初始化（He Initialization）也称 Kaiming 初始化（Kaiming Initialization），专门处理 ReLU 家族的整流效应。若线性输出 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 近似关于 0 对称，经过 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{ReLU}(z)=\max(0,z)\)</span> 后，大约一半的质量被截断为 0，并且</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[\mathrm{ReLU}(z)^2]=\frac{1}{2}\mathbb{E}[z^2]\]</span>
<p>因此，若仍使用 Xavier 量级，信号的二阶原点矩会随层数持续衰减。He 初始化通过把权重方差提高到</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(w)=\frac{2}{\mathrm{fan\_in}}\]</span>
<p>来补偿这一半波损失。对应实现可写为正态版 <span displaypfx="inline-" class="mathjax-container">\(w\sim\mathcal{N}\!\left(0,\frac{2}{\mathrm{fan\_in}}\right)\)</span>，或均匀版 <span displaypfx="inline-" class="mathjax-container">\(w\sim U\!\left[-\sqrt{\frac{6}{\mathrm{fan\_in}}},\sqrt{\frac{6}{\mathrm{fan\_in}}}\right]\)</span>。</p>
<p>具象地看，ReLU 像一道只允许正值通过的闸门：一批近似对称分布的信号经过后，负半边会被直接截成 0，等于天然损失了一部分能量。He 初始化做的事情就是在闸门前把信号预先放大一些，使它通过这道“半波闸门”之后，整体尺度仍能维持在稳定区间。Xavier 假定通道基本不漏能量，He 则明确把 ReLU 的漏损补偿计入初始化方差。</p>
<p>对 Leaky ReLU/PReLU，补偿因子可推广为 <span displaypfx="inline-" class="mathjax-container">\(\frac{2}{(1+a^2)\,\mathrm{fan\_in}}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 是负半轴斜率。实践上，ReLU 及其变体默认优先使用 He 初始化；tanh/sigmoid 更适合 Xavier。归一化层与残差连接可以进一步放宽初始化的容错区间，但不能替代合理的初始尺度控制。</p>
<div class="blog_h3"><span class="graybg">大语言模型里的初始化</span></div>
<p>Transformer / 大语言模型（Large Language Model, LLM）里的初始化通常不按“整模统一套 Xavier”或“整模统一套 He”来理解，而更像一套围绕<span style="background-color: #c0c0c0;">残差流（Residual Stream）稳定性、归一化层和深度扩展</span>设计的小方差初始化配方。原因在于：LLM 的主干不是简单的“线性层 + 单一激活函数”堆叠，而是由自注意力、残差连接、LayerNorm / RMSNorm、MLP / SwiGLU 等子结构共同组成；纯粹以某个激活函数为中心推导的 Xavier / He，只能覆盖其中一部分局部直觉。</p>
<p>工程上最常见的做法是：嵌入层与线性层权重用零均值的小方差正态分布初始化，偏置置零或省略；随后依靠 LayerNorm / RMSNorm 与残差结构维持训练初期的数值稳定。BERT、GPT-2 一类经典 Transformer 常见做法是使用标准差约为 <span displaypfx="inline-" class="mathjax-container">\(0.02\)</span> 的正态或截断正态初始化；很多更现代的 Decoder-only LLM 仍延续“小方差高斯初始化”这一主线，只是在具体投影层上再叠加按隐藏维度、层数或残差分支做缩放的配方。</p>
<p>从直觉上看，LLM 初始化更像是在给一条很深的多车道主干道设置初始车流密度。若注意力里的 <span displaypfx="inline-" class="mathjax-container">\(Q/K/V/O\)</span> 投影、FFN 的升维/降维投影以及其他会把结果写回残差流的线性层初始尺度过大，残差分支会把信号越叠越猛，训练初期容易震荡；若尺度过小，几十层上百层残差块叠起来后，真正进入有效学习区的信号又会太弱。因此，现代 LLM 初始化的重点往往不是“严格选 Xavier 还是 He”，而是<span style="background-color: #c0c0c0;">让残差支路在深层网络中既能传递信息，又不会在训练一开始就把数值尺度推离稳定区</span>。</p>
<p>具体到模块分工，也可以这样理解：MLP 内部若使用 ReLU 家族激活，He 的补偿思想仍然成立；若使用 GELU、SiLU、SwiGLU 这类更平滑或带门控的激活，则实现里往往直接采用统一的小方差正态初始化，再由归一化、残差和训练配方共同保证稳定性。换言之，LLM 并没有抛弃 Xavier / He 背后的“方差守恒”原则，而是把这套原则嵌入到了更完整的 Transformer 初始化策略里。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">方法</td>
<td style="text-align: center;">核心方差</td>
<td style="text-align: center;">适用激活</td>
<td style="text-align: center;">主要问题 / 逻辑</td>
</tr>
</thead>
<tbody>
<tr>
<td>零初始化</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)=0\)</span></td>
<td>无</td>
<td>破坏对称性；所有神经元学到相同特征</td>
</tr>
<tr>
<td>标准正态随机</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)=1\)</span></td>
<td>无</td>
<td>尺度随深度快速放大；易导致爆炸</td>
</tr>
<tr>
<td>Xavier / Glorot</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{2}{\mathrm{fan\_in}+\mathrm{fan\_out}}\)</span></td>
<td>Tanh、Sigmoid</td>
<td>兼顾前向与反向尺度；假设激活近似线性</td>
</tr>
<tr>
<td>He / Kaiming</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{2}{\mathrm{fan\_in}}\)</span></td>
<td>ReLU、Leaky ReLU</td>
<td>补偿 ReLU 的半波截断；保持二阶原点矩稳定</td>
</tr>
<tr>
<td>LLM 常用小方差高斯初始化</td>
<td>常取固定小标准差，或再叠加按宽度 / 深度缩放</td>
<td>Transformer 模块整体</td>
<td>服务残差流、归一化层与深层稳定训练；不是单纯按某一种激活推导</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">正则化</span></div>
<p>正则化（Regularization）在经验风险（Empirical Risk）上增加复杂度惩罚，缓解过拟合（Overfitting）。假设函数/模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 决定预测；样本损失 <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span> 度量单样本误差；代价函数/成本函数 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 汇总训练集误差；目标函数 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)\)</span> 则是 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 加上正则化项： <span displaypfx="inline-" class="mathjax-container">\(J(\theta)=L(\theta)+\lambda\Omega(\theta)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=\frac{1}{m}\sum_{i=1}^{m}\ell\!\left(f_{\theta}(x^{(i)}),y^{(i)}\right)+\lambda\Omega(\theta)\]</span>
<div class="blog_h3"><span class="graybg">L1 正则化（Lasso）</span></div>
<p>L1 正则化（L1 Regularization）使用 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)=\|\theta\|_1\)</span>。它倾向产生稀疏解（Sparsity），即一部分权重被直接压到 0，因此可同时做参数学习和特征选择（Feature Selection）。</p>
<p>从优化角度看，L1 的梯度是次梯度（Subgradient）：对单个参数 <span displaypfx="inline-" class="mathjax-container">\(\theta_k\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(\theta_k\ne 0\)</span> 时有 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial}{\partial \theta_k}\|\theta\|_1=\mathrm{sign}(\theta_k)\)</span>；在 0 点是一段区间 <span displaypfx="inline-" class="mathjax-container">\([-1,1]\)</span>。这使得优化过程更容易把小权重“推过 0”，形成精确稀疏。</p>
<p>在很多实现中，会用近端算子（Proximal Operator）给出更清晰的“压到 0”机制：对标量 <span displaypfx="inline-" class="mathjax-container">\(w\)</span>，软阈值化（Soft-Thresholding）是</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{soft}(w,\alpha)=\mathrm{sign}(w)\max(|w|-\alpha,0)\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(|w|\le \alpha\)</span> 时直接变为 0。</p>
<div class="blog_h3"><span class="graybg">L2 正则化（Ridge / Weight Decay）</span></div>
<p>L2 正则化（L2 Regularization）使用 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)=\|\theta\|_2^2\)</span>。它倾向均匀缩小参数，但通常不会把参数精确压到 0。直观上，L1 在 0 点不可导，更容易触发“阈值化”解；L2 是光滑二次惩罚，更新更连续。</p>
<p>梯度层面，L2 会在原梯度上叠加一个“拉回原点”的项：若目标为 <span displaypfx="inline-" class="mathjax-container">\(\ell(\theta)+\lambda\|\theta\|_2^2\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta=\nabla \ell(\theta)+2\lambda\theta\)</span>。工程上常把这个效果称为权重衰减（Weight Decay）：每一步都把权重按比例缩小。</p>
<p>需要区分一个常见细节：在自适应优化器（如 Adam）里，“把 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\theta\|_2^2\)</span> 加到损失里”与“直接做 weight decay”在数值上并不完全等价；因此实践中常用解耦权重衰减（Decoupled Weight Decay，典型实现是 AdamW）来获得更可控的正则化行为。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/regularization-l1-l2-geometry.png"><img class="alignnone size-full" src="https://blog.gmem.cc/wp-content/uploads/2026/03/regularization-l1-l2-geometry.png" alt="regularization-l1-l2-geometry" width="1920" height="930" /></a></p>
<p>把带惩罚的目标改写成约束形式后，几何差异会非常直观： <span displaypfx="inline-" class="mathjax-container">\(\min_\theta L(\theta)\ \mathrm{s.t.}\ \|\theta\|_1\le t\)</span> 的可行域在二维里是菱形， <span displaypfx="inline-" class="mathjax-container">\(\min_\theta L(\theta)\ \mathrm{s.t.}\ \|\theta\|_2\le t\)</span> 的可行域则是圆盘。损失等高线从外向内收缩时，第一次接触 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 边界，往往更容易落在角点；角点恰好对应某些坐标精确等于 0。 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 边界处处光滑，接触点通常只是把所有坐标一起缩小，而不是把其中一部分直接压成 0。</p>
<p>从一维更新机制看，这个差异也对应两种不同的收缩方式。 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 更像连续比例收缩：权重越大，拉回原点的力越强，但通常不会在有限步内变成精确 0。 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 则对应软阈值化（Soft-Thresholding）：一旦 <span displaypfx="inline-" class="mathjax-container">\(|w|\)</span> 小于阈值，就会被直接压成 0。因此， <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 不只是“把权重变小”，而是会主动把一部分坐标从模型中删掉，这就是稀疏性（Sparsity）的来源。</p>
<div class="blog_h3"><span class="graybg">Dropout</span></div>
<p>Dropout 通过随机屏蔽部分神经元输出，减少共适应（Co-adaptation），等价于在训练时对网络做一种随机子网络集成（Ensemble）。这里的共适应，是指若干神经元彼此形成了固定搭配：模型过度依赖它们同时出现、按特定组合共同完成判断，而不是让每个单元都学到相对独立、稳健的特征。Dropout 随机拿掉其中一部分单元后，网络不能再把能力押注在某一组固定配合上，只能把有用模式分散到更稳健的表示里。令隐藏向量为 <span displaypfx="inline-" class="mathjax-container">\(h\)</span>，mask <span displaypfx="inline-" class="mathjax-container">\(m_i\sim \mathrm{Bernoulli}(p)\)</span>，常见的 inverted dropout 写法为：</p>
<span displaypfx="" class="mathjax-container">\[\tilde h = \frac{m\odot h}{p}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是保留概率（Keep Probability）。这样推理时可直接使用原网络（不再采样 mask），避免额外缩放。</p>
<div class="blog_h3"><span class="graybg">Batch Normalization</span></div>
<p>Batch Normalization（BatchNorm）在训练时用 mini-batch 的均值/方差做归一化，并学习缩放/平移参数。对特征维上的某个分量 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，典型形式是：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{BN}(x)=\gamma\cdot \frac{x-\mu_{\text{batch}}}{\sqrt{\sigma_{\text{batch}}^2+\epsilon}}+\beta\]</span>
<p>推理阶段通常使用训练过程累积的运行均值（Running Mean）与运行方差（Running Variance），因此训练/推理行为不同。BatchNorm 在 CNN 中极常见，因为卷积特征图在同一通道上的空间位置具有较强同质性，跨样本统计量较容易稳定；但在 Transformer 中，主流做法并不是沿 batch 维做归一化，而是使用与 batch 统计无关的 LayerNorm 或 RMSNorm。</p>
<p>原因在于 Transformer 的基本计算单元是 token 表示。序列长度常常可变，batch size 在训练与推理中也经常变化，尤其大模型训练会受到显存限制而使用较小、波动甚至分布式切分后的 micro-batch。若使用 BatchNorm，某个 token 的归一化结果会显式依赖同一 batch 中其他样本与其他位置的统计量，这会引入跨样本耦合，使训练和推理的数值语义不一致，也不利于自回归解码阶段逐 token 稳定生成。</p>
<div class="blog_h3"><span class="graybg">Layer Normalization</span></div>
<p>Layer Normalization（LayerNorm）对每个样本（或每个 token）的特征维做归一化，不依赖 batch 统计量，因此更适合变长序列与自回归推理。对向量 <span displaypfx="inline-" class="mathjax-container">\(x\in\mathbb{R}^{d}\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{LN}(x)=\gamma\odot \frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}+\beta,\quad \mu=\frac{1}{d}\sum_{i=1}^{d}x_i,\ \sigma^2=\frac{1}{d}\sum_{i=1}^{d}(x_i-\mu)^2\]</span>
<p>这里的均值 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 与方差 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 都只在当前样本、当前 token 的特征维内部计算，因此每个 token 都能独立完成归一化。这个性质与 Transformer 的残差流（Residual Stream）非常匹配：无论 batch 如何变化、序列如何裁剪、推理时是否一次只输入一个 token，归一化规则都保持一致。</p>
<p>当前主流的 Transformer 归一化实践可以概括为两类。Encoder-only 与很多 Vision Transformer（ViT）架构通常沿用 LayerNorm；Decoder-only 大语言模型（Large Language Model, LLM）则大量采用 Pre-Norm 残差块，并进一步把 LayerNorm 简化为 RMSNorm。前者保留去均值与方差缩放，后者只保留尺度归一化，计算更轻，也更适合超大规模训练。</p>
<div class="blog_h3"><span class="graybg">Early Stopping</span></div>
<p>Early Stopping 用验证集指标监控训练过程，在泛化性能不再提升时提前停止，从而避免在训练集上继续拟合噪声。工程上常用“耐心（Patience）”：若验证集指标连续 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 次评估都未改善，则停止训练并回滚到最佳 checkpoint。它属于训练流程级的隐式正则化（Implicit Regularization）。</p>
<div class="blog_h3"><span class="graybg">分类任务正则化</span></div>
<p>分类任务正则化（Regularization for Classification Tasks）直接作用在分类训练的监督方式、样本混合方式或概率分布形状上。它的主要目标包括：缓解过度自信、减轻类别不平衡造成的梯度偏置、降低标签噪声影响，以及让决策边界在训练样本之间保持更平滑的过渡。</p>
<div class="blog_h4"><span class="graybg">Label Smoothing</span></div>
<p>Label Smoothing（标签平滑）把 one-hot 标签从“真实类为 1、其余类为 0”改写成更平滑的目标分布。对 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 分类问题，常见写法是：</p>
<span displaypfx="" class="mathjax-container">\[y_i^{\mathrm{LS}}=(1-\varepsilon)\mathbf{1}[i=c]+\frac{\varepsilon}{C}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 是真实类别， <span displaypfx="inline-" class="mathjax-container">\(\varepsilon\in(0,1)\)</span> 是平滑系数。真实类别仍占最大权重，但其他类别也会分到一小部分概率质量。训练因此不再持续奖励模型把正确类别概率推到 1、把其余类别压到 0，输出分布通常会更平滑，概率校准（Calibration）也更稳定。</p>
<p>在固定阈值或直接取 <span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 的分类系统里，Label Smoothing 带来的收益往往更多体现在 loss 曲线与概率可信度上，而不是直接转化为同等幅度的 F1 提升。若模型输出概率还会进入阈值调优、排序、融合、拒识或风险控制，这种校准改进的价值会更明显。</p>
<div class="blog_h4"><span class="graybg">类别重加权与重采样</span></div>
<p>类别重加权（Class Reweighting）与重采样（Resampling）主要处理类别不平衡（Class Imbalance）。其核心思想是让少数类样本在训练中获得更大的有效权重，避免优化过程被大量易分类的多数类样本主导。加权交叉熵的常见形式为：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{wCE}}(y,p)=-\sum_{i=1}^{C}\alpha_i y_i\log p_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类的损失权重。若某类样本极少，可以给它更大的 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>，使模型在误分该类时承担更高代价。重采样则直接改变 mini-batch 的类别组成，例如过采样少数类、欠采样多数类，或用类别均衡采样器（Class-balanced Sampler）保证 batch 内标签分布更均衡。</p>
<p>这类方法的直接效果，是让决策边界不再默认偏向多数类。代价则是：过强的重加权会放大少数类中的噪声标签，过强的过采样会提高过拟合风险。因此它通常要与验证集上的 Precision / Recall / F1 一起调节，而不只盯着训练损失。</p>
<div class="blog_h4"><span class="graybg">Mixup 与 CutMix</span></div>
<p>Mixup 与 CutMix 通过构造“介于两个训练样本之间”的新样本，显式约束分类器在样本间插值区域上的行为，从而平滑决策边界。Mixup 的典型形式是：</p>
<span displaypfx="" class="mathjax-container">\[\tilde x=\lambda x_i+(1-\lambda)x_j,\qquad \tilde y=\lambda y_i+(1-\lambda)y_j\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\lambda\in[0,1]\)</span> 通常从 Beta 分布采样。输入和标签都被线性混合，于是模型被要求在 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(x_j\)</span> 之间给出相应的插值预测。CutMix 则不是对整张输入做加权平均，而是把一张图像中的局部区域替换为另一张图像的对应区域，同时按被替换面积比例混合标签。</p>
<p>这类方法对图像分类尤其有效，因为它们直接惩罚“决策边界贴着训练样本走”的过拟合行为。换一个视角看，Mixup / CutMix 并不是简单的数据增强，而是在告诉模型：输入空间里两点之间的过渡区域也应当保持语义上的平滑可解释。</p>
<div class="blog_h4"><span class="graybg">置信度惩罚与熵正则</span></div>
<p>置信度惩罚（Confidence Penalty）和熵正则（Entropy Regularization）直接约束输出分布不要过早塌缩到极端尖锐形状。一个常见做法是在交叉熵之外，再加入预测分布熵的奖励项：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}=\mathcal{L}_{\mathrm{CE}}-\beta H(p),\qquad H(p)=-\sum_{i=1}^{C}p_i\log p_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\beta&gt;0\)</span> 控制正则强度。由于熵越大表示分布越平滑，这一项会抑制模型过快形成极端概率。它和 Label Smoothing 的方向相近，但切入点不同：Label Smoothing 改的是监督目标分布，置信度惩罚改的是模型预测分布本身。</p>
<p>两者都常用于需要更好概率校准的分类系统。相比之下，Focal Loss 更强调“把梯度预算留给困难样本”，因此它在类别不平衡或难例挖掘场景更常见；Label Smoothing 与熵正则则更偏向控制过度自信与改善概率形状。</p>
<div class="blog_h3"><span class="graybg">回归任务正则化</span></div>
<p>回归任务正则化（Regularization for Regression Tasks）除了常见的参数惩罚外，还经常直接约束预测函数 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 的形状。回归目标是连续值，因此“曲线是否足够平滑、是否满足单调关系、是否具有合理曲率”往往和任务正确性本身直接相关。</p>
<div class="blog_h4"><span class="graybg">平滑性正则化与样条惩罚</span></div>
<p>平滑性正则化（Smoothness Regularization）要求回归函数不要在输入空间里出现不必要的高频震荡。最典型的形式是惩罚导数，尤其是二阶导数：</p>
<span displaypfx="" class="mathjax-container">\[J(f)=\sum_{i=1}^{N}(y_i-f(x_i))^2+\lambda\int (f''(x))^2\,dx\]</span>
<p>第二项就是经典的样条平滑惩罚（Smoothing Spline Penalty）。它惩罚曲率过大，相当于抑制函数频繁弯折。 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越大，拟合曲线越平滑； <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越小，模型越愿意追随样本中的局部波动。很多非参数回归（Nonparametric Regression）与时间序列平滑，本质上都在做这种“数据拟合 + 曲率惩罚”的权衡。</p>
<div class="blog_h4"><span class="graybg">Total Variation 与 Fused Lasso</span></div>
<p>Total Variation（TV）正则与 Fused Lasso 适合分段平滑（Piecewise Smooth）的回归目标。它们不强求函数处处光滑，而是允许少数突变点存在，同时惩罚过多的相邻跳变。离散形式的 TV 惩罚常写成：</p>
<span displaypfx="" class="mathjax-container">\[\Omega_{\mathrm{TV}}(f)=\sum_{t=2}^{T}|f_t-f_{t-1}|\]</span>
<p>若同时对参数本身加 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 惩罚，再对相邻参数差分加 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 惩罚，就得到 Fused Lasso：</p>
<span displaypfx="" class="mathjax-container">\[J(\beta)=L(\beta)+\lambda_1\sum_j |\beta_j|+\lambda_2\sum_{j=2}^{d}|\beta_j-\beta_{j-1}|\]</span>
<p>这类方法非常适合信号去噪、时序回归、基因拷贝数分段估计，以及任何“整体大致平稳、局部允许少数结构突变”的问题。与样条惩罚相比，它更偏好形成平坦区段，而不是全局光滑弯曲曲线。</p>
<div class="blog_h4"><span class="graybg">单调性约束</span></div>
<p>很多回归任务天然带有单调先验：广告出价上升，曝光概率通常不应系统性下降；贷款风险特征上升，违约风险不应系统性降低；药物剂量增加，效应在一定区间内通常应单调增强。单调性约束（Monotonicity Constraint）把这种领域知识直接写进模型：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial f(x)}{\partial x_k}\ge 0\quad \text{或}\quad \frac{\partial f(x)}{\partial x_k}\le 0\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x_k\)</span> 是某个具有明确方向含义的特征。在线性模型里，这等价于约束对应权重非负或非正；在树模型和神经网络里，则可以通过结构限制、投影步骤或软惩罚项来实现。单调约束的价值不只在提高泛化，还在于提升可解释性与业务一致性。</p>
<div class="blog_h4"><span class="graybg">凸性与曲率约束</span></div>
<p>当回归函数预期具有凸性（Convexity）、凹性（Concavity）或有限曲率时，可以继续对二阶导数施加方向性约束。例如一维凸函数满足：</p>
<span displaypfx="" class="mathjax-container">\[f''(x)\ge 0\]</span>
<p>这类约束在成本函数建模、供需曲线估计、剂量反应建模和某些经济学回归问题里非常常见。即使不要求严格凸性，也常通过曲率上界控制函数不要弯折过猛，例如惩罚 Hessian 范数或二阶差分幅度。它们的作用与平滑惩罚相近，但强调的是“弯曲方向和弯曲强度应满足领域结构”，而不只是单纯地压低高频波动。</p>
<div class="blog_h4"><span class="graybg">Lipschitz 与梯度约束</span></div>
<p>Lipschitz 约束（Lipschitz Constraint）控制的是输入微小变化会把输出放大多少。若存在常数 <span displaypfx="inline-" class="mathjax-container">\(K\)</span>，使得</p>
<span displaypfx="" class="mathjax-container">\[|f(x)-f(x')|\le K\|x-x'\|\]</span>
<p>则函数变化速度受到统一上界控制。对可导函数，一个常见做法是直接惩罚输入梯度范数：</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=L(\theta)+\lambda\,\mathbb{E}_{x}\|\nabla_x f_\theta(x)\|_2^2\]</span>
<p>这种正则化常用于需要鲁棒输出的回归系统，例如物理量估计、坐标回归和噪声敏感的传感器建模。它抑制模型对局部输入扰动过度敏感，也能缓解高维回归中出现的不稳定尖峰。</p>
<div class="blog_h4"><span class="graybg">概率回归的分布约束</span></div>
<p>概率回归（Probabilistic Regression）不仅预测均值，还会预测方差、分位数或整个条件分布。此时正则化对象不再只有均值函数，还包括分布参数之间的结构关系。例如异方差回归（Heteroscedastic Regression）中，方差参数必须保持正值；分位数回归（Quantile Regression）中，不同分位点曲线应尽量避免交叉（Quantile Crossing）。</p>
<p>若模型同时预测多个分位点 <span displaypfx="inline-" class="mathjax-container">\(\hat q_{\tau_1}(x),\hat q_{\tau_2}(x)\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\tau_1&lt;\tau_2\)</span>，则理想上应满足：</p>
<span displaypfx="" class="mathjax-container">\[\hat q_{\tau_1}(x)\le \hat q_{\tau_2}(x)\]</span>
<p>工程上常通过排序约束、投影修正或惩罚项来维持这种分布一致性。对高斯 NLL、混合密度网络（Mixture Density Network）或生存分析模型，也会对尺度参数、危险率函数或累积分布形状加入额外约束，使预测分布既拟合数据，又保持统计上可解释、数值上稳定。</p>
<div class="blog_h1"><span class="graybg">深度学习</span></div>
<p>深度学习（Deep Learning）是以多层神经网络为核心、通过大规模数据和梯度优化自动学习表示的建模范式。若上一章讨论的是神经网络的基本部件，例如线性层、激活函数、损失函数、反向传播、初始化与正则化；这一章讨论的则是这些部件在更深层、更大规模、更强归纳偏置下，如何组合成现代模型家族，并在视觉、语音、语言、生成和图结构任务上形成方法论分水岭。</p>
<p>“深度”并不只是层数更多。更关键的变化是：模型开始把原始输入逐层改写成越来越抽象的中间表示，从边缘、纹理、局部模式，逐步组合到部件、对象、语义关系与任务决策。于是，模型能力的来源不再只是最后那一层分类器，而是整条表示变换链本身。</p>
<p>下文的卷积神经网络（CNN）、循环神经网络（RNN）、生成模型（Generative Model）、图神经网络（GNN）和 ONNX，分别对应深度学习里几条非常重要的主线：空间局部归纳偏置、时序递推建模、生成式分布学习、关系数据表示学习，以及模型部署交换格式。它们共同构成了大模型出现之前深度学习的主要版图，也为 Transformer 与现代多模态模型奠定了方法学基础。</p>
<div class="blog_h2"><span class="graybg">表示学习与端到端学习</span></div>
<p>在传统机器学习流程里，特征工程和预测器常常是分开的：先由人手设计特征，再把这些特征交给线性模型、树模型或核方法完成分类与回归。深度学习把这两步合并进同一个可微计算图。前面的层负责把原始输入转写为更有判别力、更有结构感的表示，后面的层负责完成具体任务，所有参数围绕同一个目标函数联合优化。这就是端到端学习（End-to-End Learning）的核心含义。</p>
<p>表示学习（Representation Learning）之所以重要，是因为感知任务真正困难的部分，往往不在“最后怎么分一下类”，而在“怎样把原始信号变成对任务友好的坐标系”。图像像素、语音波形、文本序列和图结构都高度高维、局部相关且语义分散。深度网络的价值，在于它能用层级结构自动提取适合当前任务的中间表示，而不再完全依赖人工定义纹理统计量、边界特征、语言规则或图特征模板。</p>
<p>这也解释了为什么深度学习会改变整个方法栈。过去很多系统的主要工作量放在“特征怎么造”；深度学习之后，工作重心逐渐转向“架构如何设计、数据如何构造、训练如何稳定、预训练如何迁移、部署如何落地”。从研究到工业实践，核心竞争力开始沿着表示学习能力重新分布。</p>
<div class="blog_h2"><span class="graybg">为什么深度学习成为里程碑</span></div>
<p>多层神经网络并不是 2010 年代才出现的新概念。真正的转折点在于，一系列条件在同一时期同时成熟，使深网络第一次具备了大规模可训练、可迁移、可复用的现实基础。深度学习成为里程碑，靠的不是单一公式突破，而是数据、算力、优化、架构和软件工程几条线一起闭环。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">关键条件</td>
<td style="text-align: center;">作用</td>
<td style="text-align: center;">为什么重要</td>
</tr>
</thead>
<tbody>
<tr>
<td>大规模数据</td>
<td>提供足够多样的监督信号与统计规律</td>
<td>深网络参数量大，若没有足够样本，表达能力会迅速转化为过拟合风险</td>
</tr>
<tr>
<td>GPU / 并行算力</td>
<td>把大规模矩阵乘法、卷积和反向传播变成可承受的训练过程</td>
<td>很多深层模型在理论上可定义，但在工程上长期“训不动”</td>
</tr>
<tr>
<td>优化技术成熟</td>
<td>ReLU、Xavier / He 初始化、BatchNorm、残差连接、门控结构等共同提升可训练性</td>
<td>它们解决的是梯度消失、数值失稳和深层退化等根本瓶颈</td>
</tr>
<tr>
<td>强归纳偏置的架构</td>
<td>CNN 利用空间局部性，RNN 利用时序递推，GNN 利用图邻接关系</td>
<td>深度并不自动等于有效，架构必须贴合数据结构</td>
</tr>
<tr>
<td>预训练与迁移学习</td>
<td>先在大数据上学通用表示，再迁移到下游任务</td>
<td>这使深度学习从“每个任务从零训练”转向“共享表征资产”</td>
</tr>
<tr>
<td>框架与工程生态</td>
<td>自动求导、分布式训练、模型导出和部署工具日益成熟</td>
<td>研究原型和生产系统之间的距离被显著缩短</td>
</tr>
</tbody>
</table>
<p>从方法史角度看，深度学习真正改变的是“模型从何处获得有用表示”。经典机器学习更多依赖人工抽取特征，再用较浅模型做判别；深度学习则把表示构造本身纳入训练。这个变化一旦与数据和算力结合，就会呈现出非常强的规模效应。</p>
<div class="blog_h2"><span class="graybg">关键技术演进</span></div>
<div class="blog_h3"><span class="graybg">AlexNet 与视觉模型复兴</span></div>
<p>AlexNet 是现代深度学习史上的标志性节点。它的意义不只是 ImageNet 分类精度显著提升，而是证明了一个足够深、足够宽、用 GPU 训练、配合 ReLU 和数据增强的卷积网络，能够系统性压过人工特征加浅层分类器的旧路线。视觉领域由此从“设计特征”转向“训练特征”。</p>
<p>这次转折的后果极其深远。分类只是起点，检测、分割、检索、视频理解和视觉问答很快都转向以深层 backbone 为中心的范式。很多后续工作不再从零设计整套视觉特征，而是围绕预训练卷积网络做迁移、微调和多任务扩展。</p>
<div class="blog_h3"><span class="graybg">残差网络（ResNet）与可训练深度</span></div>
<p>ResNet 的历史意义在于，它解决了“网络更深但反而更难优化”的退化问题。残差连接（Residual Connection）为信息和梯度提供了一条更稳定的主通路，使模型不必在每一层都重写完整映射，而可以学习对当前表示的局部修正。深层网络由此从几十层推进到上百层乃至更复杂的堆叠结构。</p>
<p>残差思想后来超出了视觉本身。Transformer、扩散模型和大量现代深层架构都把残差连接当作标准部件，因为它本质上解决的是深网络训练稳定性，而不是某一个特定任务的技巧。残差网络因此不仅是一类 CNN 结构，更是现代深度学习的通用工程原则。</p>
<div class="blog_h3"><span class="graybg">Seq2Seq 与 Attention</span></div>
<p>在序列任务上，深度学习的关键进展并不只来自更强的 RNN / LSTM / GRU，还来自编码器-解码器（Encoder-Decoder）和注意力（Attention）机制。Seq2Seq 让模型能够把输入序列映射为输出序列，最早大规模改变了机器翻译、语音识别和摘要生成等任务；注意力则突破了“所有信息都必须挤进一个固定长度向量”的瓶颈，使解码器可以在生成每一步时，动态读取输入序列中最相关的部分。</p>
<p>这条技术线的重要性在于，它把序列建模从“单纯递推记忆”推进到“按需访问上下文”。Transformer 正是在这条线上进一步把注意力从辅助机制推到主干架构，因此 Seq2Seq 与 Attention 构成了通往下一篇 Transformer 主线的直接前史。</p>
<div class="blog_h3"><span class="graybg">GAN 与生成式建模跃迁</span></div>
<p>深度学习早期最强的成果主要集中在识别任务，而 GAN 把研究重点大幅推进到“高质量生成”本身。它展示了深网络不仅能判断图像属于什么类别，还能学习数据分布并生成逼真的新样本。这使图像合成、风格迁移、超分辨率、图像到图像翻译等方向迅速发展，也改变了人们对“模型能力边界”的直觉。</p>
<p>GAN 之后，生成式建模继续沿着 VAE、流模型、扩散模型等路线演进。它们关注的问题已经不只是判别准确率，而是样本质量、潜空间结构、可控生成和条件生成。生成模型因此成为深度学习内部一条独立而强势的方法线，后文会单独展开。</p>
<div class="blog_h3"><span class="graybg">预训练与迁移学习</span></div>
<p>深度学习的另一个方法学跃迁，是从“每个任务都从随机初始化开始学”转向“先学通用表示，再迁移到具体任务”。在视觉里，这条线最初体现为 ImageNet 预训练 backbone；在语音和语言里，则逐步发展为更大规模的自监督预训练。迁移学习显著降低了下游任务对标注数据量的依赖，也让模型参数本身成为可复用资产。</p>
<p>这一变化与大模型时代直接相连。大语言模型并不是凭空冒出来的新物种，而是深度学习在预训练、表示共享和规模扩展三条线上持续推进后的自然结果。因此，把深度学习理解成“大模型之前的旧阶段”并不准确；更准确的说法是，大模型建立在深度学习已经完成的方法论基础之上。</p>
<div class="blog_h2"><span class="graybg">深度学习的实际应用</span></div>
<p>深度学习之所以成为主流，不是因为它在论文基准上偶尔领先，而是因为它在大量真实任务上持续改写了系统能力边界。它最擅长的场景，通常具备三个特征：输入高维、原始信号结构复杂、手工特征难以穷尽。只要这三个条件同时出现，表示学习的优势就会迅速放大。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">应用方向</td>
<td style="text-align: center;">深度学习在做什么</td>
<td style="text-align: center;">典型模型或系统</td>
<td style="text-align: center;">关键价值</td>
</tr>
</thead>
<tbody>
<tr>
<td>图像分类、检测与分割</td>
<td>从像素中直接学习目标、边界和语义区域的层级表示</td>
<td>CNN backbone、Faster R-CNN、U-Net、Mask R-CNN</td>
<td>显著降低人工视觉特征设计需求，并统一多类视觉任务的表示基础</td>
</tr>
<tr>
<td>人脸识别与设备认证</td>
<td>学习稳定的人脸嵌入，用于身份匹配、聚类和检索</td>
<td>FaceNet、ArcFace、Face ID 一类终端系统</td>
<td>把“看起来像不像”变成可度量的特征空间距离，并兼顾鲁棒性与低误识率</td>
</tr>
<tr>
<td>语音识别与语音合成</td>
<td>把连续波形映射为音素、文本或声学表示，再进一步合成自然语音</td>
<td>Deep Speech、Conformer、Tacotron、WaveNet</td>
<td>显著提升端到端语音系统的准确率与自然度</td>
</tr>
<tr>
<td>机器翻译与序列理解</td>
<td>学习跨语言或跨序列位置的上下文依赖与对齐关系</td>
<td>Seq2Seq、Attention、Transformer</td>
<td>把规则驱动和短上下文模型推进到可扩展的端到端序列建模</td>
</tr>
<tr>
<td>医学影像与工业质检</td>
<td>从高维图像中识别微小异常、边界结构和组织模式</td>
<td>ResNet、U-Net、3D CNN</td>
<td>在噪声高、细节密、人工判读成本高的场景中放大模型辅助价值</td>
</tr>
<tr>
<td>推荐、排序与多模态检索</td>
<td>把用户、内容、上下文和行为序列编码进共享表示空间</td>
<td>Wide &amp; Deep、DeepFM、双塔检索模型</td>
<td>提升匹配能力，并支持召回、排序、粗排到精排的分层建模</td>
</tr>
</tbody>
</table>
<p>人脸识别本身毫无疑问属于深度学习的典型落地方向，而 Face ID 这类系统则更接近<span style="background-color: #c0c0c0;">“深度学习模型 + 传感器 + 活体检测 + 安全芯片 + 阈值策略”</span>的产品级集成。深度网络负责学习人脸表征与匹配规则，但真正可商用的身份认证系统，还必须处理深度信息、环境光变化、攻击对抗、设备端隐私隔离和误识率控制等工程问题。这类例子很能说明：模型往往是核心能力来源，但完整系统从来不只是一张网络结构图。</p>
<div class="blog_h2"><span class="graybg">卷积神经网络（CNN）</span></div>
<div class="blog_h3"><span class="graybg">卷积层与池化层</span></div>
<p>卷积（Convolution）在连续形式下定义为函数重叠积分：</p>
<span displaypfx="" class="mathjax-container">\[(f*g)(t)=\int_{-\infty}^{+\infty} f(\tau)\,g(t-\tau)\,d\tau\]</span>
<p>在离散二维图像中常写为：</p>
<span displaypfx="" class="mathjax-container">\[(I*K)(i,j)=\sum_m\sum_n I(i-m,j-n)\,K(m,n)\]</span>
<p>深度学习框架里多数“卷积层”实现的是互相关（Cross-Correlation）而非严格翻转核的数学卷积，但工程上沿用“卷积”命名。卷积核（Kernel）共享权重（Weight Sharing），天然利用局部性（Locality）与平移等变（Translation Equivariance）。</p>
<p>直观上，卷积层做的事情是：对每个位置取一个局部窗口，把窗口里的像素与卷积核做加权求和，得到该位置的响应。不同卷积核学习不同的局部模式（边缘、纹理、角点等）。</p>
<p>一维互相关示例：输入 <span displaypfx="inline-" class="mathjax-container">\(x=[0,0,1,1,1,0,0]\)</span>，核 <span displaypfx="inline-" class="mathjax-container">\(w=[1,0,-1]\)</span>，则在从左到右滑动时，核会对“上升沿/下降沿”给出大幅响应（本质上是比较左右两侧的差异）。这就是经典的边缘检测直觉在离散信号上的对应。</p>
<p>卷积的“系统视角”能统一理解 CNN 与信号处理：把卷积核看作响应函数（Response Kernel），输出就是对历史输入的加权累积。参见“微积分 ➡ 卷积（Convolution）”中的因果卷积与“打点滴累积药效”直觉例子。</p>
<div class="blog_h3"><span class="graybg">经典架构</span></div>
<div class="blog_h4"><span class="graybg">LeNet</span></div>
<p>LeNet 可以看作现代卷积神经网络（Convolutional Neural Network, CNN）的原型。它面对的是手写数字识别这类“局部笔画决定整体类别”的任务，因此核心思想很直接：先用卷积层提取局部边缘、角点和笔画，再用池化（Pooling）降低分辨率与局部扰动敏感性，最后把高层特征送入全连接层做分类。</p>
<p>经典 LeNet-5 的结构可以概括为“卷积 - 池化 - 卷积 - 池化 - 全连接”。前面的卷积层负责把原始像素变成越来越抽象的特征图（Feature Map），后面的全连接层负责把这些局部特征整合成类别判断。它的重要性不只是“识别数字有效”，更在于证明了三件事可以协同工作：局部感受野（Local Receptive Field）、权重共享（Weight Sharing）和层级特征提取（Hierarchical Feature Learning）。</p>
<p>卷积层里的一个典型计算是：</p>
<span displaypfx="" class="mathjax-container">\[y_{i,j}^{(k)}=\sum_{u}\sum_{v}\sum_{c} W_{u,v,c}^{(k)}\,x_{i+u,j+v,c}+b^{(k)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是输入图像或上一层特征图， <span displaypfx="inline-" class="mathjax-container">\(W^{(k)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个卷积核， <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 是输入通道索引， <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 是空间位置， <span displaypfx="inline-" class="mathjax-container">\(y_{i,j}^{(k)}\)</span> 是输出特征图在该位置上的响应。这个公式表达的就是：用同一个卷积核在整张图上滑动，寻找“哪里出现了我关心的局部模式”。</p>
<p>训练上，LeNet 已经体现出现代深度学习的基本闭环：前向传播得到类别 logits，损失函数衡量预测与真实标签的差距，反向传播把梯度传回卷积核和全连接层参数，再用梯度下降更新权重。它最典型的应用是 MNIST 手写数字识别，也常被用作理解 CNN 的第一块教学样板，因为结构短、计算图清晰、局部模式学习的直觉非常强。</p>
<div class="blog_h4"><span class="graybg">AlexNet</span></div>
<p>AlexNet 标志着深度卷积网络在大规模视觉识别上的突破。它面对的不是手写数字这种相对简单的灰度图，而是 ImageNet 级别的彩色自然图像，因此核心思想从“能提特征”进一步推进到“更深、更宽、更可训练”：增加网络容量，用 ReLU（Rectified Linear Unit）加快优化，用 Dropout 缓解过拟合，用数据增强提升泛化，再借助 GPU 让大模型真正训得动。</p>
<p>其典型结构是多层卷积堆叠后接全连接层，早期卷积核较大、步幅较大，用于迅速降采样并提取低层纹理；中后期卷积核变小，重点转向更细粒度的组合特征。AlexNet 还使用了局部响应归一化（Local Response Normalization, LRN）这一今天较少使用、但在当时有历史意义的设计。整体上，它把 CNN 从“可用于小型任务的模型”推向了“可以在大规模视觉基准上碾压传统方法的通用架构”。</p>
<p>它最有代表性的非线性是 ReLU：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ReLU}(z)=\max(0,z)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是线性层或卷积层的输出。与 sigmoid / tanh 相比，ReLU 在正半轴不饱和，梯度传播更直接，因此深层网络更容易优化。AlexNet 的历史意义之一，就是把“激活函数的选择会显著影响深网络可训练性”这件事变成了工程共识。</p>
<p>训练上，AlexNet 典型地结合随机裁剪、翻转、颜色扰动等数据增强，并在全连接层使用 Dropout。它的直接应用是大规模图像分类；更深远的影响则是推动了整个视觉领域转向“预训练 CNN + 迁移学习”的工作流。很多后续检测、分割与检索系统，都曾把 AlexNet 当作特征提取骨干网络（Backbone）。</p>
<div class="blog_h4"><span class="graybg">VGG</span></div>
<p>VGG 的核心思想是：用结构极其规整的小卷积核反复堆叠，把网络做深。它放弃了早期“大卷积核 + 大步幅”的粗放设计，转而几乎全程使用 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积，通过增加层数来扩大感受野、提高非线性表达能力，并让整套架构在工程上更统一、更易复用。</p>
<p>VGG 的典型结构非常整齐：若干个 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积层组成一个 stage，stage 之间通过池化层下采样，最后再接全连接分类头。它的重要工程思想是“深度本身就是能力来源之一”。相较于更杂糅的早期网络，VGG 的层次感非常强：浅层提边缘和纹理，中层提局部部件，高层提更完整的语义结构。</p>
<p>为什么反复使用 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积有效，可以从感受野和参数量两方面理解。两个连续的 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积，其有效感受野接近一个 <span displaypfx="inline-" class="mathjax-container">\(5\times 5\)</span> 卷积，但中间多了一次非线性变换，参数量通常还更少。若忽略通道数变化，单层 <span displaypfx="inline-" class="mathjax-container">\(5\times 5\)</span> 卷积大约有 25 个核参数，而两层 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积一共是 18 个核参数。</p>
<p>训练上，VGG 比 AlexNet 更依赖较好的初始化、较强的正则化和更大的算力预算，因为它的参数量尤其在全连接部分非常大。它的直接应用是图像分类，但工程上更著名的是作为“通用视觉特征提取器”：在风格迁移、感知损失（Perceptual Loss）、检测与分割早期系统中，VGG 特征长期是强基线。它的代价也很明显：参数多、推理重、显存占用高，这直接推动了后续更高效架构的出现。</p>
<div class="blog_h4"><span class="graybg">ResNet</span></div>
<p>ResNet（Residual Network）的关键突破不是再把卷积堆得更深，而是重新设计“深层网络应该学什么”。它提出残差学习（Residual Learning）：一层或一组层不必直接学习完整映射 <span displaypfx="inline-" class="mathjax-container">\(H(x)\)</span>，而是学习相对于输入的增量 <span displaypfx="inline-" class="mathjax-container">\(F(x)=H(x)-x\)</span>。这样网络输出就写成：</p>
<span displaypfx="" class="mathjax-container">\[y=F(x)+x\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是块输入， <span displaypfx="inline-" class="mathjax-container">\(F(x)\)</span> 是若干卷积层、归一化和激活组成的残差分支输出， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是该残差块的最终输出。若最优映射本身就接近恒等映射（Identity Mapping），那么让网络学习“只改一点点”通常比“从零重建整个映射”更容易优化。</p>
<p>具象地看，普通深网络像要求每一层都重新写一遍完整答案；ResNet 则允许每一层在原答案旁边写批注：需要修改的地方补上增量，不需要改的地方直接走捷径。这个“捷径连接（Skip Connection）”让梯度能沿更短路径传播，因此网络深到几十层、上百层时仍可训练。ResNet 解决的核心不是表达能力不足，而是深层优化退化（Degradation）问题：层数增加后，训练误差反而上升。</p>
<p>训练上，ResNet 通常结合 BatchNorm、较深的 stage 结构和全局平均池化（Global Average Pooling）来替代庞大的全连接头。它在图像分类上大获成功后，很快成为检测、分割、关键点、视频理解等视觉任务的主流骨干网络。更深远的影响是：残差连接后来成为 Transformer、扩散模型和许多现代深网络的标准部件，因为它本质上是在为深层优化建立稳定的信息主通路。</p>
<div class="blog_h2"><span class="graybg">循环神经网络（RNN）</span></div>
<div class="blog_h3"><span class="graybg">RNN</span></div>
<p>循环神经网络（Recurrent Neural Network, RNN）按时间步（Time Step）处理序列。第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个时刻输入是 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span>，隐藏状态（Hidden State）递推为：</p>
<span displaypfx="" class="mathjax-container">\[h_t=\phi(W_{xh}x_t+W_{hh}h_{t-1}+b_h),\quad y_t=W_{hy}h_t+b_y\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(W_{xh}\)</span>（输入到隐层）与 <span displaypfx="inline-" class="mathjax-container">\(W_{hh}\)</span>（隐层到隐层）在所有时刻共享，这是 RNN 能在变长序列上泛化的关键。</p>
<p>把它展开（Unroll）到时间轴上会更直观：同一套参数在每个时间步重复使用，形成一个深度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 的计算图。这也是 RNN 训练常说的“通过时间的反向传播（Backpropagation Through Time, BPTT）”。</p>
<p>RNN 的经典难点是梯度消失/爆炸（Vanishing/Exploding Gradients）：反向传播时会反复乘以 <span displaypfx="inline-" class="mathjax-container">\(W_{hh}\)</span> 的雅可比，从而导致梯度范数指数级衰减或增长。LSTM/GRU 通过门控（Gating）与更“线性”的记忆通道缓解这一问题。</p>
<p>Seq2Seq（Sequence-to-Sequence）是任务范式，不限定具体单元。早期 Seq2Seq 常由 RNN/LSTM/GRU 的编码器-解码器（Encoder-Decoder）实现；后续被 Transformer 大规模替代。</p>
<p>“乘法 + 加法 + 非线性”为何有效：线性变换负责特征重表达，非线性激活（Nonlinearity）提供函数逼近能力，时间递推提供记忆路径，三者叠加形成高表达力。</p>
<div class="blog_h3"><span class="graybg">LSTM</span></div>
<p>LSTM（Long Short-Term Memory）是为了解决普通 RNN 难以稳定保留长程信息的问题而设计的门控循环结构。它的核心思想不是简单把隐藏状态不断往前传，而是显式维护一个记忆单元（Cell State） <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span>，并用门控决定“忘掉什么、写入什么、读出什么”。这使得序列中的关键信息可以沿着一条更接近线性的通道向后传播，而不必在每个时间步都被强非线性反复改写。</p>
<p>LSTM 的典型更新写成：</p>
<span displaypfx="" class="mathjax-container">\[f_t=\sigma(W_f x_t+U_f h_{t-1}+b_f),\quad i_t=\sigma(W_i x_t+U_i h_{t-1}+b_i)\]</span>
<span displaypfx="" class="mathjax-container">\[\tilde c_t=\tanh(W_c x_t+U_c h_{t-1}+b_c),\quad c_t=f_t\odot c_{t-1}+i_t\odot \tilde c_t\]</span>
<span displaypfx="" class="mathjax-container">\[o_t=\sigma(W_o x_t+U_o h_{t-1}+b_o),\quad h_t=o_t\odot \tanh(c_t)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 是当前输入， <span displaypfx="inline-" class="mathjax-container">\(h_{t-1}\)</span> 是前一时刻隐藏状态， <span displaypfx="inline-" class="mathjax-container">\(c_{t-1}\)</span> 是前一时刻记忆单元； <span displaypfx="inline-" class="mathjax-container">\(f_t\)</span> 是遗忘门（Forget Gate），控制旧记忆保留多少； <span displaypfx="inline-" class="mathjax-container">\(i_t\)</span> 是输入门（Input Gate），控制当前候选记忆 <span displaypfx="inline-" class="mathjax-container">\(\tilde c_t\)</span> 写入多少； <span displaypfx="inline-" class="mathjax-container">\(o_t\)</span> 是输出门（Output Gate），控制当前记忆暴露给隐藏状态多少； <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 是 sigmoid，把门值压到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 区间； <span displaypfx="inline-" class="mathjax-container">\(\odot\)</span> 是逐元素乘法。</p>
<p>具象地看，LSTM 像一条带阀门的记忆水管。普通 RNN 每走一步都把旧信息和新输入一锅重拌，长距离信息容易被冲淡；LSTM 则允许旧记忆沿主管道直接往后流，同时用三个阀门分别控制“放掉旧水”“注入新水”“输出多少”。这正是它能比普通 RNN 更稳定地记住长距离依赖的原因。</p>
<p>训练上，LSTM 仍然通过时间反向传播（BPTT）学习参数，但门控结构显著改善了梯度流。它曾长期是机器翻译、语音识别、语言建模、时间序列预测和序列标注的主力模型。在 Transformer 出现之前，大量 Seq2Seq 系统都建立在双向 LSTM 或多层 LSTM 之上；在某些中小规模时序任务里，LSTM 今天仍然是强而稳的基线。</p>
<div class="blog_h3"><span class="graybg">GRU</span></div>
<p>GRU（Gated Recurrent Unit）可以看作 LSTM 的简化版本。它保留了“用门控控制信息保留与更新”的核心思想，但把记忆单元与隐藏状态合并，不再单独维护 <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span>，从而用更少参数换取更紧凑的结构与更快的训练速度。</p>
<p>GRU 的一组典型公式是：</p>
<span displaypfx="" class="mathjax-container">\[z_t=\sigma(W_z x_t+U_z h_{t-1}+b_z),\quad r_t=\sigma(W_r x_t+U_r h_{t-1}+b_r)\]</span>
<span displaypfx="" class="mathjax-container">\[\tilde h_t=\tanh(W_h x_t+U_h(r_t\odot h_{t-1})+b_h)\]</span>
<span displaypfx="" class="mathjax-container">\[h_t=(1-z_t)\odot h_{t-1}+z_t\odot \tilde h_t\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span> 是更新门（Update Gate），决定旧状态保留多少、新候选状态写入多少； <span displaypfx="inline-" class="mathjax-container">\(r_t\)</span> 是重置门（Reset Gate），决定在构造候选状态 <span displaypfx="inline-" class="mathjax-container">\(\tilde h_t\)</span> 时，历史信息应当被参考到什么程度。若 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span> 很小，模型更倾向保留旧记忆；若很大，模型更倾向用新信息刷新状态。</p>
<p>从直觉上看，GRU 把 LSTM 的三道阀门压缩成两道更紧凑的控制逻辑：一方面决定“要不要更新”，另一方面决定“生成候选更新时要不要忘掉旧状态的一部分”。这让它在很多任务上能以更少参数达到与 LSTM 相近的效果，尤其适合数据量不极大、模型容量受限或推理效率敏感的场景。</p>
<p>训练上，GRU 与 LSTM 一样使用 BPTT。应用上，它常见于语音、时序预测、较轻量的编码器-解码器模型，以及很多工业界的表格时间序列任务。若需要更强的显式记忆控制，LSTM 往往更稳；若更看重结构简洁和训练效率，GRU 常是更自然的选择。</p>
<div class="blog_h2"><span class="graybg">生成模型</span></div>
<p>生成模型（Generative Model）关注的核心问题不是“这个样本属于哪一类”，而是“数据本身是如何产生出来的”。更形式化地说，它试图学习数据分布 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>，或条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(x|c)\)</span>，从而能够采样、重建、补全、去噪或按条件生成新样本。不同生成模型的主要差别在于它们选择了不同的概率建模路径：有的学隐变量，有的学博弈过程，有的学逐步去噪，有的更偏重表示压缩。</p>
<div class="blog_h3"><span class="graybg">自编码器（AE）</span></div>
<p>自编码器（Autoencoder, AE）的核心思想是“先压缩，再重建”。它把输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 通过编码器（Encoder）映射到低维或受约束的隐表示（Latent Representation） <span displaypfx="inline-" class="mathjax-container">\(z\)</span>，再通过解码器（Decoder）把 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 重建回 <span displaypfx="inline-" class="mathjax-container">\(\hat x\)</span>。如果模型在受限瓶颈下仍能较好重建输入，就说明 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 抓住了数据中的关键结构。</p>
<p>其基本形式可以写成：</p>
<span displaypfx="" class="mathjax-container">\[z=f_{\theta}(x),\qquad \hat x=g_{\phi}(z)\]</span>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{AE}}=\|x-\hat x\|_2^2\quad \text{或}\quad -\sum_i x_i\log \hat x_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f_{\theta}\)</span> 是编码器， <span displaypfx="inline-" class="mathjax-container">\(g_{\phi}\)</span> 是解码器， <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是瓶颈表示， <span displaypfx="inline-" class="mathjax-container">\(\hat x\)</span> 是重建结果。若输入是连续值，常用均方误差；若输入近似二值或归一化到概率意义，常用逐维交叉熵。这个目标迫使模型学习“怎样用更紧凑的表示保存足够重建原样本的信息”。</p>
<p>自编码器本身并不天然是强生成模型，因为它主要学会的是“给定输入怎么重建自己”，而不是如何从一个规则、可采样的潜空间中稳定地产生新样本。它更适合理解为表示学习或降维模型。通过在结构或训练目标上增加限制，例如稀疏自编码器（Sparse AE）、去噪自编码器（Denoising AE）或收缩自编码器（Contractive AE），它可以学到更稳健的隐表示。</p>
<p>应用上，AE 常用于降维、异常检测、去噪、预训练和表征学习。例如在工业异常检测里，模型只用正常样本训练，推理时若某个输入无法被良好重建，重建误差就可能提示该样本偏离了正常分布。</p>
<div class="blog_h3"><span class="graybg">变分自编码器（VAE）</span></div>
<p>变分自编码器（Variational Autoencoder, VAE）是在 AE 基础上把“隐空间可采样”这件事做成概率建模的方案。它不再把编码器输出一个确定的潜向量，而是输出一个潜变量分布 <span displaypfx="inline-" class="mathjax-container">\(q_{\phi}(z|x)\)</span>；同时假设存在生成分布 <span displaypfx="inline-" class="mathjax-container">\(p_{\theta}(x|z)\)</span>。这样，模型既能重建输入，又能从一个规则的潜空间里采样新样本。</p>
<p>VAE 的核心训练目标是证据下界（Evidence Lower Bound, ELBO）：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{VAE}}=\mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)]-D_{\mathrm{KL}}\big(q_{\phi}(z|x)\,\|\,p(z)\big)\]</span>
<p>其中第一项是重建项，鼓励给定 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 时能把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 生成回来；第二项是 KL 散度（Kullback-Leibler Divergence），把编码器输出的后验近似分布 <span displaypfx="inline-" class="mathjax-container">\(q_{\phi}(z|x)\)</span> 拉向先验分布 <span displaypfx="inline-" class="mathjax-container">\(p(z)\)</span>，通常取标准高斯 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(0,I)\)</span>。这一步的作用是把隐空间整理成“规则、连续、可插值、可采样”的形状。</p>
<p>训练上的关键技巧是重参数化（Reparameterization Trick）：若直接从 <span displaypfx="inline-" class="mathjax-container">\(q_{\phi}(z|x)\)</span> 采样，梯度难以回传；VAE 通常把采样写成 <span displaypfx="inline-" class="mathjax-container">\(z=\mu+\sigma\odot \epsilon\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\sim\mathcal{N}(0,I)\)</span>，而 <span displaypfx="inline-" class="mathjax-container">\(\mu,\sigma\)</span> 由编码器输出。这样随机性被转移到与参数无关的 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 上，梯度就能顺利穿过 <span displaypfx="inline-" class="mathjax-container">\(\mu,\sigma\)</span> 回传。</p>
<p>应用上，VAE 特别适合做潜空间操作，例如插值生成、属性控制、缺失补全和概率建模。与 GAN 相比，VAE 生成结果往往更平滑、更稳定，但图像清晰度常较弱；与扩散模型相比，VAE 在采样效率上更高，但生成质量上通常不是最强路线。</p>
<div class="blog_h3"><span class="graybg">生成对抗网络（GAN）</span></div>
<p>生成对抗网络（Generative Adversarial Network, GAN）的核心思想是对抗式学习：让生成器（Generator）负责“伪造样本”，让判别器（Discriminator）负责“分辨真假”，两者在博弈中共同提高。生成器学会把随机噪声变成越来越像真实数据的样本，判别器则学会识别这些样本是否来自真实分布。</p>
<p>经典 GAN 的目标写成：</p>
<span displaypfx="" class="mathjax-container">\[\min_G\max_D\ V(D,G)=\mathbb{E}_{x\sim p_{\mathrm{data}}}[\log D(x)]+\mathbb{E}_{z\sim p(z)}[\log(1-D(G(z)))] \]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是从先验分布中采样的随机噪声， <span displaypfx="inline-" class="mathjax-container">\(G(z)\)</span> 是生成器产生的假样本， <span displaypfx="inline-" class="mathjax-container">\(D(x)\)</span> 输出输入样本为真样本的概率。判别器希望让真实样本分数高、伪样本分数低；生成器则希望骗过判别器，让 <span displaypfx="inline-" class="mathjax-container">\(G(z)\)</span> 看起来足够真实。</p>
<p>具象地看，GAN 像“伪造者”和“鉴定师”的对抗升级。鉴定师越强，伪造者就被迫学会更精细的伪造技巧；伪造者越强，鉴定师也必须学会更细致的辨别规则。理论上，这种动态会把生成分布推向真实数据分布；工程上，它带来的最大问题是训练不稳定，常见现象包括模式崩塌（Mode Collapse）、震荡和判别器过强导致生成器梯度过弱。</p>
<p>GAN 在图像生成、图像翻译、超分辨率和风格迁移里曾经极其成功，因为它特别善于生成锐利、感知上真实的图像纹理。但它对训练技巧依赖很强，后来在大规模高保真图像生成上逐渐被扩散模型压过；即便如此，GAN 在需要低步数快速生成或特定视觉变换任务中仍然很有生命力。</p>
<div class="blog_h3"><span class="graybg">扩散模型（Diffusion）</span></div>
<p>扩散模型（Diffusion Model）的核心思想是：把“直接学会生成复杂数据”拆成“先逐步加噪，再逐步去噪”这条更稳定的路径。前向过程把真实样本一步步污染成近似高斯噪声，反向过程则训练一个神经网络学会在每一步去掉一小部分噪声。最终从纯噪声出发，经过多步反向去噪，就能逐步生成结构清晰的样本。</p>
<p>记号上，前向扩散链从真实样本 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 出发，依次得到 <span displaypfx="inline-" class="mathjax-container">\(x_1,x_2,\dots,x_T\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 表示原始数据样本， <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步加噪后的样本。</p>
<p>一个常见的前向加噪过程写成：</p>
<span displaypfx="" class="mathjax-container">\[q(x_t|x_{t-1})=\mathcal{N}\!\left(x_t;\sqrt{1-\beta_t}\,x_{t-1},\beta_t I\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\beta_t\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步的噪声强度：它控制从 <span displaypfx="inline-" class="mathjax-container">\(x_{t-1}\)</span> 走到 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 时，原信号衰减多少、随机噪声注入多少。训练时常把从 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 到 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 的若干步合并写成</p>
<span displaypfx="" class="mathjax-container">\[x_t=\sqrt{\bar\alpha_t}\,x_0+\sqrt{1-\bar\alpha_t}\,\epsilon,\qquad \epsilon\sim\mathcal{N}(0,I)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\bar\alpha_t\)</span> 是由噪声日程（Noise Schedule）累乘得到的系数，控制到第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步时，原始信号保留了多少、噪声混入了多少。实际训练中，网络通常不直接预测 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span>，而是预测噪声 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>，典型损失是：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{diff}}=\mathbb{E}\big[\|\epsilon-\epsilon_{\theta}(x_t,t)\|_2^2\big]\]</span>
<p>这条路径之所以有效，在于“预测一步噪声”比“直接一次生成整张复杂图片”更容易优化。训练上，扩散模型相对稳定，较少出现 GAN 那种对抗不平衡；代价是采样通常需要多步迭代，推理速度较慢。应用上，扩散模型已经成为图像生成、文生图、图像编辑、超分辨率、视频生成和分子设计的重要主线。Stable Diffusion 一类系统，本质上就是把扩散过程放到了潜空间（Latent Space）里执行，以降低像素空间扩散的计算成本。</p>
<div class="blog_h2"><span class="graybg">图神经网络（GNN）</span></div>
<p>图神经网络（Graph Neural Network, GNN）处理的对象不是规则网格上的序列或图像，而是由节点（Node）和边（Edge）组成的图（Graph）。它的核心问题是：当样本之间存在不规则连接关系时，如何让一个节点的表示同时反映“自己的特征”和“邻居结构中的上下文”。因此，GNN 的基本直觉不是滑动卷积核，也不是按时间递推，而是让节点在图上传递消息、聚合邻居信息，再更新自身表示。</p>
<div class="blog_h3"><span class="graybg">图卷积网络（GCN）</span></div>
<p>图卷积网络（Graph Convolutional Network, GCN）把卷积思想推广到图结构上。它的核心思想是：一个节点的新表示，不应只由自己决定，还应由其邻居节点的表示共同决定；但这种聚合不能简单相加，而需要根据图结构做适当归一化，否则高度节点会在信息聚合中占据过大权重。</p>
<p>GCN 的经典一层更新公式写成：</p>
<span displaypfx="" class="mathjax-container">\[H^{(l+1)}=\sigma\!\left(\tilde D^{-1/2}\tilde A\tilde D^{-1/2}H^{(l)}W^{(l)}\right)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(H^{(l)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层所有节点的表示矩阵； <span displaypfx="inline-" class="mathjax-container">\(W^{(l)}\)</span> 是该层可学习权重； <span displaypfx="inline-" class="mathjax-container">\(\tilde A=A+I\)</span> 表示在原邻接矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 上加自环（Self-loop），让节点保留自己的信息； <span displaypfx="inline-" class="mathjax-container">\(\tilde D\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(\tilde A\)</span> 的度矩阵（Degree Matrix）； <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 是非线性激活。中间那项对邻居求和并按度做归一化，本质上是在做“平滑的邻居平均”。</p>
<p>具象地看，GCN 像在社交网络里更新一个人的画像：不仅看这个人自己填写的特征，还参考他一跳邻居的大致特征，再做归一化，避免“朋友特别多的人”把自己的表示稀释得过于严重。多层堆叠后，一个节点就能间接接触到两跳、三跳甚至更远范围的信息。</p>
<p>训练上，GCN 常用于节点分类、图分类和链路预测。它的经典应用包括引文网络分类、分子图预测和推荐系统图表示学习。局限也很典型：层数太深时，节点表示会越来越相似，出现过平滑（Oversmoothing）；大图上直接用全图邻接矩阵训练也会带来显存与计算压力。</p>
<div class="blog_h3"><span class="graybg">图注意力网络（GAT）</span></div>
<p>图注意力网络（Graph Attention Network, GAT）在 GCN 的“统一归一化邻居平均”之上进一步提出：不同邻居的重要性不应被预先固定，而应由模型动态学习。它的核心思想是把注意力机制引入图结构，使每个节点在聚合邻居时，能够自适应决定“更该听谁的话”。</p>
<p>一层 GAT 的典型计算是：</p>
<span displaypfx="" class="mathjax-container">\[e_{ij}=a(Wh_i,Wh_j),\qquad \alpha_{ij}=\frac{\exp(e_{ij})}{\sum_{k\in \mathcal{N}(i)}\exp(e_{ik})}\]</span>
<span displaypfx="" class="mathjax-container">\[h_i'=\sigma\!\left(\sum_{j\in \mathcal{N}(i)} \alpha_{ij}\,Wh_j\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 的输入表示， <span displaypfx="inline-" class="mathjax-container">\(W\)</span> 是线性变换矩阵， <span displaypfx="inline-" class="mathjax-container">\(a(\cdot,\cdot)\)</span> 是注意力打分函数， <span displaypfx="inline-" class="mathjax-container">\(e_{ij}\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 对节点 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 的未归一化重要性分数， <span displaypfx="inline-" class="mathjax-container">\(\alpha_{ij}\)</span> 是在邻居集合 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(i)\)</span> 内 softmax 归一化后的注意力权重。</p>
<p>与 GCN 相比，GAT 的关键收益是更灵活：若某个邻居特别重要，模型可以给它更高权重；若某个邻居噪声较大，则可自动抑制。训练上，GAT 仍通过监督或自监督目标学习节点表示，常用多头注意力（Multi-head Attention）稳定训练。应用上，它常见于异质关系更复杂、邻居重要性差异显著的图任务，例如社交网络分析、知识图谱局部推断和分子性质预测。</p>
<div class="blog_h3"><span class="graybg">GraphSAGE</span></div>
<p>GraphSAGE（Graph Sample and Aggregate）的核心思想是把 GNN 从“转导式（Transductive）图编码”推进到“归纳式（Inductive）图表示学习”。传统 GCN 常依赖整张训练图；GraphSAGE 则强调：即使测试时出现训练中未见过的新节点，只要能拿到它的邻居特征，也应能在线生成它的表示。</p>
<p>其典型更新形式是先采样邻居，再做聚合：</p>
<span displaypfx="" class="mathjax-container">\[h_{\mathcal{N}(v)}^{(k)}=\mathrm{AGG}^{(k)}\big(\{h_u^{(k-1)}:u\in \mathcal{N}(v)\}\big)\]</span>
<span displaypfx="" class="mathjax-container">\[h_v^{(k)}=\sigma\!\left(W^{(k)}[h_v^{(k-1)}\|h_{\mathcal{N}(v)}^{(k)}]\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(h_v^{(k)}\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(v\)</span> 在第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 层的表示， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(v)\)</span> 是其邻居集合， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{AGG}\)</span> 可以是均值、池化或 LSTM 聚合器， <span displaypfx="inline-" class="mathjax-container">\([\cdot\|\cdot]\)</span> 表示向量拼接。GraphSAGE 的关键工程点是“采样”，因为超大图中不可能每次把全部邻居完整展开。</p>
<p>具象地看，GraphSAGE 像为每个节点建立一套“从邻居摘要中构造自我画像”的规则。它不要求记住整张训练图中每个节点的专属嵌入，而是学会一套可迁移的邻域聚合函数。这正是它能处理新节点、动态图和大规模图数据的原因。</p>
<p>应用上，GraphSAGE 在推荐系统、社交网络、风控图谱和工业知识图谱中非常常见，因为这些场景经常不断出现新节点、新边，归纳式能力比单纯在固定图上做转导预测更重要。</p>
<div class="blog_h3"><span class="graybg">消息传递机制（Message Passing）</span></div>
<p>消息传递（Message Passing）不是某一个具体模型，而是理解 GNN 的统一抽象框架。无论是 GCN、GAT 还是 GraphSAGE，本质上都可以拆成两步：第一步，节点从邻居那里接收消息；第二步，把这些消息与自己的旧表示结合，更新成新的节点表示。</p>
<p>这一抽象常写成：</p>
<span displaypfx="" class="mathjax-container">\[m_v^{(l+1)}=\mathrm{AGG}\Big(\{M^{(l)}(h_v^{(l)},h_u^{(l)},e_{uv})\,:\,u\in\mathcal{N}(v)\}\Big)\]</span>
<span displaypfx="" class="mathjax-container">\[h_v^{(l+1)}=U^{(l)}\big(h_v^{(l)},m_v^{(l+1)}\big)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(h_v^{(l)}\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(v\)</span> 在第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层的表示， <span displaypfx="inline-" class="mathjax-container">\(e_{uv}\)</span> 是边特征， <span displaypfx="inline-" class="mathjax-container">\(M^{(l)}\)</span> 是消息函数，决定一条边上传递什么信息； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{AGG}\)</span> 是聚合函数，如求和、均值、最大值或注意力加权和； <span displaypfx="inline-" class="mathjax-container">\(U^{(l)}\)</span> 是更新函数，把旧表示与聚合后的消息合成为新表示。</p>
<p>这个框架的重要性在于，它把看似不同的图模型放进了一套统一语言里：GCN 相当于使用归一化线性消息与均值式聚合，GAT 相当于把注意力权重写进聚合，GraphSAGE 相当于强调采样与归纳式聚合。理解了消息传递，就能把很多图模型看成“消息函数、聚合函数、更新函数”三处设计选择的不同组合。</p>
<p>训练上，消息传递式 GNN 常用于三类任务：节点级任务（例如节点分类）、边级任务（例如链路预测）、图级任务（例如分子性质预测）。它们的共同难点包括：过平滑、邻居爆炸（Neighborhood Explosion）、异质图关系复杂，以及深层堆叠后长程依赖难以稳定传播。很多现代 GNN 改进，本质上都在围绕这几个瓶颈重新设计消息传递规则。</p>
<div class="blog_h2"><span class="graybg">ONNX</span></div>
<p>ONNX（Open Neural Network Exchange，开放神经网络交换格式）是一种描述机器学习模型的开放标准。它的核心作用，不是训练模型，而是把已经定义并训练好的神经网络表示成一种<span style="background-color: #c0c0c0;">可交换、可部署、跨框架可读</span>的中间格式，使模型能够从训练环境转移到不同推理环境中运行。</p>
<div class="blog_h3"><span class="graybg">它解决什么问题</span></div>
<p>训练阶段常用 PyTorch、TensorFlow、JAX 等框架；部署阶段则常落到 ONNX Runtime、TensorRT、OpenVINO、移动端推理引擎或嵌入式执行环境。问题在于，这些系统各自拥有不同的内部表示方式、图执行器和算子实现，原生模型格式并不天然互通。ONNX 解决的正是这条链路中的“中间表示”问题。</p>
<p>可以把 ONNX 理解为模型世界里的 PDF：训练框架先把网络导出为 <pre class="crayon-plain-tag">.onnx</pre> 文件，部署侧再由相应 runtime 读取、优化并执行。它并不等于某一种具体推理引擎，而更像一个让不同框架与部署后端彼此对接的交换层。</p>
<div class="blog_h3"><span class="graybg">一个 ONNX 文件里有什么</span></div>
<p>从结构上看，ONNX 文件本质上是一份<span style="background-color: #c0c0c0;">静态计算图（Static Computation Graph）加权重参数的描述</span>。其中通常包含四类核心信息：</p>
<ul>
<li>计算图：模型由哪些算子组成，例如卷积（Conv）、矩阵乘法（MatMul）、归一化、激活、注意力等。</li>
<li>张量元信息：输入、输出与中间张量的形状（Shape）和数据类型（Data Type）。</li>
<li>参数权重：训练后得到的权重矩阵、偏置和其他可学习参数；大模型也可能采用外部权重文件。</li>
<li>算子版本信息：模型依赖的 ONNX opset 版本，以及每个算子的语义约定。</li>
</ul>
<p>这种表示方式的关键价值，是把“Python 前向代码如何写”转写成“部署时应该执行哪条算子图”。一旦导出完成，部署系统通常不再依赖训练时的 Python 类定义，而是按 ONNX 图里的算子与依赖关系直接执行。</p>
<div class="blog_h3"><span class="graybg">常见用途</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">场景</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>部署加速</td>
<td>交给 ONNX Runtime、TensorRT 等后端做图优化、算子融合、低精度执行和量化推理</td>
</tr>
<tr>
<td>跨框架部署</td>
<td>例如用 PyTorch 训练，再导出 ONNX，交由另一套推理栈加载运行</td>
</tr>
<tr>
<td>多端适配</td>
<td>同一模型可进一步转接到服务器、边缘设备、移动端或嵌入式环境，前提是目标引擎支持相应算子</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">与 PyTorch 的关系</span></div>
<p>在 PyTorch 生态里，ONNX 最常见的入口是导出。开发者通常先在 PyTorch 中定义并训练模型，再用 <pre class="crayon-plain-tag">torch.onnx.export(...)</pre> 把模型转换成 ONNX 图。这个过程的本质，是把 PyTorch 里的前向路径从“解释执行的 Python 模块”改写成“显式算子图”。</p>
<p>导出之后，部署侧执行的已不再是原始 Python 前向代码，而是 ONNX 图中的算子序列。因此，依赖复杂动态控制流、框架私有算子或运行时分支逻辑的模型，在导出时往往需要额外改写、简化或替换成更可静态化的结构。</p>
<div class="blog_h3"><span class="graybg">局限与边界</span></div>
<p>ONNX 的价值很大，但它并不是“只要导出就一定能无缝部署”的万能格式。第一，并非所有训练框架中的算子都存在完美的 ONNX 映射，复杂模型可能导出失败，或需要改写成等价但更标准的图结构。第二，不同推理引擎对 ONNX 的支持程度并不完全一致：即使同样读取 ONNX 文件，也可能只支持某些 opset 版本或某一部分算子子集。第三，ONNX 更适合表达相对稳定的前向计算图；当模型强依赖高度动态的运行逻辑时，静态导出路径会更受约束。</p>
<p>因此，ONNX 更准确的定位不是“另一种训练框架”，而是<span style="background-color: #c0c0c0;">训练环境与部署环境之间的中间表示层</span>。它的意义在于把模型从原始框架内部释放出来，交给更适合推理、优化和跨平台执行的后端系统。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-3">人工智能理论知识 - Transformers和大模型</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ai-knowledge-quick-ref-3/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>人工智能理论知识 - 算法和经典机器学习</title>
		<link>https://blog.gmem.cc/ai-knowledge-quick-ref-2</link>
		<comments>https://blog.gmem.cc/ai-knowledge-quick-ref-2#comments</comments>
		<pubDate>Wed, 15 Apr 2026 12:43:43 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=42147</guid>
		<description><![CDATA[<p>这一篇从常用算法进入机器学习基础概念、经典机器学习与神经网络，重点讨论“模型如何被构造、训练、评估与正则化”。前一篇给出了数学语言，这一篇开始进入真正的建模问题：数据怎样表示，损失怎样定义，优化怎样推进，不同模型家族各自擅长什么；再往后才会过渡到 Transformer 与大语言模型。 常用算法 基础数据结构和算法 这一节处理的核心问题是：当面对搜索、更新、统计、调度、最短路径、依赖分析或训练流水线等任务时，数据应该怎样组织，操作应该怎样执行，才能既正确又高效。数据结构（Data Structure）决定“数据在内存里如何表示”，算法（Algorithm）决定“在这种表示上如何完成查询、插入、删除、遍历、排序与优化”。很多系统性能问题，本质上不是算力不足，而是底层组织方式与操作方式不匹配。 可以把它理解成“仓库布局与搬运规则”的组合：同样一批货物，若排成连续货架、串成链式节点、组织成树状目录，或连接成路网，后续的查找、插入、合并与运输成本会完全不同。现代 AI 工程虽然把注意力集中在模型上，但数据加载器、特征流水线、参数缓存、向量检索、计算图调度、图学习和索引系统，最终都建立在这些基础结构之上。 结构 / 算法 核心能力 典型复杂度 常见场景 数组 / 动态数组 按下标随机访问；顺序扫描效率高 访问 ；中间插入/删除 <a class="read-more" href="https://blog.gmem.cc/ai-knowledge-quick-ref-2">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-2">人工智能理论知识 - 算法和经典机器学习</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>这一篇从常用算法进入机器学习基础概念、经典机器学习与神经网络，重点讨论“模型如何被构造、训练、评估与正则化”。前一篇给出了数学语言，这一篇开始进入真正的建模问题：数据怎样表示，损失怎样定义，优化怎样推进，不同模型家族各自擅长什么；再往后才会过渡到 Transformer 与大语言模型。</p>
<div class="blog_h1"><span class="graybg">常用算法</span></div>
<div class="blog_h2"><span class="graybg">基础数据结构和算法</span></div>
<p>这一节处理的核心问题是：当面对搜索、更新、统计、调度、最短路径、依赖分析或训练流水线等任务时，数据应该怎样组织，操作应该怎样执行，才能既正确又高效。数据结构（Data Structure）决定“数据在内存里如何表示”，算法（Algorithm）决定“在这种表示上如何完成查询、插入、删除、遍历、排序与优化”。很多系统性能问题，本质上不是算力不足，而是底层组织方式与操作方式不匹配。</p>
<p>可以把它理解成“仓库布局与搬运规则”的组合：同样一批货物，若排成连续货架、串成链式节点、组织成树状目录，或连接成路网，后续的查找、插入、合并与运输成本会完全不同。现代 AI 工程虽然把注意力集中在模型上，但数据加载器、特征流水线、参数缓存、向量检索、计算图调度、图学习和索引系统，最终都建立在这些基础结构之上。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">结构 / 算法</td>
<td style="text-align: center;">核心能力</td>
<td style="text-align: center;">典型复杂度</td>
<td style="text-align: center;">常见场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>数组 / 动态数组</td>
<td>按下标随机访问；顺序扫描效率高</td>
<td>访问 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>；中间插入/删除 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span></td>
<td>张量、批数据、embedding、排序、滑动窗口</td>
</tr>
<tr>
<td>链表</td>
<td>已知位置后插入/删除代价低</td>
<td>局部插删 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>；查找 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span></td>
<td>LRU、任务拼接、频繁重排的序列</td>
</tr>
<tr>
<td>栈（Stack）</td>
<td>后进先出（LIFO）</td>
<td>push / pop / top 均为 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span></td>
<td>递归展开、表达式解析、单调栈</td>
</tr>
<tr>
<td>队列（Queue）</td>
<td>先进先出（FIFO）</td>
<td>enqueue / dequeue 均为 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span></td>
<td>BFS、任务队列、流式缓冲</td>
</tr>
<tr>
<td>哈希表（Hash Table）</td>
<td>按键快速索引</td>
<td>平均查找/插入/删除 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span></td>
<td>字典、词表、缓存、去重</td>
</tr>
<tr>
<td>Bloom Filter</td>
<td>近似集合成员查询</td>
<td>插入/查询均为 <span displaypfx="inline-" class="mathjax-container">\(O(k)\)</span></td>
<td>缓存预检查、去重预过滤、存储层键存在性判断</td>
</tr>
<tr>
<td>树（Tree）</td>
<td>表达层次关系与有序结构</td>
<td>平衡查找常为 <span displaypfx="inline-" class="mathjax-container">\(O(\log n)\)</span></td>
<td>索引、优先队列、前缀匹配、规则分裂</td>
</tr>
<tr>
<td>图（Graph）</td>
<td>表达任意对象之间的关系</td>
<td>遍历通常为 <span displaypfx="inline-" class="mathjax-container">\(O(|V|+|E|)\)</span></td>
<td>社交网络、知识图谱、路线规划、依赖分析</td>
</tr>
</tbody>
</table>
<p>复杂度表只给出渐近上界，不能直接替代工程判断。真实系统还要同时考虑缓存友好性（Cache Locality）、常数项、并发开销、内存占用和实现复杂度。例如链表在理论上支持常数时间插入，但它对 CPU 缓存并不友好；数组在理论上中间插入较慢，但顺序扫描极快，因此在现代硬件上经常更有优势。</p>
<div class="blog_h3"><span class="graybg">数组与动态数组</span></div>
<p>数组（Array）处理的核心问题是：当元素类型一致、数量可以按顺序编号时，如何支持最低成本的随机访问与批量扫描。它的关键性质是<span style="background-color: #c0c0c0;">连续内存（Contiguous Memory）</span>。若每个元素大小为 <span displaypfx="inline-" class="mathjax-container">\(s\)</span>，首地址为 <span displaypfx="inline-" class="mathjax-container">\(\text{base}\)</span>，则第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个元素地址为</p>
<span displaypfx="" class="mathjax-container">\[\text{addr}(a_i)=\text{base}+i\cdot s\]</span>
<p>这个式子说明数组访问为何是 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>：位置可以直接计算，不需要沿指针逐步跳转。矩阵、张量、mini-batch、时间序列缓存、本地特征块和 embedding 表中的一行，本质上都依赖这种“地址可算”的结构。</p>
<p>数组的代价也非常明确：若在中间插入或删除元素，后面的元素必须整体搬移，因此复杂度通常是 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span>。这意味着数组适合“读多写少、顺序稳定”的任务，不适合“在任意位置频繁插入”的任务。</p>
<p>动态数组（Dynamic Array）是在数组上的工程扩展：容量不足时申请更大的连续空间，把原有元素整体拷贝过去，再继续追加。一次扩容代价很高，但若容量按倍数增长，则追加操作的均摊（Amortized）复杂度仍可视为 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。Python 的 list、C++ 的 vector、Java 的 ArrayList 都遵循这一思想。</p>
<p>直觉上，数组像按编号排好的货架：拿第 137 件货非常快，但若要把一件货塞进中间，后面整排货物都要整体后移。</p>
<div class="blog_h3"><span class="graybg">链表</span></div>
<p>链表（Linked List）处理的是另一类问题：当序列顺序经常变化时，能否避免数组那样的大规模搬移。链表不要求连续内存，而是让每个节点（Node）保存数据和指向下一个节点的指针（Pointer）；双向链表（Doubly Linked List）还会额外保存前驱指针。</p>
<p>若已经拿到某个节点的位置，那么在其前后插入或删除节点只需要调整局部指针，代价通常是 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。但链表无法像数组那样通过下标直接定位第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个元素，查找往往必须从头逐个走过去，因此通常是 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span>。</p>
<p>链表适合做“结构改动频繁、定位方式不是按下标而是按已有节点句柄”的任务。例如 LRU 缓存中，经常需要把刚访问的元素移到头部；若配合哈希表记录节点位置，链表就能高效完成重排。</p>
<p>链表像一串用绳子串起来的标签。改顺序很方便，但想直接摸到第 500 个标签，就只能沿着绳子一个个数过去。</p>
<div class="blog_h3"><span class="graybg">栈（Stack）</span></div>
<p>栈（Stack）定义的是一种<span style="background-color: #c0c0c0;">后进先出（Last In First Out, LIFO）</span>的访问约束。它处理的问题不是“怎样存更多数据”，而是“怎样强制最近进入的状态最先退出”。典型操作包括入栈 push、出栈 pop 与查看栈顶 top，它们都发生在同一端，因此实现代价通常是 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。</p>
<p>函数调用栈、递归回溯、表达式求值、括号匹配、深度优先搜索中的显式状态保存，都依赖这种结构。其本质是把“尚未处理完的上下文”按嵌套顺序压起来，等内部任务结束后再按相反顺序恢复。</p>
<p>单调栈（Monotonic Stack）是栈在算法中的重要变体。它通过维护一个单调递增或单调递减的栈，把“下一个更大元素”“柱状图最大矩形”等问题从 <span displaypfx="inline-" class="mathjax-container">\(O(n^2)\)</span> 降到 <span displaypfx="inline-" class="mathjax-container">\(O(n)\)</span>。原因在于每个元素最多入栈和出栈各一次。</p>
<div class="blog_h3"><span class="graybg">队列（Queue）</span></div>
<p>队列（Queue）定义的是<span style="background-color: #c0c0c0;">先进先出（First In First Out, FIFO）</span>的访问约束。进入得早的元素先被处理，后来进入的元素排在尾部等待。它适合表达“任务排队、波前扩张、按到达顺序消费”的过程。</p>
<p>广度优先搜索（Breadth-First Search, BFS）之所以使用队列，正是因为 BFS 要按距离层层扩展：先处理距离起点为 1 的节点，再处理距离为 2 的节点。这个“分层推进”机制与 FIFO 完全一致。</p>
<p>循环队列（Circular Queue）通过把底层数组首尾相连，可以避免频繁搬移；双端队列（Deque）则允许两端都做插入和删除，因此能够支持滑动窗口最值、0-1 BFS 等更复杂的算法模式。</p>
<div class="blog_h3"><span class="graybg">哈希表（Hash Table）</span></div>
<p>哈希表（Hash Table）处理的核心问题是：当数据按“键（Key）”组织，而不是按位置组织时，如何快速找到对应的值（Value）。其思想是先通过哈希函数（Hash Function）把键映射成一个整数，再把这个整数映射到桶（Bucket）或槽位（Slot）上。</p>
<p>若哈希函数分布均匀，且装载因子（Load Factor）控制合理，则查找、插入和删除的平均复杂度都可接近 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>。这正是词表映射、去重、缓存索引、参数名字典和特征 ID 映射大量采用哈希表的原因。</p>
<p>哈希表的难点在冲突（Collision）处理。多个键可能映射到同一位置，常见解决方案包括链地址法（Separate Chaining）和开放定址法（Open Addressing）。因此“哈希表平均 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span>”并不意味着永远常数时间，它依赖于哈希函数质量、负载控制和冲突处理策略。</p>
<div class="blog_h4"><span class="graybg">Bloom Filter（布隆过滤器）</span></div>
<p>Bloom Filter 本质上属于<span style="background-color: #c0c0c0;">概率型数据结构（Probabilistic Data Structure）</span>，更准确地说，是一种近似集合成员查询结构（Approximate Membership Query, AMQ）。它解决的问题并不是“把元素精确存下来”，而是“用极小内存快速判断某元素是否可能出现过”。因此它通常作为哈希表、数据库索引或缓存系统之前的一层预过滤结构。</p>
<p>Bloom Filter 由一个长度为 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 的比特数组 <span displaypfx="inline-" class="mathjax-container">\(B\in\{0,1\}^m\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个哈希函数 <span displaypfx="inline-" class="mathjax-container">\(h_1,\dots,h_k\)</span> 构成，其中每个哈希函数都把元素 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 映射到区间 <span displaypfx="inline-" class="mathjax-container">\(\{0,1,\dots,m-1\}\)</span> 中的一个位置。插入元素 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时，执行</p>
<span displaypfx="" class="mathjax-container">\[B[h_1(x)]=B[h_2(x)]=\cdots=B[h_k(x)]=1\]</span>
<p>查询元素 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时，检查 <span displaypfx="inline-" class="mathjax-container">\(B[h_1(x)],\dots,B[h_k(x)]\)</span>。只要其中至少有一个位置为 0，就可以断定 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 一定不在集合中；若这些位置全部为 1，则只能说明 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 可能在集合中。</p>
<p>这一定义直接带来 Bloom Filter 最重要的判定性质：<span style="background-color: #c0c0c0;">它允许假阳性（False Positive），但不允许假阴性（False Negative）</span>。原因在于，不同元素可能把同一批 bit 位置反复置为 1，于是一个从未插入过的元素也可能“碰巧”命中全 1；但只要某个位置仍为 0，就说明没有任何已插入元素覆盖过这条哈希路径，因此该元素一定不存在。</p>
<p>设一共插入了 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个元素，则某个 bit 在所有插入结束后仍为 0 的概率近似为 <span displaypfx="inline-" class="mathjax-container">\(\left(1-\frac{1}{m}\right)^{kn}\approx e^{-kn/m}\)</span>。于是查询一个未出现元素时， <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个位置恰好都为 1 的假阳性概率近似为</p>
<span displaypfx="" class="mathjax-container">\[p\approx \left(1-e^{-kn/m}\right)^k\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 是 bit 数组长度， <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是已插入元素数， <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 是哈希函数数目， <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是假阳性概率。这个公式揭示了 Bloom Filter 的基本权衡： <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 越大，冲突越少； <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 越大，数组越接近被“染满”； <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 太小会降低区分能力，太大则会过度占满 bit 位。固定 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 时，常见的近似最优选择是</p>
<span displaypfx="" class="mathjax-container">\[k\approx \frac{m}{n}\ln 2\]</span>
<p>直觉上，Bloom Filter 像一排共享的指示灯。每来一个元素，就按亮若干盏灯；查询时，只要对应灯中有一盏没亮，就可以确认它从未出现过。若全部亮着，也只能说明“这些灯曾被某些元素点亮过”，却不能保证就是当前这个元素点亮的。</p>
<p>Bloom Filter 最适合用于“先快速排除绝大多数不存在项，再把少量可疑项交给精确结构复核”的场景。例如缓存系统可先判断某个 key 是否可能在缓存中，若 Bloom Filter 直接给出“不在”，就可以避免无意义回源；LSM-Tree 存储系统可用它判断某个键是否可能存在于某个 SSTable；爬虫去重、黑名单预过滤、向量检索候选预筛都大量使用这一思想。</p>
<p>Bloom Filter 的边界也很明确。第一，它不保存原始元素，因此不能枚举集合内容，也不能像哈希表那样返回关联值。第二，标准 Bloom Filter 不支持安全删除，因为把某个 bit 清零可能误伤其他元素留下的痕迹；若确实需要删除，通常要改用计数 Bloom Filter（Counting Bloom Filter）。第三，当假阳性代价非常高、系统需要完全精确的成员判断时，应优先使用哈希表、B 树或其他精确索引结构。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/bloom.jpg"><img class="alignnone size-full wp-image-41339" src="https://blog.gmem.cc/wp-content/uploads/2026/03/bloom.jpg" alt="bloom" width="1024" height="559" /></a></p>
<div class="blog_h3"><span class="graybg">树（Tree）</span></div>
<p>树（Tree）处理的是“层次结构”和“递归划分”问题。树中的节点之间具有父子关系，除了根节点（Root）外，每个节点都有唯一父节点。它天然适合表达目录层级、决策分裂、区间划分、优先级组织与前缀共享。</p>
<p>树之所以重要，在于它把原本线性的搜索空间组织成递归结构，使很多操作能通过“向左还是向右”“进入哪个子树”逐步缩小问题规模。若每次都能把候选空间缩小到原来的一半，复杂度就会从线性级下降到对数级。</p>
<div class="blog_h4"><span class="graybg">二叉树与遍历</span></div>
<p>二叉树（Binary Tree）规定每个节点至多有两个孩子。前序遍历（Preorder）、中序遍历（Inorder）、后序遍历（Postorder）和层序遍历（Level-order）分别对应不同的信息读取顺序：前序适合序列化结构，中序适合读取二叉搜索树中的有序键，后序适合先处理子问题再合并，层序适合按深度观察整体形状。</p>
<div class="blog_h4"><span class="graybg">二叉搜索树与平衡树</span></div>
<p>二叉搜索树（Binary Search Tree, BST）在每个节点上保持“左子树键值更小、右子树键值更大”的顺序约束，因此查找、插入和删除都可以沿着比较路径进行。若树高度为 <span displaypfx="inline-" class="mathjax-container">\(h\)</span>，这些操作的复杂度一般与 <span displaypfx="inline-" class="mathjax-container">\(O(h)\)</span> 成正比。</p>
<p>问题在于普通 BST 在极端情况下会退化成链表，此时 <span displaypfx="inline-" class="mathjax-container">\(h=n\)</span>。平衡树（Balanced Tree）如 AVL 树、红黑树（Red-Black Tree）通过旋转（Rotation）维护高度受控，使 <span displaypfx="inline-" class="mathjax-container">\(h=O(\log n)\)</span>，从而把查找、插入和删除稳定在对数复杂度。数据库索引和有序映射容器大量依赖这一思想。</p>
<div class="blog_h4"><span class="graybg">堆与优先队列</span></div>
<p>堆（Heap）维护的是局部顺序，而不是整棵树的全局有序性：在最小堆（Min-Heap）中，每个父节点都不大于子节点，因此根节点始终是全局最小值；最大堆（Max-Heap）则相反。它通常用数组实现，父子下标关系可以直接计算。</p>
<p>堆最适合实现优先队列（Priority Queue）：每次都要快速取出当前最重要、最小或最大的元素时，插入和弹出都只需 <span displaypfx="inline-" class="mathjax-container">\(O(\log n)\)</span>。Dijkstra、A* 搜索、任务调度、Top-K 维护和流式中位数都大量依赖优先队列。</p>
<div class="blog_h4"><span class="graybg">Trie 与前缀结构</span></div>
<p>Trie 树（Prefix Tree）把字符串按前缀共享组织起来。若插入单词集合 <span displaypfx="inline-" class="mathjax-container">\(\{w_1,\dots,w_m\}\)</span>，公共前缀只存一次，因此“是否存在某个前缀”“以某前缀开头的词有多少”都可以沿字符路径直接完成。</p>
<p>Trie 特别适合词典匹配、自动补全、敏感词过滤和子词切分。它牺牲了一部分空间，换来按字符长度而非按词典规模进行搜索的能力。</p>
<div class="blog_h3"><span class="graybg">图（Graph）</span></div>
<p>图（Graph）处理的是最一般的关系结构。若顶点集合为 <span displaypfx="inline-" class="mathjax-container">\(V\)</span>，边集合为 <span displaypfx="inline-" class="mathjax-container">\(E\)</span>，则图可写成 <span displaypfx="inline-" class="mathjax-container">\(G=(V,E)\)</span>。树本质上是图的一个特殊子类，但图允许环、允许多条连接、允许方向和权重，因此能表达社交关系、知识链接、网页跳转、道路网络、依赖图与神经网络计算图。</p>
<p>图的常见表示方式有邻接矩阵（Adjacency Matrix）和邻接表（Adjacency List）。前者适合稠密图，能 <span displaypfx="inline-" class="mathjax-container">\(O(1)\)</span> 判断两点是否相连；后者适合稀疏图，空间复杂度更低，遍历邻居更高效。</p>
<div class="blog_h4"><span class="graybg">BFS 与 DFS</span></div>
<p>广度优先搜索（BFS）与深度优先搜索（DFS）是图遍历的两种基本组织方式。BFS 使用队列按层推进，适合无权最短路、层次扩展与最少步数问题；DFS 使用递归或显式栈沿一条路径尽量走深，适合回溯、环检测、拓扑排序、强连通分量与树形动态规划。</p>
<p>对邻接表表示的图，两者的时间复杂度通常都是 <span displaypfx="inline-" class="mathjax-container">\(O(|V|+|E|)\)</span>。区别不在渐近复杂度，而在访问顺序：BFS 保证按距离层层扩展，DFS 更擅长描述“先深入、后回退”的结构性问题。</p>
<div class="blog_h4"><span class="graybg">最短路径</span></div>
<p>最短路径（Shortest Path）问题处理的是：从起点到终点，总代价最小的路径是什么。若图无权，BFS 就能得到边数最少的路径；若边权非负，常用 Dijkstra 算法。它每次从优先队列中取出当前距离估计最小的顶点，并尝试松弛（Relax）相邻边。</p>
<p>Dijkstra 的核心更新为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}[v]=\min\big(\mathrm{dist}[v],\mathrm{dist}[u]+w(u,v)\big)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}[u]\)</span> 是当前已知的从源点到 <span displaypfx="inline-" class="mathjax-container">\(u\)</span> 的最短距离估计， <span displaypfx="inline-" class="mathjax-container">\(w(u,v)\)</span> 是边权。这个公式表达的是最短路的本质：若“先到 <span displaypfx="inline-" class="mathjax-container">\(u\)</span>，再走到 <span displaypfx="inline-" class="mathjax-container">\(v\)</span>”更便宜，就更新对 <span displaypfx="inline-" class="mathjax-container">\(v\)</span> 的距离认知。</p>
<div class="blog_h4"><span class="graybg">拓扑排序</span></div>
<p>拓扑排序（Topological Sort）处理的是有向无环图（Directed Acyclic Graph, DAG）中的依赖顺序。若边 <span displaypfx="inline-" class="mathjax-container">\(u\to v\)</span> 表示“<span displaypfx="inline-" class="mathjax-container">\(u\)</span> 必须先于 <span displaypfx="inline-" class="mathjax-container">\(v\)</span>”，那么拓扑序就是一种满足全部先后约束的线性排列。</p>
<p>课程先修关系、编译依赖、工作流调度、神经网络计算图执行次序，本质上都属于这一问题。拓扑排序的价值不只是“排出一个顺序”，而是把依赖图转成一条能够实际执行的流水线。</p>
<div class="blog_h4"><span class="graybg">最小生成树（Minimum Spanning Tree, MST）</span></div>
<p>最小生成树（Minimum Spanning Tree, MST）处理的是这样的问题：给定一个连通无向带权图 <span displaypfx="inline-" class="mathjax-container">\(G=(V,E)\)</span>，需要从边集合 <span displaypfx="inline-" class="mathjax-container">\(E\)</span> 中选出一部分边，把所有顶点连成一个整体，同时不产生环，并使总权重最小。若把所有生成树的集合记为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{T}(G)\)</span>，则标准形式可以写成</p>
<span displaypfx="" class="mathjax-container">\[T^*=\arg\min_{T\in \mathcal{T}(G)}\sum_{e\in T} w(e)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(T^*\)</span> 是最优生成树；<span displaypfx="inline-" class="mathjax-container">\(w(e)\)</span> 是边 <span displaypfx="inline-" class="mathjax-container">\(e\)</span> 的权重；<span displaypfx="inline-" class="mathjax-container">\(\sum_{e\in T} w(e)\)</span> 表示树中全部边的总代价。这里的“生成树”有三个同时成立的约束：第一， <span displaypfx="inline-" class="mathjax-container">\(T\subseteq E\)</span>；第二，图在边集 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 下必须连通；第三， <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 不能含环，因此边数必然满足 <span displaypfx="inline-" class="mathjax-container">\(|T|=|V|-1\)</span>。</p>
<p>这个定义明确了 MST 不是“找一棵看上去便宜的树”，而是在<span style="background-color: #c0c0c0;">所有能够覆盖全部顶点的无环连通方案</span>中做全局最小化。只强调“连通”会多出冗余边，只强调“边权小”又可能导致图不连通；MST 同时满足这两个条件。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mst-graph.png"><img class="alignnone size-full wp-image-41335" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mst-graph.png" alt="mst-graph" width="6242" height="2905" /></a></p>
<p>直觉上，可以把 MST 理解成“以最低总造价把一组城市接通，但不修多余的回路”。如果形成了环，说明这条网络中至少有一条边是重复支出；如果某些城市没有接入，说明方案根本不可用。MST 就是在“全覆盖”和“最低成本”之间取得最紧的平衡。</p>
<p>MST 成立的核心理论基础是<span style="background-color: #c0c0c0;">割性质（Cut Property）</span>：把顶点集切成两个不相交部分后，跨越这个切分的最小权边，一定存在于某棵最小生成树中。这个性质的含义是：局部最便宜的“安全边（Safe Edge）”可以被逐步加入，而不会破坏全局最优性。Prim 与 Kruskal 虽然组织方式不同，但本质上都在不断选择这样的安全边。</p>
<p>Prim 算法的思路是“从一个起点向外生长一棵树”。设当前已经纳入树中的顶点集合为 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，则 Prim 每一步都在所有满足 <span displaypfx="inline-" class="mathjax-container">\(u\in S,\ v\notin S\)</span> 的边中，选择权重最小的一条，把新顶点接入当前树。这个过程像不断把新城市接入已经建好的主干网，因此特别适合用优先队列维护“当前边界上最便宜的边”。若图用邻接表存储并配合二叉堆实现优先队列，时间复杂度通常为 <span displaypfx="inline-" class="mathjax-container">\(O(|E|\log |V|)\)</span>。</p>
<p>Kruskal 算法的思路是“按全图范围从便宜到昂贵依次选边”。它先对所有边按权重升序排序，然后从小到大扫描：若当前边连接的是两个不同连通块，就把它加入结果；若会在当前结构中形成环，就跳过。为了高效判断“两个端点是否已经连通”，Kruskal 通常配合并查集（Disjoint Set Union, DSU）。排序代价主导总复杂度，因此复杂度通常写成 <span displaypfx="inline-" class="mathjax-container">\(O(|E|\log |E|)\)</span>，与 <span displaypfx="inline-" class="mathjax-container">\(O(|E|\log |V|)\)</span> 在数量级上接近。</p>
<p>两种算法解决的是同一个优化问题，但适合的工程语境不同。Prim 更像“从局部网络不断扩张”，适合稠密图或从某个核心节点逐步向外建设的场景；Kruskal 更像“全局看所有候选边，再逐一合并连通块”，在边集天然可排序、图较稀疏时实现尤其直接。</p>
<p>一个最小例子可以把公式和过程连起来。设四个顶点 <span displaypfx="inline-" class="mathjax-container">\(A,B,C,D\)</span>，边权为： <span displaypfx="inline-" class="mathjax-container">\(w(A,B)=1\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(B,C)=2\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(A,C)=4\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(B,D)=3\)</span>， <span displaypfx="inline-" class="mathjax-container">\(w(C,D)=5\)</span>。Kruskal 会先按边权排序： <span displaypfx="inline-" class="mathjax-container">\((A,B),(B,C),(B,D),(A,C),(C,D)\)</span>。前 3 条边分别把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 连起来，此时已经得到 <span displaypfx="inline-" class="mathjax-container">\(|V|-1=3\)</span> 条边，且图连通无环，于是生成树为</p>
<span displaypfx="" class="mathjax-container">\[T=\{(A,B),(B,C),(B,D)\}\]</span>
<p>其总代价为</p>
<span displaypfx="" class="mathjax-container">\[\sum_{e\in T}w(e)=1+2+3=6\]</span>
<p>若改选边集 <span displaypfx="inline-" class="mathjax-container">\(\{(A,B),(A,C),(B,D)\}\)</span>，总代价是 <span displaypfx="inline-" class="mathjax-container">\(1+4+3=8\)</span>；若再加入 <span displaypfx="inline-" class="mathjax-container">\((B,C)\)</span>，虽然成本局部看不高，但边数会超过 <span displaypfx="inline-" class="mathjax-container">\(|V|-1\)</span> 并形成环，因此不再是树。这个例子把“最低成本”“连通”“无环”三项约束如何同时生效展示得很清楚。</p>
<p>MST 常见于网络布线、电力传输、骨架路网设计、图像分割、聚类和图压缩。层次聚类中的单链接（Single Linkage）就可以通过图的最小生成树来理解：先把点看成顶点，把样本间距离看成边权，再在 MST 上剪断最长的若干条边，就得到若干连通簇。这也是为什么 MST 不只是图论题型，而是很多数据分析与机器学习方法的底层结构。</p>
<p>MST 也有明确边界。它只适用于<span style="background-color: #c0c0c0;">无向、连通、带权</span>图上的“全连通最低总成本”问题；若任务要求的是“从源点到其余点的最短路”，应使用最短路径算法；若图有方向，目标就不再是普通 MST，而会进入最小树形图（Minimum Arborescence）等更复杂的问题。</p>
<div class="blog_h2"><span class="graybg">动态规划（Dynamic Programming, DP）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>动态规划（Dynamic Programming, DP）处理的是这样一类问题：目标是求一个全局最优值、最优路径，或所有路径的总和，但如果直接把所有可能性全部枚举出来，计算量会迅速爆炸。它常见于序列决策、路径规划、字符串匹配、图搜索，以及隐马尔可夫模型（HMM）、条件随机场（CRF）这类结构化预测模型。</p>
<p>这类问题通常有两个共同特征。第一，<span style="background-color: #c0c0c0;">重叠子问题（Overlapping Subproblems）</span>：同一个中间子问题会被反复计算。第二，<span style="background-color: #c0c0c0;">最优子结构（Optimal Substructure）</span>：大问题的最优解可以由小问题的最优解递推得到。例如，在长度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 的序列上，若每一步有 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|\)</span> 个可能状态，直接枚举所有状态路径往往需要考虑 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|^T\)</span> 条候选路径；当 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 稍大时，这种暴力方法几乎不可用。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>动态规划的核心在于：<span style="background-color: #c0c0c0;">先定义能够代表子问题的状态，再写出状态之间的递推关系，并把已经算过的结果缓存下来复用</span>。因此，它本质上是一种计算组织方式，而不是某个固定公式。</p>
<p>一个直观比喻是出差换乘。设想要从起点出发，经过很多站点，最终到达目的地。暴力法会把“到达每一站的所有走法”全部记下来；动态规划不会这样做。它只会为每个中间站保留一份最有价值的摘要，例如“到达这个站的最低成本”或“到达这个站的最大得分”。当继续前往下一站时，系统只需要查这份账本，而不必回头展开所有历史路径。</p>
<p>因此，动态规划通常包含四个步骤：定义状态、定义边界条件、写出转移方程、确定计算顺序。状态定义决定“中间结果要存什么”；边界条件决定“第一步从哪里开始”；转移方程决定“当前结果如何从更小问题得到”；计算顺序则保证所有依赖项在使用前已经计算完毕。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/dp.jpg"><img class="alignnone size-full wp-image-41269" src="https://blog.gmem.cc/wp-content/uploads/2026/03/dp.jpg" alt="dp" width="1408" height="768" /></a></p>
<div class="blog_h3"><span class="graybg">为什么局部次优前缀不会漏掉全局最优</span></div>
<p>动态规划能够丢弃大量“暂时看起来不够好”的前缀路径，前提是<span style="background-color: #c0c0c0;">状态（State）已经完整刻画了未来决策所需的全部信息</span>。一旦这个条件成立，到达同一状态的两条前缀路径，未来能够接上的可行后缀集合完全相同，因此只需要保留其中更优的那一条。</p>
<p>设两条前缀路径都到达同一状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span>，其当前累计代价分别为 <span displaypfx="inline-" class="mathjax-container">\(f_1(s)\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(f_2(s)\)</span>，且 <span displaypfx="inline-" class="mathjax-container">\(f_1(s)\le f_2(s)\)</span>。若从状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 出发，后续任意可行决策产生的附加代价记为 <span displaypfx="inline-" class="mathjax-container">\(g(s)\)</span>，并且这个附加代价只由当前状态决定，而不再依赖此前的完整历史，则有</p>
<span displaypfx="" class="mathjax-container">\[f_1(s)+g(s)\le f_2(s)+g(s)\]</span>
<p>这个不等式表明：在同一状态上，较差的前缀会被较优前缀完全支配（Dominated）。无论后面接哪一段后缀路径，较差前缀都不可能反超。因此 Bellman 最优性原理允许动态规划只保留“到达该状态的最优值”，而不必保留全部历史路径。</p>
<p>所谓“一个当前次优的路径，后来却通向全局最优”，本质上对应另一种情形：这条路径与当前更优路径虽然看起来到达了同一个位置，但它们对未来的可行动作并不相同。此时它们实际上并不属于同一个状态，而是状态定义缺失了关键信息。</p>
<p>一个典型例子是带资源约束的路径规划。若状态只写成当前位置 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span>，那么两条到达同一格子的路径会被合并；但若其中一条还保留一次传送机会，另一条已经把传送用掉，则它们未来的决策空间显然不同。正确的状态应扩展为 <span displaypfx="inline-" class="mathjax-container">\((i,j,\mathrm{used})\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\((i,j,\mathrm{fuel})\)</span> 这类更完整的形式。只有在状态把“剩余资源、上一步动作、已使用预算、是否持仓”等会影响未来的因素都编码进去后，动态规划的剪枝才是安全的。</p>
<p>因此，动态规划处理“局部次优可能导向全局最优”的方式，不是保留所有看起来有潜力的路径，而是通过<span style="background-color: #c0c0c0;">正确设计状态，使真正会影响未来的差异体现在不同状态上</span>。同一状态内部只保留最优前缀；不同状态之间分别递推。动态规划的正确性，最终依赖的正是这一点：未来只依赖当前状态，而不依赖通向当前状态的完整历史。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若记 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 为阶段或时间步， <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 为当前状态，一个非常典型的动态规划写法是：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[t,s]=\max_{s'\in \mathrm{Prev}(s)}\left(\mathrm{DP}[t-1,s']+\mathrm{score}(s',s,t)\right)\]</span>
<p>这条式子表达的是：要得到“第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步处于状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 时的最优值”，不必重新枚举所有完整路径，而是只需查看所有能够转移到 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 的前驱状态 <span displaypfx="inline-" class="mathjax-container">\(s'\)</span>，并在它们已有的最优值基础上，加上这一步的局部得分，再从中取最大。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[t,s]\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步、状态为 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 的最优子问题值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Prev}(s)\)</span>：所有可以转移到状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 的前驱状态集合。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{score}(s',s,t)\)</span>：从 <span displaypfx="inline-" class="mathjax-container">\(s'\)</span> 转移到 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 时，在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步新增的局部得分或代价。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\max\)</span>：表示当前任务要找“最好的一条路径”。若任务目标是最小代价，则可改为 <span displaypfx="inline-" class="mathjax-container">\(\min\)</span>；若任务目标是把所有路径概率加总，则可改为 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span>。</li>
</ul>
<p>边界条件通常写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[1,s]=\mathrm{init}(s)+\mathrm{local}(s,1)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{init}(s)\)</span> 表示序列从状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 开始的初始代价或初始分数， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{local}(s,1)\)</span> 表示第一步在该状态产生的局部贡献。没有这个起点，后续递推就无从展开。</p>
<p>动态规划真正带来的收益来自复杂度压缩。以一阶序列模型为例，若每一步有 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|\)</span> 个候选状态、总长度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span>，暴力枚举往往需要考虑 <span displaypfx="inline-" class="mathjax-container">\(|\mathcal{S}|^T\)</span> 条完整路径；而若采用“时间步 + 当前状态”的动态规划状态定义，则通常只需在每个时间步枚举所有前驱状态，计算复杂度可以降为 <span displaypfx="inline-" class="mathjax-container">\(O(T|\mathcal{S}|^2)\)</span>。这种从指数级到多项式级的下降，正是动态规划在序列模型中不可替代的原因。</p>
<p>若任务不仅要求最优值，还要求恢复最优路径，则通常还会额外保存“当前最优值来自哪个前驱状态”的回溯信息（Backpointer）。这意味着动态规划不仅能回答“最优值是多少”，还能回答“这条最优路径具体怎么走”。</p>
<p>更重要的是，动态规划并不只对应一种运算。对于最优路径问题，递推中的核心运算往往是 <span displaypfx="inline-" class="mathjax-container">\(\max\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\min\)</span>；对于总概率、配分函数这类问题，核心运算则是 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span>。这也是为什么 HMM 的维特比算法、前向算法，以及 CRF 的前向后向算法，虽然目标不同，但都属于动态规划。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在 HMM 中，动态规划最典型地体现在两类问题上。第一类是维特比算法（Viterbi Algorithm）：它要求“给定观测序列后，哪一条隐藏状态路径最可能”，因此递推中的核心运算是 <span displaypfx="inline-" class="mathjax-container">\(\max\)</span>。第二类是前向算法（Forward Algorithm）：它要求“所有隐藏状态路径合起来，总概率是多少”，因此递推中的核心运算是 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span>。两者使用的是同一张状态网格，只是“每一步如何聚合前驱信息”不同。</p>
<p>在 CRF 中，动态规划同样是核心计算工具。训练时，需要对所有可能标签路径做归一化，这对应配分函数（Partition Function）的计算；解码时，需要找得分最高的那条标签路径，这对应最优路径搜索。在线性链 CRF 中，这两件事都可以通过“时间步 + 当前标签”的动态规划状态来高效完成，否则若直接枚举所有标签序列，计算量会随序列长度呈指数增长。</p>
<p>因此，在机器学习语境里，动态规划可以概括为：<span style="background-color: #c0c0c0;">把原本必须整体枚举的结构化问题，改写成一系列局部状态上的递推计算，并通过缓存中间结果把重复计算消掉</span>。一旦看到“序列路径很多、局部决策可递推、同类子问题会重复出现”这三个信号，通常就应该优先考虑动态规划。</p>
<div class="blog_h4"><span class="graybg">例子 1：编辑距离（Edit Distance）</span></div>
<p>设字符串 <span displaypfx="inline-" class="mathjax-container">\(A=a_1a_2\dots a_m\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(B=b_1b_2\dots b_n\)</span>。编辑距离要回答的问题是：至少经过多少次插入、删除、替换，才能把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>。若直接枚举所有编辑序列，可能性会指数增长；但若定义 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[i,j]\)</span> 表示“把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的前 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个字符变成 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 的前 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个字符所需的最小编辑次数”，问题就能递推解决。</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[i,j]=\min\begin{cases}\mathrm{DP}[i-1,j]+1\\ \mathrm{DP}[i,j-1]+1\\ \mathrm{DP}[i-1,j-1]+\mathbf{1}(a_i\ne b_j)\end{cases}\]</span>
<p>这里三项分别对应：删除 <span displaypfx="inline-" class="mathjax-container">\(a_i\)</span>、插入 <span displaypfx="inline-" class="mathjax-container">\(b_j\)</span>、或把 <span displaypfx="inline-" class="mathjax-container">\(a_i\)</span> 替换成 <span displaypfx="inline-" class="mathjax-container">\(b_j\)</span>（若本来相同，则替换代价为 0）。边界条件是 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[0,j]=j\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[i,0]=i\)</span>，因为空串变成长度为 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的串需要做 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 次插入，反之需要做 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 次删除。这个例子非常典型地体现了动态规划：状态是前缀长度，转移是三种编辑操作，目标是求最小总代价。</p>
<div class="blog_h4"><span class="graybg">例子 2：网格最短路径（Grid Shortest Path）</span></div>
<p>设一个 <span displaypfx="inline-" class="mathjax-container">\(m\times n\)</span> 网格，每个格子 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 都有进入代价 <span displaypfx="inline-" class="mathjax-container">\(w_{i,j}\)</span>，只能向右或向下移动。问题是：从左上角走到右下角的最小总代价是多少。若定义 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{DP}[i,j]\)</span> 表示“到达格子 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 的最小总代价”，则递推很直接：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{DP}[i,j]=w_{i,j}+\min\big(\mathrm{DP}[i-1,j],\mathrm{DP}[i,j-1]\big)\]</span>
<p>因为到达 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 只有两种可能：从上方 <span displaypfx="inline-" class="mathjax-container">\((i-1,j)\)</span> 走下来，或从左侧 <span displaypfx="inline-" class="mathjax-container">\((i,j-1)\)</span> 走过来。边界条件是第一行与第一列只能沿单一路径累计。这个例子说明，动态规划并不局限于字符串或序列模型；只要问题具有“局部来源有限、全局目标可递推”的结构，就可以用同样的思路求解。</p>
<div class="blog_h2"><span class="graybg">贪心算法（Greedy Algorithm）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>贪心算法（Greedy Algorithm）处理的是这样一类问题：希望快速构造一个全局可行解，并且每一步都只做当前看来最优的局部选择，而不回头修改已经作出的决定。它广泛出现在排序、调度、压缩、近似优化，以及许多机器学习训练与推断流程中。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>贪心的核心假设是：<span style="background-color: #c0c0c0;">当前最好的局部选择，能够导向全局最优，或至少导向足够好的近似解</span>。它像走山路时每一步都先选眼前最高、最稳的落脚点，而不是先把整座山的所有路径都完全规划出来。贪心的优势是快、简单、容易实现；风险是局部最优未必等于全局最优。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若记第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步可选动作集合为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{A}_t\)</span>，一个抽象的贪心选择可写为：</p>
<span displaypfx="" class="mathjax-container">\[a_t^*=\arg\max_{a\in\mathcal{A}_t}\ \mathrm{score}(a\mid \text{current state})\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{score}(a\mid \text{current state})\)</span> 是当前状态下动作 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 的局部收益；<span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 表示从所有可选动作里挑出得分最高的那个。贪心算法关心的是“眼下哪一步最好”，而不是“未来所有步骤联合起来后哪条完整路径最好”。</p>
<p>因此，贪心方法是否正确，取决于问题本身是否满足贪心选择性质（Greedy-choice Property）。如果这个性质成立，局部最优就能拼成全局最优；如果不成立，贪心通常只能作为启发式方法或近似算法。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>决策树训练就是一个典型例子。每个节点都不会提前规划整棵树的全局最优结构，而是只在当前节点上选择信息增益、基尼下降或误差下降最大的切分。这个过程本质上就是贪心：<span style="background-color: #c0c0c0;">每一步都先把当前最值得切的地方切开</span>。它训练快、解释性强，但也正因为是局部选择，单棵树通常不是全局最优树结构。</p>
<div class="blog_h2"><span class="graybg">分治算法（Divide and Conquer）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>分治算法（Divide and Conquer）处理的是“大问题可以被拆成若干个同结构小问题”的场景。它广泛出现在排序、搜索、矩阵运算、索引构建，以及大规模数据处理与并行计算中。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>分治的思想可以概括为三步：<span style="background-color: #c0c0c0;">分解（Divide）— 递归求解（Conquer）— 合并（Combine）</span>。它像整理一大堆文档时，先按主题拆成若干小堆，再分别处理，最后再合并成有序结果。与动态规划不同，分治更强调“子问题相互独立”，而不是“子问题结果需要反复复用”。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>分治算法的时间复杂度常写成递推式：</p>
<span displaypfx="" class="mathjax-container">\[T(n)=aT\left(\frac{n}{b}\right)+f(n)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是问题规模；<span displaypfx="inline-" class="mathjax-container">\(a\)</span> 表示被拆成多少个子问题；<span displaypfx="inline-" class="mathjax-container">\(n/b\)</span> 是每个子问题的规模；<span displaypfx="inline-" class="mathjax-container">\(f(n)\)</span> 是“分解 + 合并”本身的额外代价。这个式子不告诉我们具体怎么做，但它准确描述了分治算法的结构骨架。</p>
<p>例如，归并排序（Merge Sort）把长度为 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 的数组分成两个规模约为 <span displaypfx="inline-" class="mathjax-container">\(n/2\)</span> 的子数组，递归排好序后再线性合并，因此它的复杂度递推就是 <span displaypfx="inline-" class="mathjax-container">\(T(n)=2T(n/2)+O(n)\)</span>。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在机器学习工程中，分治思想常见于大规模近邻索引构建。例如构建 kd-tree 时，算法会按某个维度把样本集递归切成两半，再在左右子集上继续构树。这样得到的层次化空间划分，能显著加速后续的近邻搜索。它的本质并不是“学习一个模型”，而是通过递归拆分把原本需要全表扫描的搜索过程组织得更高效。</p>
<div class="blog_h2"><span class="graybg">图搜索与最短路径</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>许多软件与机器学习问题都可以抽象成图（Graph）：节点（Node）表示状态、样本、词、网页或知识实体，边（Edge）表示转移、相似性、依赖关系或可达关系。图搜索与最短路径算法要回答的问题是：如何从起点高效找到目标节点，或找到总代价最小的一条路径。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>图搜索的核心是“沿着边扩展状态空间，但尽量避免无意义的重复探索”。无权图最短路径常用广度优先搜索（BFS），因为它按层扩展，第一次到达目标通常就是步数最少的路径；带非负权图常用 Dijkstra，因为它总是优先扩展当前总代价最小的候选节点。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>带权最短路径算法里的基本更新步骤通常写成“松弛（Relaxation）”：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}(v)=\min\big(\mathrm{dist}(v),\ \mathrm{dist}(u)+w(u,v)\big)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}(u)\)</span> 是当前已知从起点到节点 <span displaypfx="inline-" class="mathjax-container">\(u\)</span> 的最小代价， <span displaypfx="inline-" class="mathjax-container">\(w(u,v)\)</span> 是边 <span displaypfx="inline-" class="mathjax-container">\(u\rightarrow v\)</span> 的权重， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}(u)+w(u,v)\)</span> 则是“先到 <span displaypfx="inline-" class="mathjax-container">\(u\)</span> 再走到 <span displaypfx="inline-" class="mathjax-container">\(v\)</span>”这条新候选路径的总代价。若它比当前记录的 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}(v)\)</span> 更小，就更新。</p>
<p>这个式子看起来和动态规划很像，原因是二者都在做“由已知子结果递推新结果”。区别在于：图搜索更强调如何选择下一个要扩展的节点，以及如何在一般图结构中避免重复访问。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在语音识别、机器翻译和图搜索推断中，解码过程经常会把候选状态组织成图或格（Lattice）。此时，寻找最优输出序列本质上就是图上的路径搜索问题。很多动态规划解码器也可以从“图上最优路径”的角度理解，因此图搜索是连接通用软件算法与结构化机器学习推断的重要桥梁。</p>
<div class="blog_h2"><span class="graybg">二分查找（Binary Search）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>二分查找（Binary Search）处理的是“搜索空间有序，或可行性判断具有单调性”的问题。它不仅用于有序数组查找，也广泛用于阈值搜索、参数调优、数值逼近和工程系统中的边界定位。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>二分查找的核心是：<span style="background-color: #c0c0c0;">每次利用单调性砍掉一半搜索空间</span>。它像猜数字游戏：如果知道答案一定在某个区间里，而且中点左侧和右侧满足不同性质，那么每问一次都能把候选范围减半。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若当前搜索区间为 <span displaypfx="inline-" class="mathjax-container">\([l,r]\)</span>，中点通常取：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{mid}=\left\lfloor\frac{l+r}{2}\right\rfloor\]</span>
<p>接着依据单调判定函数 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{check}(\mathrm{mid})\)</span> 缩小区间：</p>
<ul>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{check}(\mathrm{mid})\)</span> 为真，说明答案在左半边或恰好是中点，则令 <span displaypfx="inline-" class="mathjax-container">\(r=\mathrm{mid}\)</span>。</li>
<li>若为假，说明答案在右半边，则令 <span displaypfx="inline-" class="mathjax-container">\(l=\mathrm{mid}+1\)</span>。</li>
</ul>
<p>算法正确性的关键不在于公式本身，而在于维护区间不变式（Invariant）：在每一步更新后，真正的答案仍然留在当前区间中。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在机器学习里，二分查找常用于阈值定位。例如，当需要找到“使召回率至少达到某个目标值的最小分类阈值”时，只要阈值越大召回率越低这一单调关系成立，就可以在阈值区间上做二分查找，而不必逐点穷举。类似地，很多数值求根、超参数边界搜索、分位数定位问题也都可写成二分框架。</p>
<div class="blog_h2"><span class="graybg">随机采样（Random Sampling）</span></div>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>随机采样（Random Sampling）处理的是这样一类问题：总体太大、精确计算太贵，或者目标本身就是概率性的，因此只能通过抽样近似整体行为。它是统计学习、蒙特卡洛估计、bootstrap、自助重采样、mini-batch 训练和负采样的共同基础。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>随机采样的核心是：<span style="background-color: #c0c0c0;">不必每次都看完整总体，而是通过足够有代表性的随机子样本估计总体性质</span>。它像民意调查：不可能每天逐个询问所有人，但若抽样方式合理，少量样本也能给出相对稳定的总体估计。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>若目标是估计随机变量 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 下某个函数 <span displaypfx="inline-" class="mathjax-container">\(f(X)\)</span> 的期望 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[f(X)]\)</span>，最常见的蒙特卡洛估计写为：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\mu}=\frac{1}{n}\sum_{i=1}^{n} f(x_i),\qquad x_i\sim p(x)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x_i\sim p(x)\)</span> 表示样本 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 是按分布 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span> 随机抽到的； <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是样本数； <span displaypfx="inline-" class="mathjax-container">\(\hat{\mu}\)</span> 是用样本均值近似真实期望的估计量。样本越多，估计通常越稳定，但代价也越高。</p>
<p>这一思想在机器学习里非常普遍：SGD 不是每次都在全量数据上算梯度，而是用 mini-batch 的样本均值近似全数据梯度；negative sampling 不是对所有负类都求和，而是随机抽一小部分负样本近似完整目标。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>bootstrap 是一个很典型的例子。随机森林训练时，会对原始训练集做有放回采样，得到多份不同的 bootstrap 子集，再分别训练多棵树。这里真正起作用的不是“树”本身，而是随机采样制造了多个略有差异的数据视角，从而让集成后的模型更稳。</p>
<div class="blog_h1"><span class="graybg">机器学习基础概念</span></div>
<p>机器学习基础概念（Machine Learning Foundations）回答四类核心问题：数据从哪里来、模型在学什么、模型为什么能泛化、结果该如何评价。把这些问题分开看，会比死记算法名称更有效：学习范式决定监督信号来自哪里，假设空间与归纳偏置决定模型愿意相信什么，数据集工程决定模型实际看到了什么，模型评估决定这些学习结果是否真的能迁移到未见样本。</p>
<div class="blog_h2"><span class="graybg">假设/目标/代价/损失</span></div>
<p>这四个词描述的是同一条“训练=优化”的概念链，但位于不同层级。把层级理清后，公式与实现会自然对齐：模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 先给出预测，再用损失函数把预测变成数值误差，最后把误差在数据集上汇总成代价函数，并加入正则/约束得到最终的目标函数。</p>
<div class="blog_h3"><span class="graybg">假设函数（Hypothesis Function）</span></div>
<p>假设函数（Hypothesis Function）也常被直接称为模型（Model）或预测函数（Predictor），记作 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span>。它回答的问题是：<span style="background-color: #c0c0c0;">给定输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，模型输出什么</span>。参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 决定这条映射的具体形状。</p>
<p>线性回归（Linear Regression）的假设函数是最经典的例子：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=f_\theta(x)=\mathbf{w}^\top x+b,\quad \theta=(\mathbf{w},b)\]</span>
<div class="blog_h3"><span class="graybg">目标函数（Objective Function）</span></div>
<p>目标函数（Objective Function）记作 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)\)</span>，是优化器真正要优化的函数。工程上最常见、也最清晰的写法是：<span style="background-color: #c0c0c0;">目标函数 = 代价函数 + 正则化项</span>（没有正则化时可视为正则项为 0，因此 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)=L(\theta)\)</span>）。</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=L(\theta)+\lambda\,\Omega(\theta)\]</span>
<p>在线性回归里，若用 L2 正则（Ridge / Weight Decay），常见目标函数可以写成：</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=L(\theta)+\lambda\|\mathbf{w}\|_2^2\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 展开后，就是：</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=\frac{1}{N}\sum_{i=1}^{N}(\mathbf{w}^\top x_i+b-y_i)^2+\lambda\|\mathbf{w}\|_2^2\]</span>
<div class="blog_h3"><span class="graybg">代价/成本函数（Cost Function）</span></div>
<p>代价函数/成本函数（Cost Function）记作 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span>，通常指把样本损失在训练集上做平均或求和后的整体量，也就是经验风险（Empirical Risk）。不少教材会把它直接称为训练损失（training loss），并且在不引起歧义时把它与目标函数混用。</p>
<p>在线性回归里，常用“均方误差的平均”作为代价函数：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\ell_i(\theta)\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\ell_i(\theta)\)</span> 取为平方误差后，等价写法是：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}(\mathbf{w}^\top x_i+b-y_i)^2\]</span>
<div class="blog_h3"><span class="graybg">损失函数（Loss Function）</span></div>
<p>损失函数（Loss Function）记作 <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span>，通常定义在单个样本上，把“预测与目标的差距”映射为一个标量。它回答的问题是：<span style="background-color: #c0c0c0;">这一条样本我错了多少</span>。</p>
<p>在线性回归里，最常见的单样本损失是平方误差：</p>
<span displaypfx="" class="mathjax-container">\[\ell_i(\theta)=\ell(\hat y_i,y_i)=(\hat y_i-y_i)^2,\quad \hat y_i=f_\theta(x_i)\]</span>
<div class="blog_h2"><span class="graybg">假设空间、容量与归纳偏置</span></div>
<p>同一份训练数据，之所以会被不同模型学出完全不同的规律，根源在于每个模型都自带一套“允许学什么、不允许学什么”的结构约束。假设空间（Hypothesis Space）、模型容量（Model Capacity）与归纳偏置（Inductive Bias）共同描述的，就是这套约束。</p>
<div class="blog_h3"><span class="graybg">假设空间</span></div>
<p>假设空间（Hypothesis Space）是模型可表达函数的集合，常记为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{H}=\{f_\theta:\theta\in\Theta\}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 是由参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 决定的预测函数， <span displaypfx="inline-" class="mathjax-container">\(\Theta\)</span> 是参数可取的范围。这个定义的关键不在于“参数有多少”，而在于<span style="background-color: #c0c0c0;">模型最终允许出现哪些映射形状</span>。例如，一元线性回归的假设空间只包含直线；二次多项式回归的假设空间包含抛物线；深度神经网络的假设空间则更大，能表示复杂得多的非线性函数。</p>
<p>因此，训练并不是在所有可能函数中盲目搜索，而是在某个特定假设空间里找一个最合适的函数。假设空间太小，真实规律可能根本装不进去；假设空间太大，模型又容易把偶然噪声也解释成模式。</p>
<div class="blog_h3"><span class="graybg">模型容量</span></div>
<p>模型容量（Model Capacity）描述的是假设空间的表达能力有多强，也就是模型能拟合多复杂规律。容量高，不代表一定更好；它只表示模型“有能力”表示复杂函数。是否真的学得好，还取决于数据量、正则化、优化过程和任务本身。</p>
<p>容量可以从多个角度理解。参数更多通常意味着容量更高，但这不是唯一标准；树的深度、核方法的核函数形式、特征维度、网络层数、隐藏维度、注意力头数，都会改变容量。工程上常用一个朴素判断：<span style="background-color: #c0c0c0;">如果模型连训练集主要结构都拟合不了，容量偏低；如果训练集几乎完美、验证集却明显变差，容量往往偏高或约束不足</span>。</p>
<p>容量与复杂度控制始终是一组平衡。表格数据上的浅层树模型可能已经足够；图像、语音、自然语言这类高度复杂任务，则通常需要更高容量的模型族。容量本身不是缺点，关键在于它是否与数据规模和任务难度匹配。</p>
<div class="blog_h3"><span class="graybg">归纳偏置</span></div>
<p>归纳偏置（Inductive Bias）是模型在有限样本下从已见数据推广到未见数据时，默认采用的结构性偏好。只靠训练集上有限个点，无法唯一确定整个输入空间上的函数；模型之所以还能做出泛化判断，是因为它隐含地偏好某些解释，而排斥另一些解释。</p>
<p>把学习目标写成经验风险最小化时，这一点会更清楚：</p>
<span displaypfx="" class="mathjax-container">\[\hat f=\arg\min_{f\in\mathcal{H}}\hat R_n(f),\qquad \hat R_n(f)=\frac{1}{n}\sum_{i=1}^{n}\ell(f(x_i),y_i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\hat R_n(f)\)</span> 是经验风险（Empirical Risk），表示函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在训练集上的平均损失； <span displaypfx="inline-" class="mathjax-container">\(\hat f\)</span> 是最终选出的模型。关键约束正是 <span displaypfx="inline-" class="mathjax-container">\(f\in\mathcal{H}\)</span>：优化并不是在所有可能函数中找最优解，而是在一个被模型结构预先限制过的空间里找解。这个限制本身就是归纳偏置。</p>
<p>归纳偏置的来源很多。线性模型偏好线性关系；KNN（K-Nearest Neighbors）偏好局部相似样本给出相似输出；卷积神经网络（CNN）偏好局部连接与平移等变（Translation Equivariance）；树模型偏好分段常数的轴对齐切分；Transformer 则偏好通过注意力在 token 之间建立可变依赖。正则化、数据增强、参数共享、预训练初始化、优化器的更新轨迹，也都会进一步塑造模型的归纳偏置。</p>
<div class="blog_h2"><span class="graybg">期望风险、经验风险与结构风险</span></div>
<p>在统计学习（Statistical Learning）里，训练目标可以从三个层次来理解：期望风险（Expected Risk）描述模型在真实数据分布上的平均误差；经验风险（Empirical Risk）描述模型在有限训练集上的平均误差；结构风险（Structural Risk）则在经验风险之外，把模型复杂度一并纳入考虑。三者回答的是同一个问题的不同版本：<span style="background-color: #c0c0c0;">模型到底应该怎样才算“学得好”</span>。</p>
<div class="blog_h3"><span class="graybg">期望风险</span></div>
<p>期望风险也常被称为真实风险（True Risk）或总体风险（Population Risk）。若真实数据来自未知分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>，模型为 <span displaypfx="inline-" class="mathjax-container">\(f\)</span>，单样本损失为 <span displaypfx="inline-" class="mathjax-container">\(\ell(f(x),y)\)</span>，则期望风险定义为：</p>
<span displaypfx="" class="mathjax-container">\[R(f)=\mathbb{E}_{(x,y)\sim P}\big[\ell(f(x),y)\big]\]</span>
<p>这条式子的含义很直接：把模型放到所有可能出现的真实样本上，计算平均损失。理论上，这才是机器学习真正想最小化的对象，因为泛化能力最终取决于模型在未知数据上的表现，而不是只取决于训练集上的表现。</p>
<p>困难在于，真实分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span> 并不可见。训练时手里只有有限样本，而没有“全体可能数据”的上帝视角。因此，期望风险通常不能被直接计算，只能被估计。</p>
<div class="blog_h3"><span class="graybg">经验风险</span></div>
<p>经验风险是用训练集对期望风险做出的现实近似。设训练集为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{D}=\{(x_i,y_i)\}_{i=1}^{n}\)</span>，则经验风险定义为：</p>
<span displaypfx="" class="mathjax-container">\[\hat R_n(f)=\frac{1}{n}\sum_{i=1}^{n}\ell\big(f(x_i),y_i\big)\]</span>
<p>它就是模型在当前这批已观测样本上的平均损失。工程上常见的 training loss，本质上反映的正是经验风险，或它的 mini-batch 近似。经验风险最小化（Empirical Risk Minimization, ERM）的训练逻辑也很朴素：既然期望风险不可直接计算，就先把训练集上的平均损失压低。</p>
<p>ERM 的关键前提是：训练集足够代表真实分布。当样本数量增加且采样足够合理时，经验风险通常会更接近期望风险；但在有限样本条件下，两者并不相等。二者之间的差距，本质上就是泛化误差（Generalization Gap）的来源。若模型只是把训练集中的偶然模式、局部噪声和标注误差也一并记住，那么 <span displaypfx="inline-" class="mathjax-container">\(\hat R_n(f)\)</span> 可以很低，而 <span displaypfx="inline-" class="mathjax-container">\(R(f)\)</span> 仍然很高，这正是过拟合（Overfitting）的典型形式。</p>
<div class="blog_h3"><span class="graybg">结构风险</span></div>
<p>结构风险（Structural Risk）是在经验风险之外，再把模型复杂度或假设空间规模纳入考虑的目标。它对应的思想是结构风险最小化（Structural Risk Minimization, SRM）：模型不仅要在训练集上拟合得好，还要避免复杂到足以随意记忆有限样本。</p>
<p>在统计学习理论的严格表述中，SRM 常写成在一族嵌套假设空间之间做选择；在工程实践里，它更常以“经验风险 + 复杂度惩罚”的形式出现，例如：</p>
<span displaypfx="" class="mathjax-container">\[J(f)=\hat R_n(f)+\lambda\,\Omega(f)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)\)</span> 是复杂度项（Complexity Penalty）， <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 控制数据拟合与复杂度约束之间的权衡。L2 正则化（L2 Regularization）、L1 正则化（L1 Regularization）、Weight Decay、早停（Early Stopping）以及对模型深度、宽度和树复杂度的限制，都可以看作结构风险最小化思想的具体实现。</p>
<p>因此，结构风险并不是对经验风险的否定，而是对 ERM 的补充：在有限样本条件下，<span style="background-color: #c0c0c0;">仅仅把训练误差压到最低，并不能保证模型在真实分布上表现最好</span>。模型必须同时控制复杂度，才能让“训练集上学到的规律”更有机会迁移到未见样本。</p>
<div class="blog_h3"><span class="graybg">为什么它重要</span></div>
<p>这三者共同构成了机器学习里最基本的张力。期望风险是理论上真正想优化的目标，但它不可直接见；经验风险是训练时可观测、可优化的替代量；结构风险则提醒我们，有限样本下不能把“训练集表现更好”直接等同于“真实世界表现更好”。换句话说，监督学习不仅是在找一个拟合训练集的函数，更是在有限数据和有限模型约束下，寻找一个最可能泛化的解释。</p>
<p>从这里继续往下，就会自然出现另一个问题：如果高维真实数据本身就带有强结构约束，那么经验风险为何常能在有限样本下逼近期望风险，模型又为何能够泛化到未见样本？流形假设（Manifold Hypothesis）正是对这个问题的一条几何回答：真实数据并不会任意填满整个高维空间，而是集中在某个低维、连续、受约束的结构附近。</p>
<div class="blog_h2"><span class="graybg">流形假设</span></div>
<p>流形假设（Manifold Hypothesis）给出了现代机器学习里一条极其重要的几何直觉：现实世界中有意义的数据，虽然表面上嵌在极高维空间里，但真正有效的变化自由度通常远低于表观维度。也就是说，高维观测往往不是填满整个空间，而是集中分布在一个低维流形（Low-dimensional Manifold）附近。</p>
<p>图像是最容易理解的例子。一个 <span displaypfx="inline-" class="mathjax-container">\(1024\times 1024\)</span> 的 RGB 图像在像素空间中维度极高，但自然图像并不会均匀占据这个巨大空间：物体形状、光照条件、视角变化、相机成像规律与纹理结构都受到强约束。因此，“像真实猫照片”的图像实际上只落在高维像素空间中的极小区域里。文本也类似。一个长度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 的 token 序列组合数极其巨大，但真正同时符合语法、语义和任务约束的文本，只占离散组合空间中很小的一部分。模型之所以能够泛化，一个重要原因正是：它并不需要学会覆盖整个高维空间，而只需要学会沿着这些低维结构建模。</p>
<div class="blog_h3"><span class="graybg">什么是流形</span></div>
<p>流形（Manifold）首先是一个几何对象。它的关键性质不是“弯不弯”，而是<span style="background-color: #c0c0c0;">局部上看起来像普通的低维欧几里得空间，整体上却可以弯曲、卷曲并嵌入到更高维空间中</span>。更正式地说，若一个集合对其上每一点，都存在一个足够小的邻域，可以用 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^d\)</span> 中的局部坐标平滑描述，那么这个集合就可以看作一个 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维流形。</p>
<p>地球表面是最直观的例子。站在操场或街道上时，局部地面几乎是平的，可以用二维坐标定位；但从整体看，地球表面显然不是二维平面，而是嵌入三维空间中的弯曲曲面。因此，地球表面就是一个嵌在三维空间里的二维流形。对只能沿地面运动的观察者而言，真正相关的不是三维空间全部坐标，而是地表上那两个局部自由度。</p>
<p>机器学习教材里常见的瑞士卷（Swiss Roll）把这个概念进一步可视化。可以先想象一张二维纸，再把它卷进三维空间。卷起来之后，样本点在外部看来落在三维空间里，但沿着纸面定位某一点时，真正需要的仍然只是二维坐标。外在维度变高了，内在结构却没有变。这正是流形概念在机器学习里最重要的几何直觉：<span style="background-color: #c0c0c0;">数据的表观维度可以很高，但它的内在维度却可能很低</span>。</p>
<p>这类卷曲结构还带来另一个机器学习里非常关键的后果：<span style="background-color: #c0c0c0;">嵌入空间中的欧氏距离（Euclidean Distance）与流形上的测地距离（Geodesic Distance）并不一定一致</span>。两点在外部空间里看起来可能很近，因为一条直线可以直接“穿过空气”连接它们；但若真实数据只能沿流形本身变化，那么真正相关的距离应当是沿曲面或曲线走过去的那条路径长度。流形学习（Manifold Learning）之所以强调邻域图、测地近似和局部结构，正是因为外部直线距离经常不能反映数据在内在结构上的真实远近关系。</p>
<p>放到高维数据上，这个判断尤其关键。一张 <span displaypfx="inline-" class="mathjax-container">\(1000\times1000\)</span> 的灰度图像在像素空间里有一百万维，但“真实人脸图像”显然不会填满整个一百万维空间。姿态、光照、表情、年龄、拍摄距离等因素彼此耦合，使真实样本只落在高维像素空间中一个极薄、极小、受连续约束的区域附近。文本也是同样的逻辑：虽然 token 组合空间极其巨大，但真正同时满足语法、语义、上下文与任务约束的句子，只会沿着某种低维结构变化，而不会任意填满整个离散组合空间。</p>
<p>因此，在机器学习语境中谈流形，真正想表达的并不是完整搬运微分几何的全部形式系统，而是一个更直接的判断：<span style="background-color: #c0c0c0;">有意义的数据并不会随机散落在高维空间中，而是集中在某个低维、连续、受约束的结构附近</span>。后面关于自由度、主成分、隐空间、低维近似以及 LoRA 任务子空间的讨论，都是围绕这个判断展开的不同形式化视角。</p>
<div class="blog_h3"><span class="graybg">自由度</span></div>
<p>沿着前面“表观维度高、内在结构低”的判断继续往下走，就会自然落到自由度（Degrees of Freedom）这个概念上。表观维度说的是“数据在形式上有多少个坐标轴”；自由度说的是“这些数据实际上有多少种彼此独立的有效变化方式”。二者并不相同。一个对象可以嵌在极高维空间里，但真正能变化的自由度却很少。</p>
<p>例如，一张脸部图片在像素空间里有数百万维，但很多像素并不能独立随意变化：头部转向、光照强弱、表情变化、年龄纹理、拍摄距离这些因素彼此耦合，共同决定了大部分像素的联动变化。因此，“一张脸”看起来是高维数组，真正支配它变化的自由度却远小于像素总数。文本也一样。句子表面上由许多 token 组成，但语法结构、主题、语气、说话者意图与上下文约束，使它不可能在每个位置上完全独立自由地变化。</p>
<p>这也是流形假设真正重要的地方：它不是抽象地说“数据在低维流形上”，而是在说<span style="background-color: #c0c0c0;">有效自由度远少于表观维度</span>。一旦把这层理解清楚，后面关于主成分、隐空间、隐主题、低秩近似乃至 LoRA 的很多思想都会变得顺理成章，因为它们都在试图用更少的自由度，去抓住决定数据或参数变化的核心结构。</p>
<div class="blog_h3"><span class="graybg">主成分、隐空间与隐主题</span></div>
<p>一旦接受了“高维数据实际靠近低维结构”这一点，后面许多术语就会自然连起来。主成分（Principal Components）强调几何视角：在一组高维数据里，哪些方向承载了最主要的变化。隐空间（Latent Space）强调表示视角：把原始高维观测压缩到一个更低维、但仍保留关键信息的内部空间。隐主题（Latent Topics）则更偏语义视角：在文本与文档分解里，低维方向常常可以被解释为若干潜在语义因素，例如“体育”“金融”“法律”这类人类能命名的主题轴。</p>
<p>这三个词并不完全同义，但常常指向同一个底层事实：<span style="background-color: #c0c0c0;">高维观测可以通过少数主导方向或潜在因子来近似描述</span>。主成分更强调方差最大的坐标轴；隐空间更强调模型内部那间低维“房间”；隐主题则是在某些任务里，对这些低维方向做出的语义解释。它们分别对应几何、表示与语义三种语言，但共享同一条低维结构主线。</p>
<div class="blog_h3"><span class="graybg">PCA 与低维近似</span></div>
<p>PCA（Principal Component Analysis）是这条思路最经典、也最直接的算法形式。它在无监督条件下寻找方差最大的几个方向，并把数据投影到这些方向张成的低维子空间中。若数据矩阵为 <span displaypfx="inline-" class="mathjax-container">\(X\)</span>，PCA 本质上是在找一个低维线性子空间，使投影后的重建误差尽量小。在线性代数上，这与奇异值分解（SVD）直接对应：保留最大的前 <span displaypfx="inline-" class="mathjax-container">\(r\)</span> 个奇异值及其奇异向量，就得到最佳的 rank-<span displaypfx="inline-" class="mathjax-container">\(r\)</span> 近似。</p>
<p>这类思想并不只存在于经典降维里。自动编码器（Autoencoder）通过瓶颈层学习隐空间；潜在语义分析（Latent Semantic Analysis, LSA）和主题模型在文档-词矩阵里抽取潜在主题；词向量、句向量与深度表示模型把高维离散符号压缩到稠密向量空间。它们的目标函数和可解释性不同，但都默认：原始高维观测背后存在一个更低维、更有结构的变化空间。</p>
<div class="blog_h3"><span class="graybg">LoRA 与任务相关子空间</span></div>
<p>LoRA（Low-Rank Adaptation）通过把参数更新 <span displaypfx="inline-" class="mathjax-container">\(\Delta W\)</span> 限制在一个低秩子空间中，实现对大模型的参数高效微调（PEFT）。它的核心做法不是直接改写原始权重矩阵的全部自由度，而是把更新写成两个低秩矩阵的乘积：</p>
<span displaypfx="" class="mathjax-container">\[\Delta W = BA,\quad B\in\mathbb{R}^{d_{\text{out}}\times r},\ A\in\mathbb{R}^{r\times d_{\text{in}}},\ r\ll \min(d_{\text{in}},d_{\text{out}})\]</span>
<p>LoRA 应放在“低维结构”这条主线上理解，但不能把它与 PCA 直接等同。它的核心不是对输入数据做主成分分析，而是对模型参数更新施加低秩约束。</p>
<p>这个约束的含义是：模型不能在完整高维参数空间中任意改动，而只能在一个 rank-<span displaypfx="inline-" class="mathjax-container">\(r\)</span> 的低维更新子空间里移动。从思想上看，它确实与“只保留主导方向”高度相似；若事后对某个全量更新矩阵做 SVD，最佳低秩近似也会只保留最主要的奇异方向。但 LoRA 学到的并不是传统 PCA 意义上“数据方差最大”的主成分，而是<span style="background-color: #c0c0c0;">对当前任务损失下降最有用的低维更新方向</span>。前者是无监督的统计主轴，后者是由反向传播和任务目标共同决定的优化子空间。</p>
<p>因此，把 LoRA 理解为“逼迫模型只在少数主导方向上修改参数”是成立的；但这些方向更准确地说是任务相关的低维适配方向，而不是直接等同于原始数据的 PCA 主成分。也正因为如此，LoRA 与内在维度（Intrinsic Dimension）讨论天然相连：如果一个下游任务真正需要修改的有效自由度本来就不高，那么让模型只在一个低秩子空间里更新，不仅不会显著损失性能，反而会自动抑制大量无意义的噪声方向。</p>
<div class="blog_h2"><span class="graybg">学习范式</span></div>
<p>这里先按监督信号来源划分学习范式。监督学习（Supervised Learning）、无监督学习（Unsupervised Learning）、自监督学习（Self-supervised Learning）和强化学习（Reinforcement Learning）回答的是同一个问题：模型训练时的监督信号究竟来自哪里。它们属于同一分类标准，因此可以并列讨论。</p>
<div class="blog_h3"><span class="graybg">监督学习</span></div>
<p>监督学习（Supervised Learning）使用带标签的数据对 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 训练模型：输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是特征（Feature），输出 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是目标或标签（Label）。模型学习的不是“记住答案”，而是学习一个映射 <span displaypfx="inline-" class="mathjax-container">\(f_\theta:x\to y\)</span>，使它对新样本也能给出合理预测。</p>
<p>但“学一个映射”还不够，训练时还必须回答另一个更具体的问题：<span style="background-color: #c0c0c0;">怎样才算模型学得好</span>。监督学习里最常见的回答，是在训练集上逐个比较预测与真实标签的差距，再把这些差距汇总成一个总体目标；这就导向经验风险最小化（Empirical Risk Minimization, ERM）。</p>
<p>经验风险最小化的典型目标写成：</p>
<span displaypfx="" class="mathjax-container">\[\frac{1}{N}\sum_{i=1}^{N}\ell\big(f_\theta(x_i),y_i\big)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 是样本数， <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 是模型预测， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实标签， <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span> 是损失函数（Loss Function）。这条式子的含义不是抽象求和，而是：<span style="background-color: #c0c0c0;">逐个样本计算“预测错了多少”，再取平均，把平均错误压到尽可能小</span>。</p>
<p>例：垃圾邮件分类里， <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 可以是邮件文本特征， <span displaypfx="inline-" class="mathjax-container">\(y\in\{0,1\}\)</span> 表示“正常/垃圾”；房价预测里， <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 可以是面积、地段、楼龄， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是价格。前者是分类（Classification），后者是回归（Regression），但“有标签地学映射、再用损失函数衡量误差”这一训练逻辑完全一致。</p>
<div class="blog_h4"><span class="graybg">分类任务</span></div>
<p>分类任务（Classification）要预测的是<span style="background-color: #c0c0c0;">离散类别</span>，也就是样本属于哪一类。输出可以是一个类别 id，也可以是一组类别概率。例如二分类里常见输出 <span displaypfx="inline-" class="mathjax-container">\(P(y=1|x)\)</span>，表示“给定特征 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时，样本属于正类的概率”。</p>
<p>垃圾邮件识别、肿瘤良恶性判断、情感分析、图像里的猫狗识别都属于分类任务。它更像“做选择题”：模型最终要在有限候选里做判断。训练时常配合交叉熵（Cross-Entropy）这类损失，因为模型不仅要选对类别，还要给正确类别足够高的置信度。</p>
<div class="blog_h4"><span class="graybg">回归任务</span></div>
<p>回归任务（Regression）要预测的是<span style="background-color: #c0c0c0;">连续数值</span>，也就是标签不是几个固定类别，而是在某个数值区间内连续变化。输出通常直接是一个实数，或一个多维连续向量。</p>
<p>房价预测、销量预测、温度预测、广告点击率中的停留时长估计都属于回归任务。它更像“做填空题”：模型不能只说“高”或“低”，而必须给出具体数值。训练时常配合均方误差（MSE）或平均绝对误差（MAE），因为关心的是预测值与真实值到底差了多少。</p>
<p>分类与回归都属于监督学习，因为它们都有标签；真正的区别在于<span style="background-color: #c0c0c0;">标签空间的形状</span>：分类的标签空间是离散集合，回归的标签空间是连续区间。这个区别会直接决定模型输出层形式、损失函数选择以及评估指标。</p>
<div class="blog_h3"><span class="graybg">弱监督学习</span></div>
<p>弱监督学习（Weakly Supervised Learning）仍然使用标签信号训练模型，但这些标签并不像标准监督学习那样完整、精确且逐样本对齐。它处理的核心情形是：<span style="background-color: #c0c0c0;">标签存在，但监督结构不够理想</span>，例如标签只存在于更粗粒度层面、标签本身含噪，或只有部分样本带标签。</p>
<p>从概念上看，弱监督并不是单一算法，而是一组监督不完备情形的总称。常见形式包括：标签不完整（incomplete supervision），即只有部分样本带标签；标签不精确（inexact supervision），即标签附着在聚合层级而不是实例层级；标签不准确（inaccurate supervision），即标签本身带噪或来自启发式规则、远程监督（Distant Supervision）与弱标注器。它与标准监督学习的边界在于：监督信号依然存在，但标签质量、粒度或覆盖度不足以直接当作“干净答案册”。</p>
<div class="blog_h4"><span class="graybg">多实例学习（Multi-Instance Learning, MIL）</span></div>
<p>多实例学习（Multi-Instance Learning, MIL）是弱监督学习中的一个经典范式。它的关键设定是：标签不附着在单个实例（instance）上，而是附着在一个由多个实例组成的包（bag）上。设训练集由若干包 <span displaypfx="inline-" class="mathjax-container">\(\{B_1,B_2,\dots,B_n\}\)</span> 构成，其中第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个包可写成</p>
<span displaypfx="" class="mathjax-container">\[B_i=\{x_{i1},x_{i2},\dots,x_{im_i}\},\qquad y_i\in\{0,1\}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x_{ij}\)</span> 是包内第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个实例， <span displaypfx="inline-" class="mathjax-container">\(m_i\)</span> 是该包包含的实例数， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是包级标签。训练时只知道整个包的标签，不知道每个实例自己的标签。</p>
<p>MIL 最经典的标准假设（Standard MI Assumption）是：正包中至少存在一个正实例，负包中所有实例都为负。写成逻辑形式就是：</p>
<span displaypfx="" class="mathjax-container">\[y_i=1 \iff \exists j,\ z_{ij}=1;\qquad y_i=0 \iff \forall j,\ z_{ij}=0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(z_{ij}\)</span> 表示实例级未知标签。这个设定特别适合两类问题：第一，真正决定结果的证据只稀疏地存在于少数关键实例中，其余实例更像背景或噪声；第二，获取实例级标签成本高，只能拿到更粗粒度的聚合标签。</p>
<p>因此，MIL 的关键不只是“把很多实例放在一起”，而是要学习一条从<span style="background-color: #c0c0c0;">实例集合到包级判断</span>的聚合规则。早期方法常用最大池化、均值池化或手工设计的聚合函数；深度学习阶段则更常引入可学习聚合，例如注意力式 MIL（Attention-based MIL），用可训练权重自动决定哪些实例对最终包标签贡献更大。这使 MIL 不只具有表达能力，也更容易给出“模型主要关注了哪些实例”的解释线索。</p>
<p>MIL 的适用性可以用一个抽象例子来理解。若一条完整客服对话被视作一个包，其中每轮发言是实例，而整体满意度评分是包级标签，那么模型面对的就是“整体有标签、逐轮无标签”的典型结构。此时，MIL 的任务不是给每轮发言都强行分配人工标签，而是在只有整体评分的前提下，学习哪些局部发言更可能决定整段会话的最终判断。</p>
<div class="blog_h3"><span class="graybg">无监督学习</span></div>
<p>无监督学习（Unsupervised Learning）只有输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，没有人工标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>。它的目标不是拟合“标准答案”，而是从数据中发现结构（Structure），例如聚类（Clustering）、降维（Dimensionality Reduction）、密度估计（Density Estimation）与异常检测（Anomaly Detection）。</p>
<p>一个直观类比是：监督学习像“拿着答案册做题”，无监督学习像“没有答案册，只能自己把一堆材料按相似性归类”。例如电商用户没有现成“用户类型”标签，但可以根据浏览、购买、停留时间等行为聚成“价格敏感型”“冲动购买型”“高价值复购型”等群体，用于运营分层。</p>
<div class="blog_h3"><span class="graybg">自监督学习</span></div>
<p>自监督学习（Self-supervised Learning）介于监督与无监督之间：原始数据没有人工标签，但任务标签可以由数据本身自动构造出来。核心思想是<span style="background-color: #c0c0c0;">从数据内部制造预测任务</span>，让模型在完成这些任务的过程中学到可迁移表示（Representation）。</p>
<p>语言模型的下一个 token 预测就是最典型的自监督任务：前文是输入，后一个 token 是由原始文本自动给出的“监督信号”。图像领域里，旋转预测、遮挡恢复、不同增强视角匹配也属于同一路线。</p>
<div class="blog_h4"><span class="graybg">掩码预测</span></div>
<p>掩码预测（Masked Prediction）把输入中的一部分信息故意遮住，再要求模型恢复。例如 BERT 会把句子中的部分 token 替换成特殊标记 <span displaypfx="inline-" class="mathjax-container">\([MASK]\)</span>，模型要根据上下文预测被遮住的词。</p>
<p>类比来看，这像完形填空：你不是死记整句，而是学会根据上下文推断缺失信息。它迫使模型同时利用左侧和右侧上下文，因此特别适合编码器（Encoder）型表示学习。</p>
<div class="blog_h3"><span class="graybg">强化学习</span></div>
<p>强化学习（Reinforcement Learning, RL）研究的是：智能体（Agent）在环境（Environment）中持续交互，根据奖励（Reward）学习策略（Policy），使长期累计回报（Cumulative Return）尽可能大。它关心的不是“单个输入该预测什么标签”，而是<span style="background-color: #c0c0c0;">一串连续动作最终能否带来更高的长期收益</span>。</p>
<div class="blog_h4"><span class="graybg">问题设定</span></div>
<p>强化学习的基本循环可以概括为：在时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span>，智能体观察状态 <span displaypfx="inline-" class="mathjax-container">\(s_t\)</span>，选择动作 <span displaypfx="inline-" class="mathjax-container">\(a_t\)</span>，环境转移到新状态 <span displaypfx="inline-" class="mathjax-container">\(s_{t+1}\)</span>，并返回奖励 <span displaypfx="inline-" class="mathjax-container">\(r_{t+1}\)</span>。策略记为 <span displaypfx="inline-" class="mathjax-container">\(\pi(a\mid s)\)</span>，它回答的是“在当前状态下，动作应该怎样选”。</p>
<p>很多任务里，奖励并不会在正确动作发生的那一刻立刻显现。下棋时，一步好棋可能要十几步后才体现价值；推荐系统里，一次推荐是否合理，也要看后续点击、停留和转化。因此强化学习优化的不是单步得分，而是长期回报：</p>
<span displaypfx="" class="mathjax-container">\[J(\pi)=\mathbb{E}_{\pi}\!\left[\sum_{t=0}^{\infty}\gamma^t r_{t+1}\right]\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\gamma\in[0,1)\)</span> 是折扣因子（Discount Factor）。<span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 越接近 1，策略越重视长期收益；越小，策略越偏向短期收益。这个目标函数的含义很直接：在所有可能的策略中，找到那个平均下来总分最高的行为规则。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/rl.png"><img class="alignnone size-full wp-image-40981" src="https://blog.gmem.cc/wp-content/uploads/2026/03/rl.png" alt="rl" width="100%" /></a></p>
<div class="blog_h4"><span class="graybg">和监督学习的差异</span></div>
<p>监督学习通常基于带标签样本 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 训练静态映射 <span displaypfx="inline-" class="mathjax-container">\(x\mapsto y\)</span>；强化学习面对的是动态环境中的序贯决策。监督学习收到的是“正确答案应当是什么”的指导性反馈，强化学习收到的是“这一步或这条轨迹好不好”的评估性反馈。前者的误差归因通常较直接，后者则需要把最终回报回溯到一串历史动作，这就是时间信用分配（Temporal Credit Assignment）的难点来源。</p>
<p>因此，强化学习的训练数据也不是静态不变的。当前策略会决定智能体之后访问哪些状态，于是数据分布本身会随着策略更新而改变。这一点使强化学习同时面对建模问题、探索问题和训练稳定性问题。</p>
<div class="blog_h4"><span class="graybg">马尔可夫决策过程（MDP）</span></div>
<p>强化学习的标准数学框架是马尔可夫决策过程（Markov Decision Process, MDP）。一个 MDP 通常写成五元组 <span displaypfx="inline-" class="mathjax-container">\((\mathcal{S},\mathcal{A},P,R,\gamma)\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{S}\)</span> 是状态空间， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{A}\)</span> 是动作空间， <span displaypfx="inline-" class="mathjax-container">\(P(s'|s,a)\)</span> 是状态转移概率， <span displaypfx="inline-" class="mathjax-container">\(R(s,a)\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(R(s,a,s')\)</span> 是奖励函数， <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 是折扣因子。</p>
<p>“马尔可夫”这个词的核心含义是：<span style="background-color: #c0c0c0;">如果当前状态已经把决策所需的信息概括完整，那么未来只取决于当前状态和当前动作</span>。它并不要求系统真的没有历史，而是要求当前状态已经足够代表历史中与决策相关的部分。</p>
<div class="blog_h4"><span class="graybg">价值函数与 Bellman 方程</span></div>
<p>强化学习里最重要的两个量是状态价值函数（State-value Function）和动作价值函数（Action-value Function）：</p>
<span displaypfx="" class="mathjax-container">\[V^\pi(s)=\mathbb{E}_\pi[G_t\mid s_t=s],\qquad Q^\pi(s,a)=\mathbb{E}_\pi[G_t\mid s_t=s,\ a_t=a]\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(V^\pi(s)\)</span> 描述“在状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 下，按策略 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span> 继续行动，长期回报大约是多少”；<span displaypfx="inline-" class="mathjax-container">\(Q^\pi(s,a)\)</span> 则更进一步，描述“在状态 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 下先做动作 <span displaypfx="inline-" class="mathjax-container">\(a\)</span>，再按策略 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span> 继续行动，长期回报大约是多少”。</p>
<p>Bellman 方程（Bellman Equation）把“长期回报”写成“即时奖励 + 下一步价值”的递归形式。对固定策略 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span>，状态价值满足：</p>
<span displaypfx="" class="mathjax-container">\[V^\pi(s)=\mathbb{E}_\pi\big[r_{t+1}+\gamma V^\pi(s_{t+1})\mid s_t=s\big]\]</span>
<p>这条式子的直觉非常重要：一个状态值多少钱，不需要把未来整条轨迹一口气全部展开，只要看“这一步先拿到多少，再加上下一状态值多少钱”。这就是动态规划思想在强化学习中的核心落点。</p>
<p>若目标是最优控制，则最优动作价值函数满足 Bellman 最优方程：</p>
<span displaypfx="" class="mathjax-container">\[Q^*(s,a)=\mathbb{E}\big[r_{t+1}+\gamma\max_{a'}Q^*(s_{t+1},a')\mid s_t=s,\ a_t=a\big]\]</span>
<p>它表达的是：当前动作的最优价值，等于这一步的即时奖励，加上下一状态里最佳后续选择的折扣价值。价值型方法、时序差分学习（Temporal-Difference Learning, TD）以及 Q-Learning，都是围绕这个递归结构展开的。</p>
<div class="blog_h4"><span class="graybg">价值方法：Q-Learning 与 DQN</span></div>
<p>价值型方法（Value-Based Methods）先估计“某个状态或状态-动作对值多少钱”，再根据价值做决策。最经典的做法是 Q-Learning。它不直接记住“这一步该做什么”，而是维护一个动作价值估计 <span displaypfx="inline-" class="mathjax-container">\(Q(s,a)\)</span>，让模型逐步学会哪些动作长期更划算。</p>
<p>Q-Learning 的标准更新写成：</p>
<span displaypfx="" class="mathjax-container">\[Q(s,a)\leftarrow Q(s,a)+\alpha\Big(r+\gamma\max_{a'}Q(s',a')-Q(s,a)\Big)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(r+\gamma\max_{a'}Q(s',a')\)</span> 是新的目标值（TD target），它把“这一步拿到的即时奖励”和“下一步开始最好还能拿到多少”拼接在一起；括号里的整体差值是 TD 误差（TD error），表示旧估计和新观察之间的偏差。Q-Learning 每见到一次新的转移样本，就用它修正一次账本。</p>
<p>当状态空间很小，例如离散网格迷宫，价值可以直接存成 Q 表；当状态变成图像、传感器序列或高维特征时，就需要用神经网络近似 <span displaypfx="inline-" class="mathjax-container">\(Q(s,a)\)</span>，这就是 DQN（Deep Q-Network）。它没有改变基本思想，只是把“查表记账”升级成“让网络来估值”。</p>
<div class="blog_h4"><span class="graybg">策略方法：Policy Gradient、Actor-Critic 与 PPO</span></div>
<p>策略型方法（Policy-Based Methods）不先学一个价值表，而是直接参数化策略 <span displaypfx="inline-" class="mathjax-container">\(\pi_\theta(a\mid s)\)</span>。给定状态，模型直接输出动作概率分布或连续控制参数，再通过优化把高回报动作的概率提高、低回报动作的概率压低。它尤其适合连续动作空间，因为这类问题往往很难穷举所有动作再逐个估值。</p>
<p>策略梯度（Policy Gradient）的基本形式是：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_\theta J(\theta)=\mathbb{E}\big[\nabla_\theta \log \pi_\theta(a_t\mid s_t)\,G_t\big]\]</span>
<p>它的含义可以概括成一句话：最终效果好的动作，以后更常做；最终效果差的动作，以后更少做。问题在于，直接用 <span displaypfx="inline-" class="mathjax-container">\(G_t\)</span> 更新通常方差很大，训练容易抖动。</p>
<p>于是就出现了 Actor-Critic。Actor 负责输出策略，也就是“怎么行动”；Critic 负责评估当前状态或动作值，也就是“这一步大概值多少”。Critic 提供更稳定的基线或优势估计，Actor 再沿着这个更平滑的信号更新策略。这样做的结果是：策略更新方向仍然由回报决定，但梯度噪声显著更可控。</p>
<p>PPO（Proximal Policy Optimization）可以看作一种工程上非常成功的 Actor-Critic 变体。它的核心思想不是改变优化方向，而是给策略更新加护栏，限制新旧策略之间的偏移幅度，避免一步改得过猛导致训练失稳。因此，PPO 在机器人控制、游戏智能体和大模型对齐里都很常见。</p>
<div class="blog_h4"><span class="graybg">探索与利用</span></div>
<p>强化学习始终要面对探索与利用（Exploration vs. Exploitation）的权衡。利用意味着优先选择当前已知回报较高的动作；探索意味着尝试那些暂时不确定、但可能更优的动作。只利用，策略可能很快卡在局部最优；只探索，又会浪费大量样本在明显不好的选择上。</p>
<p>这个矛盾在强化学习里是结构性的，因为策略会影响后续看到的数据。常见做法包括 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>-greedy、熵正则化（Entropy Regularization）、Boltzmann exploration、上置信界（UCB）等。它们形式不同，但共同目标一致：既让模型敢于试错，又不至于长期停留在无意义的随机行动里。</p>
<div class="blog_h4"><span class="graybg">Model-Based 与 Model-Free</span></div>
<p>另一条常见划分标准是 model-based 与 model-free。二者的区别不在于是否使用神经网络，而在于<span style="background-color: #c0c0c0;">是否显式学习或利用环境动力学</span>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">核心思路</td>
<td style="text-align: center;">优势</td>
<td style="text-align: center;">代价</td>
</tr>
</thead>
<tbody>
<tr>
<td>Model-Based RL</td>
<td>显式学习或已知状态转移与奖励模型，再据此规划或生成模拟轨迹</td>
<td>样本效率通常更高；能做前瞻规划</td>
<td>环境模型一旦学偏，规划也会被带偏；实现更复杂</td>
</tr>
<tr>
<td>Model-Free RL</td>
<td>不显式建模环境，直接从交互样本学习价值函数或策略</td>
<td>实现相对直接；适合复杂高维环境</td>
<td>样本效率通常较低；对真实交互成本更敏感</td>
</tr>
</tbody>
</table>
<p>价值型方法如 Q-Learning、DQN，策略型方法如 REINFORCE、PPO，多数都属于 model-free 路线；若先学习一个世界模型（World Model）或已知环境转移，再基于模型做搜索和规划，则属于 model-based 路线。实际系统也常把两者混合使用。</p>
<div class="blog_h4"><span class="graybg">和大模型对齐的衔接</span></div>
<p>大模型时代出现的 RLHF、PPO-based alignment、GRPO 等方法，属于强化学习思想在语言模型上的应用层。它们沿用的仍然是策略、奖励、回报、优势函数和策略优化这些通用概念，只是把环境替换成“基于 prompt、回答和偏好反馈构成的交互过程”。因此，理解通用强化学习基础之后，再进入后文的强化学习对齐，会更容易看清哪些是 RL 本体，哪些是大模型场景下的特化设计。</p>
<div class="blog_h2"><span class="graybg">统计学习</span></div>
<p>统计学习（Statistical Learning）强调从<span style="background-color: #c0c0c0;">数据由某种概率机制生成</span>这一视角理解机器学习。前面的概率论与统计已经介绍了概率、似然、MLE、MAP、边缘化与随机过程；这里不重复纯数学定义，而是把这些概念收束成机器学习里的几条主线：模型究竟在建模什么分布，隐藏变量怎样进入问题，推断为什么会变难，以及“相关”与“因果”为什么不是同一件事。</p>
<div class="blog_h3"><span class="graybg">从分布到学习问题</span></div>
<p>从统计学习视角看，很多模型的区别首先不在神经网络层数或树的深度，而在于它们选择建模哪一种概率对象。判别式模型（Discriminative Model）直接学习 <span displaypfx="inline-" class="mathjax-container">\(p(y\mid x)\)</span> 或决策边界，关心“给定输入后标签怎么判”；生成式模型（Generative Model）则更关心 <span displaypfx="inline-" class="mathjax-container">\(p(x,y)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(p(x\mid y)\)</span> 或带隐藏变量的联合分布，关心“数据是如何被生成出来的”。</p>
<p>这种划分并不等于“一个更先进、一个更落后”。判别式方法通常更直接服务于分类或回归目标；生成式方法则更容易表达不确定性、缺失变量和隐含结构。朴素贝叶斯、高斯混合模型（GMM）、隐马尔可夫模型（HMM）偏生成式；逻辑回归、支持向量机（SVM）、条件随机场（CRF）偏判别式。HMM 与 CRF 的细节放在后面的经典机器学习部分展开，这里只强调它们在统计建模立场上的差异。</p>
<div class="blog_h3"><span class="graybg">概率图模型</span></div>
<p>概率图模型（Probabilistic Graphical Model, PGM）用图结构表达随机变量之间的条件独立关系，并把高维联合分布拆成较小的局部因子。它的核心价值不是“把概率画成图”，而是把<span style="background-color: #c0c0c0;">哪些变量直接相互作用、哪些依赖可以被切断</span>写成显式结构，从而让建模、推断和解释都更清晰。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">对象</td>
<td style="text-align: center;">关注点</td>
<td style="text-align: center;">典型形式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>贝叶斯网络（Bayesian Network）</td>
<td>有向依赖与条件独立</td>
<td>有向无环图（DAG）</td>
<td>适合表达“父节点影响子节点”的生成结构</td>
</tr>
<tr>
<td>因子图（Factor Graph）</td>
<td>联合分布如何分解为若干局部因子</td>
<td>变量节点 + 因子节点二部图</td>
<td>更强调分解结构，常作为统一表达方式</td>
</tr>
<tr>
<td>HMM / CRF</td>
<td>序列中的局部依赖</td>
<td>链式图结构</td>
<td>HMM 偏生成式，CRF 偏判别式</td>
</tr>
</tbody>
</table>
<p>贝叶斯网络适合表达“一个变量如何通过若干中间变量影响另一个变量”的有向依赖；因子图则更偏向把复杂联合分布拆成局部势函数（Factor）相乘的形式。若问题具有明显序列结构，HMM 和 CRF 就是最典型的链式概率图模型实例。它们之所以重要，不只是因为历史地位高，而是因为许多现代模型虽然实现方式更复杂，仍然在利用“局部分解 + 全局归一化/推断”这条思想主线。</p>
<div class="blog_h3"><span class="graybg">潜变量、EM 与变分推断</span></div>
<p>统计学习里很多问题之所以变难，不是因为观测变量本身太复杂，而是因为模型里存在隐藏变量（Latent Variable）<span displaypfx="inline-" class="mathjax-container">\(z\)</span>。一旦联合分布写成 <span displaypfx="inline-" class="mathjax-container">\(p(x,z)\)</span>，训练或预测往往都要面对边缘化：</p>
<span displaypfx="" class="mathjax-container">\[p(x)=\sum_z p(x,z)\quad \text{或} \quad p(x)=\int p(x,z)\,dz\]</span>
<p>当这个求和或积分无法直接算清时，推断（Inference）就成为核心问题。EM（Expectation-Maximization）适合一类带潜变量的参数估计问题：E 步先根据当前参数估计隐藏变量的后验分布或其期望统计量，M 步再在这些期望量上更新参数。GMM 的训练、HMM 的 Baum-Welch 算法，都是这一路线的经典例子。</p>
<p>若后验分布本身也难以精确求解，就需要近似推断。变分推断（Variational Inference, VI）的思路是：不用直接算真实后验 <span displaypfx="inline-" class="mathjax-container">\(p(z\mid x)\)</span>，而是选一个可计算的近似分布 <span displaypfx="inline-" class="mathjax-container">\(q(z)\)</span> 去逼近它。于是问题从“直接求后验”转化为“在一个可处理的分布族里找最接近后验的那个近似”。这一思路后来也自然延伸到了现代深度生成模型，例如变分自编码器（VAE）的训练就建立在变分推断框架之上。</p>
<div class="blog_h3"><span class="graybg">因果图与 do-calculus</span></div>
<p>统计相关性回答的是“变量经常一起变化吗”，因果推断（Causal Inference）回答的则是“如果我主动干预一个变量，另一个变量会怎样变”。这两类问题在形式上很接近，但含义完全不同。观察性条件概率 <span displaypfx="inline-" class="mathjax-container">\(p(y\mid x)\)</span> 只说明在看到 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 时， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 通常是什么样；干预分布 <span displaypfx="inline-" class="mathjax-container">\(p(y\mid \mathrm{do}(x))\)</span> 讨论的是把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 强行设定为某个值后， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 会怎样变化。</p>
<p>因果图（Causal Graph）通常也写成有向无环图，但它表达的不再只是统计依赖，而是因果生成结构。混杂因素（Confounder）、中介变量（Mediator）和碰撞点（Collider）之所以重要，正是因为它们决定了哪些相关性可以被解释为因果效应，哪些只是共同原因或选择偏差造成的表象相关。do-calculus 则是一套把干预分布改写成可识别表达式的规则系统，用来判断在给定图结构下，目标因果效应是否能够从可观测分布中恢复出来。</p>
<p>在这份速查里，因果推断只保留这一层定位：它是统计学习向更强解释目标的延伸。普通监督学习通常停在“预测得准”，统计学习进一步讨论“不确定性和隐藏结构怎么处理”，而因果学习则继续追问“如果系统被主动改变，结果会不会跟着改变”。三者不是互斥关系，而是建模目标逐步增强的不同层级。</p>
<div class="blog_h2"><span class="graybg">表示学习与适配策略</span></div>
<p>与“学习范式”不同，下面这些概念不再按监督信号来源分类，而是分别回答另外几个问题：表示该怎样学、计算该怎样近似、已有知识该怎样迁移、在极少样本下又该怎样适配。因此它们更适合看成与学习范式并列的训练目标或训练策略。</p>
<div class="blog_h3"><span class="graybg">表示学习（Representation Learning）</span></div>
<p>表示学习（Representation Learning）讨论的是：如何把原始输入自动变换成更有用的特征表示，使后续任务更容易处理。它关心的不只是“最后预测对不对”，还关心模型内部是否学到了稳定、可迁移、对任务有判别力的中间表示。</p>
<p>传统特征工程（Feature Engineering）与表示学习处理的是同一个核心问题：如何把原始输入变成更适合下游任务的表示。但二者的方法论不同。特征工程主要依赖人工设计表示，例如词频、n-gram、统计量、规则特征与人工交叉特征；表示学习则强调由模型通过优化过程自动学出表示，例如 PCA、自编码器、词向量、上下文化表示以及深度网络中的隐藏状态。因此，传统手工特征本身通常不直接归入表示学习；只有当表示是通过训练自动获得时，它才更准确地属于表示学习范畴。</p>
<p>从 one-hot、BoW、词嵌入，到 BERT 的上下文化表示、Sentence-BERT 的句向量，主线始终一致：把原始符号或原始观测映射到更适合计算的表示空间。监督学习、自监督学习、对比学习都可以被用来学习表示；区别只在于监督信号来自哪里、训练目标如何设计。</p>
<div class="blog_h3"><span class="graybg">对比学习</span></div>
<p>对比学习（Contrastive Learning）通过“拉近正样本、推远负样本”学习表示。这里的关键不是类别标签本身，而是样本之间的相对关系：哪些应该相似，哪些必须区分。因此它特别适合表示学习、检索、多模态对齐和度量学习（Metric Learning）。</p>
<p>它的真正价值不只在于“学会匹配”，更在于学会<span style="background-color: #c0c0c0;">区分性特征（Discriminative Features）</span>。如果训练信号只告诉模型“这两个文本有关”，模型很容易停留在泛泛的共性描述上；而当训练持续提供“相似对”和“不相似对”，模型就被迫回答更尖锐的问题：究竟是什么让这两个文本属于同一语义区域，又是什么让它们必须分开。对比学习因此天然擅长抑制“表面上正确但没有区分度”的表示，转而强化真正决定语义边界的特征。</p>
<p>例如在商品评论表示学习里，句子“物流很快，包装也完整”和“物流很快，但东西是坏的”都包含“物流很快”这类高频表述。若模型只抓住表面词汇重叠，就可能把二者编码得非常接近；但在对比学习里，前者可能与“发货速度快、体验不错”构成正样本，后者则会与“收到商品后无法使用”“质量有问题”这类负面评价更接近。模型因此会逐步学会：真正决定语义边界的，不是共享的套话，而是“包装完整”“东西是坏的”这类改变整体语义走向的区分性片段。</p>
<p>从几何角度看，对比学习学到的是一个更有结构的向量空间。语义接近的文本会在局部形成簇（Cluster），语义无关或语义相反的文本则被推向更远位置。情感分析、语义检索、重复问句检测、意图聚类之所以能直接建立在 embedding 之上，本质上就是因为模型已经把“哪些内容应当靠近、哪些内容应当远离”编码进了空间结构，而不只是输出一个任务特定的分类分数。</p>
<p>在 NLP 中，这条路线并不是突然出现的。Word2Vec 已经体现了早期的对比式思想：真实共现词是正样本，随机采样词是负样本，模型通过区分“真实上下文”和“噪声配对”学习词向量。后来的句向量和文档向量模型，则把这种思想从词级扩展到句子级和文档级：正样本可以是复述句、问答配对、查询与相关文档，负样本则是不相关句子或困难反例（Hard Negatives）。</p>
<p>一个常见形式是 InfoNCE 损失：</p>
<span displaypfx="" class="mathjax-container">\[-\log \frac{\exp(\mathrm{sim}(z_i,z_i^+)/\tau)}{\sum_{j}\exp(\mathrm{sim}(z_i,z_j)/\tau)}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(z_i\)</span> 是当前样本表示， <span displaypfx="inline-" class="mathjax-container">\(z_i^+\)</span> 是与它匹配的正样本表示， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{sim}(\cdot,\cdot)\)</span> 是相似度函数（常用余弦相似度）， <span displaypfx="inline-" class="mathjax-container">\(\tau\)</span> 是温度参数（Temperature），控制分布尖锐程度。这个目标的含义是：在一堆候选中，让正确配对拿到最高分，同时把不相关样本推远。</p>
<p>对比学习在句向量任务中的意义尤其大。交叉编码器（Cross-Encoder）把两个句子拼接后联合编码，能够做非常细的交互判断，但它直接输出的是“这一对句子有多像”，而不是可复用的独立句向量；一旦候选集合很大，计算量会迅速爆炸。双编码器（Bi-Encoder）路线则把两个文本分别编码成独立向量，再用余弦相似度或点积比较。SBERT 正是这一路线的经典代表：它通过孪生网络（Siamese Network）与对比式微调，把原本不适合作为通用句向量的 BERT 表示空间，改造成适合检索、聚类与语义匹配的 embedding 空间。</p>
<p>工程上，负样本既可以来自同一 batch 中的其他样本（In-batch Negatives），也可以来自专门构造的困难负样本（Hard Negatives）。所谓困难负样本，指的不是“完全无关”的反例，而是<span style="background-color: #c0c0c0;">在表面上很像、但语义上不应被判为同一项</span>的样本。例如检索里，与查询主题相近但并不真正回答问题的文档；句向量训练里，措辞高度相似却语义立场不同的句子；推荐里，风格相近但用户最终没有点击或转化的候选。它们之所以“困难”，正是因为模型若只依赖浅层词汇重叠、模板结构或主题相近性，很容易把这类负样本误判成正样本。</p>
<p>困难负样本的价值在于：它迫使模型放弃过于粗糙的匹配捷径，转而学习更细粒度的区分信号。随机负样本通常太容易分开，训练后期提供的梯度会迅速变弱；而困难负样本更接近真实决策边界，能持续推动表示空间学习“看起来相似但本质不同”的区别。不过它也有代价：若负样本挖掘质量不高，容易把本来就相关的样本错当成负例，形成假负样本（False Negatives），反而会伤害表示质量。因此，现代检索和 embedding 训练里，Hard Negatives 往往与 in-batch negatives、教师模型挖掘（teacher mining）或 reranker 筛选结合使用，而不是完全依赖人工拍脑袋构造。</p>
<p>CLIP、Sentence-BERT、现代检索 embedding、推荐召回模型，乃至许多 query-document dual encoder，本质上都在利用这种“正样本拉近、负样本推远”的训练逻辑。区别主要不在原理，而在样本如何构造、负样本如何选择，以及表示对象是词、句子、文档还是跨模态对。</p>
<div class="blog_h4"><span class="graybg">负采样</span></div>
<p>负采样（Negative Sampling）是与对比学习和词向量训练密切相关的一类近似策略。它的核心动机是：当候选空间极大时，没有必要每次都与所有候选比较；只保留 1 个正样本和少量负样本，就能得到足够强的判别信号。换句话说，它把原本代价高昂的“大规模归一化选择问题”，近似成若干个“真配对还是噪声配对”的二分类判断。</p>
<p>在 Word2Vec 的 Skip-gram 中，若直接对全词表做 softmax，分母需要对 <span displaypfx="inline-" class="mathjax-container">\(|{\cal V}|\)</span> 个词求和，计算代价很高。负采样则对每个正样本对 <span displaypfx="inline-" class="mathjax-container">\((w,c)\)</span> 只保留少量噪声词 <span displaypfx="inline-" class="mathjax-container">\(w_i\)</span>，并最大化：</p>
<span displaypfx="" class="mathjax-container">\[\log\sigma(v_w^\top v_c)+\sum_{i=1}^{k}\log\sigma(-v_w^\top v_{w_i})\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 是 sigmoid 函数，第一项鼓励真实配对的内积更大，第二项鼓励噪声配对的内积更小。这样一来，计算量就从与词表大小同阶，降到与 <span displaypfx="inline-" class="mathjax-container">\(1+k\)</span> 个样本同阶。负样本也不一定完全随机：Word2Vec 常按词频的 <span displaypfx="inline-" class="mathjax-container">\(0.75\)</span> 次方采样；现代对比学习则常用 in-batch negatives 或 hard negative mining。推荐系统召回、知识图谱嵌入、句向量训练等任务里，这种思想到今天仍然非常常见。</p>
<div class="blog_h3"><span class="graybg">迁移学习</span></div>
<p>迁移学习（Transfer Learning）讨论的是：先在数据更丰富、任务更通用的源任务上学到参数或表示，再把这些知识迁移到目标任务。它不是按监督信号划分出来的独立“学习方式”，而是一种<span style="background-color: #c0c0c0;">跨任务复用知识的训练策略</span>。现代大模型先预训练、再微调，本质上就是迁移学习。</p>
<p>BERT 就是这一思路的典型例子。它通常先在大规模通用文本上做语言建模预训练，例如维基百科（Wikipedia）这类覆盖面很广的语料；模型先学到词法、句法、语义关系以及上下文表示能力。随后再把这一预训练模型迁移到具体任务上，例如情感分类、自然语言推断（NLI）、命名实体识别（NER）或文本匹配，只需接上任务头并用该任务的数据继续微调，就能把通用语言知识转化为面向目标任务的能力。</p>
<p>它与对比学习不在同一层面。对比学习回答的是“预训练阶段该用什么目标来学表示”；迁移学习回答的是“学到的表示如何迁到新任务”。两者经常配合出现：例如先在海量无标签图像上用对比学习预训练视觉编码器，再把该编码器迁移到医学影像分类、工业缺陷检测或小样本识别任务上。</p>
<div class="blog_h3"><span class="graybg">少样本学习</span></div>
<p>少样本学习（Few-shot Learning）处理的是“每个任务只有极少标注样本”时如何仍然快速泛化。它通常建立在迁移学习或预训练模型之上：模型先学到一套通用表示，再在很少示例下快速适配新任务。困难不在于单个任务本身，而在于模型必须把以往经验迁移到新任务上。直觉上，它更像“学会如何快速学习”，而不是“把一个任务彻底学透”。</p>
<div class="blog_h4"><span class="graybg">零样本（Zero-shot）</span></div>
<p>零样本（Zero-shot）指模型在目标任务上没有任何专门示例，也能凭借已有知识完成任务。大语言模型通过指令理解实现的很多能力都属于这一类。例：不给任何情感分类样例，只写“判断下面评论是正面还是负面”，模型仍可能完成分类。</p>
<div class="blog_h4"><span class="graybg">单样本（One-shot）</span></div>
<p>单样本（One-shot）指只给 1 个示例。这个示例的价值不是提供统计规律，而是告诉模型“输出格式、任务边界和你想要的判别标准”。例如先给一条“商品评论 → 正面”的例子，再让模型判断下一条评论。</p>
<div class="blog_h4"><span class="graybg">K 样本（K-shot）</span></div>
<p>K 样本（K-shot）指给每类或每任务提供 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个示例。随着 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 增大，模型更容易对任务意图和判别标准形成稳定估计。工程上，prompt 中的 few-shot 示例本质上就是在上下文窗口里做一种“临时任务适配”。</p>
<div class="blog_h4"><span class="graybg">元学习（Meta-learning / MAML）</span></div>
<p>元学习（Meta-learning）研究“让模型更快适应新任务”。MAML（Model-Agnostic Meta-Learning）的核心不是直接学一个最终答案，而是学一个好的初始化参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，使模型只需少量梯度更新就能适配新任务。</p>
<p>MAML 的外层目标可概括为：</p>
<span displaypfx="" class="mathjax-container">\[\min_\theta \sum_{\mathcal{T}} \mathcal{L}_{\mathcal{T}}\big(\theta-\alpha\nabla_\theta \mathcal{L}_{\mathcal{T}}(\theta)\big)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{T}\)</span> 表示一个任务， <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 是内层更新步长。读法是：先用当前参数在某任务上走一步，再看更新后的参数在该任务上的表现好不好；如果“一步后就变好”，说明初始化是好的。类比来看，MAML 训练的不是“会做每道题的学生”，而是“只要老师讲一遍就能迅速举一反三的学生”。</p>
<div class="blog_h4"><span class="graybg">原型网络（Prototypical Networks）</span></div>
<p>原型网络（Prototypical Networks）把每个类别表示成嵌入空间中的一个“类中心（Prototype）”。对类别 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，其原型定义为该类支持集（Support Set）样本嵌入的平均：</p>
<span displaypfx="" class="mathjax-container">\[c_k=\frac{1}{|S_k|}\sum_{(x_i,y_i)\in S_k,\ y_i=k} f_\theta(x_i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 是样本的向量表示， <span displaypfx="inline-" class="mathjax-container">\(S_k\)</span> 是类别 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的支持样本集合。分类时，把新样本映射到嵌入空间，看它离哪个原型最近。直觉上，这像“每一类先算一个代表点，新样本按离哪个代表点最近来归类”。在 few-shot 图像分类中，这种方法往往比直接训练复杂分类头更稳。</p>
<div class="blog_h2"><span class="graybg">表示聚合与池化</span></div>
<p>池化（Pooling）可以先按一句人话来理解：<span style="background-color: #c0c0c0;">把一组相邻或相关的特征，压缩成更短、更稳定、更容易继续处理的摘要</span>。它不是重新发明新特征，而是对已有特征做聚合（Aggregation）或下采样（Downsampling）。这里的下采样指：<span style="background-color: #c0c0c0;">沿某些维度减少位置数或采样点数，让表示尺寸变小、分辨率变粗</span>。例如把 <span displaypfx="inline-" class="mathjax-container">\(4\times 4\)</span> 的特征图压成 <span displaypfx="inline-" class="mathjax-container">\(2\times 2\)</span>，或把一长段序列压成更短的摘要向量，都属于下采样。</p>
<p>若把一组输入特征记为 <span displaypfx="inline-" class="mathjax-container">\(x_1,\dots,x_k\)</span>，则池化可以抽象写成</p>
<span displaypfx="" class="mathjax-container">\[y=\mathrm{Pool}(x_1,\dots,x_k)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Pool}\)</span> 可以是最大值（Max Pooling）、平均值（Average Pooling）、求和（Sum Pooling）或更复杂的加权聚合。它们做的事不同，但主线一致：<span style="background-color: #c0c0c0;">把“多个位置/多个元素的表示”变成“更少的表示”</span>。</p>
<p>池化之所以重要，是因为很多任务并不需要保留每个细节位置的完整分辨率。图像分类不一定关心边缘恰好落在第 17 个还是第 18 个像素；句子分类也不一定要求记住某个情绪词出现在第 6 个还是第 7 个 token。此时，把局部细节适度压缩，往往能提升稳定性、降低计算量，并让后续层更关注“有没有出现模式”，而不是“模式的坐标是否一模一样”。</p>
<div class="blog_h3"><span class="graybg">最常见的几种池化</span></div>
<p>最大池化（Max Pooling）保留一组特征里最强的那个响应。若某个窗口里有一个边缘、某个关键词或某个邻居信号特别强，最大池化会把它留下来。它更像在问：<span style="background-color: #c0c0c0;">这一小块区域里，最显著的模式有没有出现</span>。</p>
<p>平均池化（Average Pooling）对一组特征取平均，更强调整体趋势而不是最强局部点。它更像在问：这一块区域总体上激活强不强、语义平均水平如何。</p>
<p>求和池化（Sum Pooling）常见于图网络和集合建模，用于累积总量信息。若节点数量本身有意义，求和会把“有多少邻居/总共多强”也编码进去；平均池化则更强调归一化后的平均强度。</p>
<p>全局池化（Global Pooling）表示不再只看局部窗口，而是直接把整张特征图、整段序列或整个节点集合压成一个向量。例如全局平均池化（Global Average Pooling, GAP）会把一整个空间维度平均掉，得到“每个通道在全局上的平均响应”。自适应池化（Adaptive Pooling）则把输出尺寸预先固定，例如无论输入特征图多大，最终都压成 <span displaypfx="inline-" class="mathjax-container">\(1\times 1\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(7\times 7\)</span>。</p>
<div class="blog_h3"><span class="graybg">不同网络里的含义</span></div>
<p>在卷积神经网络（CNN）里，池化最经典的含义是<span style="background-color: #c0c0c0;">沿空间维度做局部下采样</span>。例如一张特征图经过 <span displaypfx="inline-" class="mathjax-container">\(2\times 2\)</span> 最大池化后，宽高会缩小，局部最强响应被保留下来。它的直接收益有三点：减小特征图尺寸、扩大后续层的感受野（Receptive Field）、并降低模型对小幅平移和局部扰动的敏感度。</p>
<p>在时序模型和文本模型里，池化更常表示<span style="background-color: #c0c0c0;">沿时间或序列长度维度做聚合</span>。例如把所有 token 表示做平均池化，得到整句向量；把一段音频帧表示做最大池化，得到“这一整段里最强的模式”。这里池化的重点不再是二维空间下采样，而是把变长序列压成固定长度表示，方便做分类、检索或相似度计算。</p>
<p>在图神经网络（GNN）里，池化有两层常见含义。第一层是邻域聚合（Neighborhood Aggregation）：一个节点把邻居表示做均值、求和或最大值，再更新自己；这可以理解为“节点级局部池化”。第二层是图级读出（Graph-level Readout）：把整张图的节点表示再做一次全局聚合，得到整个图的表示，用于图分类、图回归等任务。</p>
<p>在 Transformer 里，池化通常不再以“池化层”这一模块形式高频出现，但概念仍然存在。句子分类常取 <span displaypfx="inline-" class="mathjax-container">\([\mathrm{CLS}]\)</span> 位置表示，或对所有 token 做平均池化；Embedding 模型也常对最后一层隐藏状态做 mean pooling / max pooling 得到句向量。进一步看，注意力（Attention）本身也可以理解成一种<span style="background-color: #c0c0c0;">带内容依赖的加权聚合</span>：区别只在于普通池化的规则通常固定，而注意力的权重是由输入动态决定的。</p>
<div class="blog_h3"><span class="graybg">池化到底保留了什么、丢掉了什么</span></div>
<p>池化保留的是摘要信息，丢掉的是更精细的位置细节。最大池化更偏向“是否出现过显著模式”，平均池化更偏向“整体平均状态如何”，求和池化则更偏向“总量有多大”。因此，池化总带有一种 trade-off：表示更紧凑、更稳、更省算力，但精确定位能力会下降。</p>
<p>这也是为什么不同任务会选择不同聚合方式。图像分类往往欢迎一定程度的位置不敏感，因此池化很自然；语义检索希望一整句压成一个句向量，因此句级池化很常见；但像语义分割、目标检测、序列标注这类任务，输出本身依赖逐位置判断，就不能过早把位置信息池掉，否则细粒度边界会被抹平。</p>
<div class="blog_h3"><span class="graybg">上采样、插值与转置卷积</span></div>
<p>若说下采样（Downsampling）是在压缩表示，那么上采样（Upsampling）就是反过来<span style="background-color: #c0c0c0;">把较粗的表示扩展回更细的空间分辨率</span>。它做的不是凭空创造新信息，而是把低分辨率特征重新铺回更高分辨率的位置网格中，方便恢复空间结构、边界或局部细节。</p>
<p>插值（Interpolation）是最直接的上采样方式。最邻近插值（Nearest Neighbor Interpolation）相当于把原像素或原特征直接复制到更密的网格里，速度快、实现简单，但边界可能显得生硬；双线性插值（Bilinear Interpolation）则按周围位置做平滑加权，结果更连续，但也更容易把边界抹平。它们的共同特点是：上采样规则是固定的，不需要学习参数。</p>
<p>转置卷积（Transposed Convolution）则是<span style="background-color: #c0c0c0;">可学习的上采样</span>。它不是“把普通卷积矩阵简单转置后就 magically 还原图像”，而是指：若把普通卷积看作一个线性算子，转置卷积对应的是这个线性算子的转置形式，因此能够把较小的特征图映射到较大的特征图。因为它带有可学习卷积核，所以模型可以学习“该怎样把粗特征展开成更适合任务的细特征”。</p>
<p>在视觉任务里，这三者的分工很常见。语义分割、图像生成、超分辨率、U-Net 解码器、扩散模型解码器都需要上采样：有时先用插值把尺寸放大，再接普通卷积细化；有时直接用转置卷积一步完成“放大 + 学习性重组”。前者通常更稳、更少棋盘格伪影（Checkerboard Artifacts），后者表达力更强，但更依赖实现细节与核大小、步幅（Stride）设置。</p>
<div class="blog_h2"><span class="graybg">泛化、过拟合与偏差—方差</span></div>
<p>机器学习更关心模型离开训练集之后能否维持稳定表现。泛化（Generalization）、过拟合（Overfitting）、欠拟合（Underfitting）与偏差—方差权衡（Bias–Variance Tradeoff）描述的，就是这件事。</p>
<div class="blog_h3"><span class="graybg">泛化</span></div>
<p>泛化（Generalization）指模型在未见样本上的表现。若训练数据与未来输入都来自同一数据分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>，则模型的总体风险可写成：</p>
<span displaypfx="" class="mathjax-container">\[R(f)=\mathbb{E}_{(x,y)\sim P}\big[\ell(f(x),y)\big]\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(R(f)\)</span> 是真实风险（Population Risk），表示模型 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在整个真实分布上的平均损失； <span displaypfx="inline-" class="mathjax-container">\(\ell(f(x),y)\)</span> 是单样本损失；期望 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}\)</span> 表示对所有可能样本做平均。训练时真正能看到的只有有限样本，因此优化的通常是经验风险 <span displaypfx="inline-" class="mathjax-container">\(\hat R_n(f)\)</span>，而不是这个理想化的总体风险。</p>
<p>训练误差与测试误差之间的差距，常称为泛化间隙（Generalization Gap）。间隙小说明模型在未见数据上比较稳定；间隙大则说明模型过度依赖训练样本中的偶然细节。</p>
<div class="blog_h3"><span class="graybg">内插与外推</span></div>
<p>内插（Interpolation）与外推（Extrapolation）描述的是：模型面对未见样本时，究竟是在<span style="background-color: #c0c0c0;">已观测范围之内补全规律</span>，还是在<span style="background-color: #c0c0c0;">已观测范围之外延伸规律</span>。二者都属于预测，但难度和风险完全不同。</p>
<p>以一维回归为例，若训练样本的输入主要落在区间 <span displaypfx="inline-" class="mathjax-container">\([a,b]\)</span> 内，那么对 <span displaypfx="inline-" class="mathjax-container">\(x\in[a,b]\)</span> 附近新样本做预测，更接近内插；对明显落在这个范围之外的 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 做预测，则更接近外推。内插依赖的是“训练数据已经覆盖了这片区域”；外推依赖的是“模型学到的规律在未见区域仍然继续成立”。后者显然要求更强。</p>
<p>在机器学习里，大多数标准泛化讨论其实都更接近内插。只要训练集与测试集近似满足独立同分布（IID）假设，测试样本通常仍然落在训练分布支持集（Support）附近，模型主要是在已知数据流形附近做平滑补全。这也是为什么现代高容量模型即使参数极多，只要训练分布覆盖充分，仍然能在测试集上表现得相当稳定。</p>
<p>外推对应的则是更困难的分布外泛化（Out-of-Distribution Generalization, OOD Generalization）。此时，测试输入不再只是训练分布中的轻微变化，而是进入了训练时很少见、甚至从未见过的区域。自动驾驶模型若只在晴天高速公路上训练，却要在暴雪、泥地和夜间乡道上决策；医学模型若主要见过成人数据，却被要求用于儿童病例；金融模型若只在平稳市场阶段训练，却要应对极端波动期，这些都属于外推问题。</p>
<p>外推之所以困难，根源在于经验风险最小化并不自动保证“规律可被安全延伸到训练分布之外”。模型完全可能在训练分布内部拟合得很好，却在一旦离开这片区域后迅速失效。因此，外推能力往往比普通测试集精度更能检验模型究竟学到了稳定结构，还是只学会了训练分布内部的高质量插值。</p>
<p>大语言模型里有一种非常典型的外推形式：长度外推（Length Extrapolation）。若模型训练时主要见到 <span displaypfx="inline-" class="mathjax-container">\(4\mathrm{K}\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(8\mathrm{K}\)</span> 上下文，却在推理时被要求处理 <span displaypfx="inline-" class="mathjax-container">\(128\mathrm{K}\)</span> 甚至更长序列，那么它面对的就不再只是“在熟悉长度范围内继续理解”，而是在训练长度之外延伸位置建模规律。RoPE 缩放、NTK-aware 调整、YaRN 等方法，本质上都是在尽量让模型把短上下文中学到的位置规律外推到更长序列上。</p>
<p>因此，可以把两者压缩成一句话：内插更像在已知区域里补全空白，外推更像拿着已总结出的规律去穿越未知边界。前者是标准机器学习评测里的常态，后者则更接近真实系统进入新环境时会遭遇的硬问题。</p>
<div class="blog_h3"><span class="graybg">过拟合</span></div>
<p>过拟合（Overfitting）指模型在训练集上持续吸收局部细节、噪声与偶然模式，但这些信息不能稳定迁移到未见样本上。它的典型外观是：训练集表现继续改善，而验证集或测试集表现停止改善、开始恶化，二者之间的泛化间隙（Generalization Gap）不断扩大。</p>
<p>过拟合描述的是一种训练现象，本身并不自动等于“模型已经不可用”。更准确的分析方式，是区分<span style="background-color: #c0c0c0;">模型到底过拟合了什么</span>。分类任务里最常见的两类退化并不完全相同：一类是决策边界本身开始贴合训练集偶然性；另一类是分类边界大体没变，但模型对既有判断越来越极端，概率校准逐步恶化。</p>
<div class="blog_h4"><span class="graybg">两种常见退化</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">退化对象</td>
<td style="text-align: center;">训练期常见信号</td>
<td style="text-align: center;">主要后果</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策边界过拟合</td>
<td>模型学到只在训练集上成立的判别规则</td>
<td>训练 F1 / Accuracy 持续上升，验证 F1 / Accuracy 停滞或下降；训练 loss 很低而验证指标回落</td>
<td>真正损害分类泛化，换一批数据就更容易判错</td>
</tr>
<tr>
<td>置信度过拟合</td>
<td>模型对已有判断越来越极端，logit 持续膨胀</td>
<td>验证 F1 基本稳定，但验证 loss、Brier Score、ECE 等校准指标恶化；预测概率更频繁地逼近 0 或 1</td>
<td>硬分类结果可能不变，但概率值本身变得不可信</td>
</tr>
</tbody>
</table>
<p>前一类退化直接伤害模型的判别泛化，后一类退化主要伤害概率校准（Calibration）。因此，若系统只需要固定阈值之后的硬标签，置信度过拟合通常是次一级问题；若系统依赖概率值本身做风险分层、阈值调度、排序融合或人工兜底，置信度过拟合就会立刻变成工程问题。</p>
<div class="blog_h4"><span class="graybg">训练期间的识别信号</span></div>
<p>识别过拟合时，不能只盯着单一 loss，而要同时观察<span style="background-color: #c0c0c0;">训练集指标、验证集指标、概率分布形状与误差结构</span>。最典型的信号包括：</p>
<ul>
<li>训练 loss 持续下降，但验证 loss 在某个阶段后停止下降，随后回升。</li>
<li>训练 Accuracy / F1 继续提高，而验证 Accuracy / F1 停滞甚至下滑。这通常提示决策边界开始贴近训练集特有模式。</li>
<li>验证 F1 基本稳定，但验证 loss 明显变差。这类“指标稳、loss 崩”的组合，更接近置信度过拟合而不是判别边界崩坏。</li>
<li>logit 绝对值持续增大，softmax 或 sigmoid 输出更集中到接近 0 和 1 的两端，说明模型在继续放大自信度。</li>
<li>训练后期错误样本逐渐集中在少量硬样本，而 easy case 的置信度仍在继续极化，说明模型已经不再学新的判别规律，而是在强化已有判断的幅度。</li>
<li>不同随机种子、不同验证切分下的波动变大，说明模型开始依赖训练样本中的偶然结构。</li>
</ul>
<div class="blog_h4"><span class="graybg">常见诱因</span></div>
<p>过拟合并不只由“参数太多”导致。更常见的诱因包括：样本量不足、类别长尾、标签噪声、训练集与线上分布不一致、数据泄露、训练时间过长、正则化过弱、batch 过小导致梯度噪声放大，以及高重复语料让模型过度记忆头部模式。对深度模型而言，容量大只是风险放大器，真正决定是否过拟合的，通常是<span style="background-color: #c0c0c0;">模型自由度与有效监督信号之间是否失衡</span>。</p>
<div class="blog_h3"><span class="graybg">欠拟合</span></div>
<p>欠拟合（Underfitting）指模型连训练数据中的主要结构都没有学出来，表现为训练集和验证集都做不好。它对应的是高偏差（High Bias）状态：<span style="background-color: #c0c0c0;">模型的平均预测长期偏离真实目标</span>，也就是模型拟合能力不够，或训练过程根本还没有进入足够低误差的区域。</p>
<p>与过拟合相比，欠拟合的特征通常不是“训练和验证拉开了差距”，而是<span style="background-color: #c0c0c0;">两边都差，而且差得很一致</span>。训练集上的 loss 仍然偏高，训练 Accuracy / F1 也上不去，说明模型尚未把任务主结构写进参数中。</p>
<div class="blog_h4"><span class="graybg">训练期间的识别信号</span></div>
<ul>
<li>训练 loss 和验证 loss 都较高，而且两者相差不大。</li>
<li>训练 Accuracy / F1 与验证 Accuracy / F1 同时偏低，没有形成明显的泛化间隙。</li>
<li>训练到后期时，两条曲线仍然一起缓慢下降，说明模型可能还没收敛；若提前停止训练，问题更接近“没训练够”。</li>
<li>即使继续训练较长时间，训练指标仍然明显低于任务应有上限，说明容量、特征或优化配置本身不足。</li>
<li>错误并不只集中在边界样本或少数难例，而是连大量 easy case 都无法稳定学会。</li>
</ul>
<div class="blog_h4"><span class="graybg">常见诱因</span></div>
<p>欠拟合常见于模型容量过小、特征表达弱、模型结构与任务不匹配、正则化过强、学习率设置不当、训练轮数不够、输入信息被过度截断，或任务本身需要非线性组合而模型只允许非常受限的线性表达。工程上，欠拟合也经常伪装成“模型很稳但一直不强”：曲线不震荡、训练不发散，却始终到不了可接受的性能区间。</p>
<div class="blog_h4"><span class="graybg">与过拟合的区分</span></div>
<p>一个实用判断准则是先看训练集是否已经被充分学会。若训练集指标本身就很差，优先考虑欠拟合；若训练集指标很好而验证集开始回落，优先考虑过拟合；若验证集分类指标基本不动，但验证 loss 与校准指标变差，则更接近置信度过拟合。把这三种状态区分清楚，后续的调参与正则化方向才不会混淆。</p>
<div class="blog_h3"><span class="graybg">坍缩（Collapse）</span></div>
<p>坍缩（Collapse）描述的是训练过程中的一种<span style="background-color: #c0c0c0;">退化解（Degenerate Solution）</span>：模型表面上仍在输出结果，但内部表示、预测分布或优化轨迹已经失去有效多样性，学习过程塌到某种简单、无用或几乎无信息的模式上。它和过拟合不同。过拟合仍然在“认真地区分训练样本”，只是把训练集细节学得过头；坍缩则意味着模型逐渐失去区分能力，或者训练目标退化为某种几乎不再提供有效学习信号的状态。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">典型表现</td>
<td style="text-align: center;">例子</td>
</tr>
</thead>
<tbody>
<tr>
<td>Mode Collapse</td>
<td>不管输入如何变化，输出都向少数模式收缩，预测类别或生成模式高度单一</td>
<td>分类器几乎把所有样本都判成“满意”；GAN 只会生成少数几种图像</td>
</tr>
<tr>
<td>Representation Collapse</td>
<td>编码器输出趋于相同或近似相同的向量，样本间表示几何结构被压扁</td>
<td>所有 hidden state / embedding 都高度相似，下游分类器只能依赖噪声做区分</td>
</tr>
<tr>
<td>Objective / Loss Collapse</td>
<td>训练目标迅速退化到近乎恒定的无信息状态，loss 长时间停在极低、极高或近乎不变的单一水平，梯度也可能同步衰减</td>
<td>自监督目标被模型用常数解“钻空子”；梯度消失后 loss 几乎不再变化；某些错误实现让目标函数被提前满足</td>
</tr>
</tbody>
</table>
<p>其中前两类最常见也最容易直观理解。Mode Collapse 强调<span style="background-color: #c0c0c0;">输出空间的多样性消失</span>；Representation Collapse 强调<span style="background-color: #c0c0c0;">内部表示空间的多样性消失</span>。第三类常被笼统地称作 loss collapse，但更准确的理解是“优化目标退化”或“训练目标坍缩”：loss 本身只是一个观测信号，真正的问题在于模型已经进入某种几乎不再产生有效学习内容的状态。</p>
<p>判断坍缩时，关键不是只看某一个时刻的平均 loss，而是看<span style="background-color: #c0c0c0;">输出分布、表示分布与样本间差异是否还存在</span>。若某个 epoch 中同时出现极低 loss 样本和极高 loss 样本，说明模型仍在把 easy case 与 hard case 区分开，训练信号仍有明显异质性；这更像正常训练中的难度分层，而不是已经坍缩。真正的坍缩通常会伴随更一致的退化迹象，例如预测类别快速单一化、embedding 方差急剧缩小、梯度长期接近 0，或者 loss 在大多数样本上收缩到近乎同一个无信息水平。</p>
<p>坍缩在不同任务中的诱因并不相同。对比学习（Contrastive Learning）里若缺少 stop-gradient、predictor、负样本或方差保持机制，表示空间很容易整体塌平；生成模型里，判别器与生成器失衡会诱发 mode collapse；分类任务里，极端类别不平衡、错误的损失实现、过强正则化或训练数据本身标签塌缩，都可能把模型推向低信息输出。工程上监控坍缩，通常需要同时看 loss、预测类别分布、embedding 方差、梯度范数与验证集指标，而不能只盯着单一数值曲线。</p>
<div class="blog_h3"><span class="graybg">偏差与方差</span></div>
<p>偏差（Bias）与方差（Variance）是分析泛化误差来源的经典视角。对平方损失（Squared Loss），常见分解写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}\big[(Y-\hat f(X))^2\big]=\mathrm{Bias}^2+\mathrm{Variance}+\sigma^2\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[(Y-\hat f(X))^2]\)</span> 是模型的平均平方误差； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Bias}^2\)</span> 表示模型平均预测与真实函数之间的系统性偏离； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Variance}\)</span> 表示模型对训练样本波动的敏感度； <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 是数据本身不可约的噪声（Irreducible Noise），即使模型和训练过程都完美，也无法完全消除。</p>
<p>更直白地说，<span style="background-color: #c0c0c0;">偏差看的是“模型平均预测离真实值有多远”，代表模型的拟合能力；方差看的是“换一份训练集后模型预测会抖动多大”，代表模型的稳定性</span>。偏差高通常对应欠拟合，因为模型学不到足够有效的数据规律；方差高通常对应过拟合，因为模型过度依赖某一份训练集的细节，离开训练集后泛化能力变差。</p>
<p>工程上，降低偏差常靠更强模型、更好特征和更充分训练；降低方差常靠更多数据、正则化、数据增强、早停（Early Stopping）和集成学习（Ensemble Learning）。很多建模决策，本质上都是在“预测还不够准”和“预测太不稳定”之间做权衡。下表用几类典型树模型把这种权衡具体化。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">偏差与方差的典型状态</td>
<td style="text-align: center;">为什么会这样</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策树</td>
<td>高偏差 + 高方差风险并存</td>
<td>没有集成机制时，树过浅会欠拟合、偏差高；树过深又会对训练集细节极敏感、方差高，因此属于“双风险”模型</td>
</tr>
<tr>
<td>随机森林</td>
<td>低偏差 + 低方差</td>
<td>单棵深树先把偏差压低，再通过 Bagging 平均掉树与树之间的高方差</td>
</tr>
<tr>
<td>GBDT / XGBoost</td>
<td>低偏差 + 低方差</td>
<td>串行累加很多棵浅树持续降低偏差，而单棵浅树本身方差较低，再配合学习率、正则化与早停控制整体方差</td>
</tr>
</tbody>
</table>
<p>因此，偏差与方差不是抽象口号，而是在解释不同模型为什么会“学不动”或“学过头”。看树模型尤其直观：单树的问题是两头都可能出错；随机森林主要靠并行平均压方差；Boosting 家族主要靠串行纠错压偏差，再用浅树和正则化把方差稳住。</p>
<div class="blog_h2"><span class="graybg">经验风险最小化与正则化</span></div>
<p>大部分监督学习训练都可以概括成同一条主线：先定义单样本损失，再在训练集上取平均形成经验风险，最后通过优化算法把它压低。正则化（Regularization）是在这条主线之上加入额外约束，用来控制复杂度并改善泛化。</p>
<div class="blog_h3"><span class="graybg">经验风险最小化</span></div>
<p>经验风险最小化（Empirical Risk Minimization, ERM）是统计学习的基本训练原则。设训练集为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{D}=\{(x_i,y_i)\}_{i=1}^{n}\)</span>，则经验风险定义为：</p>
<span displaypfx="" class="mathjax-container">\[\hat R_n(f)=\frac{1}{n}\sum_{i=1}^{n}\ell(f(x_i),y_i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是样本数， <span displaypfx="inline-" class="mathjax-container">\(f(x_i)\)</span> 是模型对第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的预测， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实标签， <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span> 是损失函数。经验风险最小化就是在假设空间 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span> 中寻找让这条平均损失最小的函数：</p>
<span displaypfx="" class="mathjax-container">\[\hat f_{\mathrm{ERM}}=\arg\min_{f\in\mathcal{H}}\hat R_n(f)\]</span>
<p>这条原则覆盖范围极广。线性回归最小化的是平方误差经验风险，逻辑回归和多分类神经网络最小化的是交叉熵经验风险，序列标注模型最小化的是序列级条件对数似然。算法形式不同，骨架是一致的。</p>
<div class="blog_h3"><span class="graybg">正则化</span></div>
<p>正则化（Regularization）是在经验风险之外，再加入一个偏好“更简单、更平滑、更稳定”解的约束项。常见写法是：</p>
<span displaypfx="" class="mathjax-container">\[\hat f=\arg\min_{f\in\mathcal{H}}\hat R_n(f)+\lambda\,\Omega(f)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)\)</span> 是正则项（Regularizer），刻画模型复杂度； <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是正则化强度，决定“拟合训练集”和“控制复杂度”之间的权衡。 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越大，模型越保守；越小，模型越自由。</p>
<p>L2 正则（L2 Regularization）偏好较小权重，常写成 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)=\|\mathbf{w}\|_2^2\)</span>；L1 正则（L1 Regularization）偏好稀疏解，常写成 <span displaypfx="inline-" class="mathjax-container">\(\Omega(f)=\|\mathbf{w}\|_1\)</span>。更广义地看，早停、Dropout、数据增强、权重共享、标签平滑、参数冻结、低秩适配，都是在不同层面对模型自由度施加约束，因此都可以看作正则化思想的工程实现。</p>
<div class="blog_h2"><span class="graybg">IID 与分布偏移</span></div>
<p>机器学习中的很多训练与评估结论，都建立在一个默认前提上：训练样本与未来样本来自同一统计机制。这个前提通常写成 IID（Independent and Identically Distributed，独立同分布）假设。只要这个前提破坏，训练集表现与线上表现之间就可能出现明显断裂。</p>
<div class="blog_h3"><span class="graybg">IID 假设</span></div>
<p>设样本对 <span displaypfx="inline-" class="mathjax-container">\((x_i,y_i)\)</span> 来自某个联合分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>，IID 假设写成：</p>
<span displaypfx="" class="mathjax-container">\[(x_1,y_1),\dots,(x_n,y_n)\overset{\mathrm{iid}}{\sim}P(X,Y)\]</span>
<p>这里“独立（Independent）”表示一个样本是否出现，不影响另一个样本的生成；“同分布（Identically Distributed）”表示所有样本都来自同一个联合分布 <span displaypfx="inline-" class="mathjax-container">\(P(X,Y)\)</span>。这个假设让训练集平均损失能够作为总体风险的近似，也让交叉验证、置信区间和很多泛化理论成立。</p>
<p>IID 是理想化近似，而不是自然界的铁律。时间序列、推荐系统、金融交易、医疗数据、A/B 实验日志、用户行为数据，常常都存在相关性、群组效应、时间漂移或采样偏差，因此不能机械套用 IID 设定。</p>
<div class="blog_h3"><span class="graybg">分布偏移</span></div>
<p>当训练分布与测试分布不一致时，就发生了分布偏移（Distribution Shift）：</p>
<span displaypfx="" class="mathjax-container">\[P_{\mathrm{train}}(X,Y)\neq P_{\mathrm{test}}(X,Y)\]</span>
<p>分布偏移有几种常见形式。协变量偏移（Covariate Shift）指 <span displaypfx="inline-" class="mathjax-container">\(P(X)\)</span> 变化，而 <span displaypfx="inline-" class="mathjax-container">\(P(Y|X)\)</span> 基本稳定；例如线上用户年龄结构变了，但“给定用户画像时是否点击”的规律没明显变。标签偏移（Label Shift）指 <span displaypfx="inline-" class="mathjax-container">\(P(Y)\)</span> 变化，例如欺诈率在促销期突然上升。概念漂移（Concept Drift / Concept Shift）指 <span displaypfx="inline-" class="mathjax-container">\(P(Y|X)\)</span> 本身发生变化，例如垃圾邮件发送策略升级后，原来有效的文本模式不再可靠。</p>
<p>因此，训练集、验证集与测试集的切分不能只追求随机均匀，还必须尽量模拟未来部署环境。若线上是时间推进场景，测试集就应按时间后移；若线上按用户或设备泛化，切分就应按实体隔离；若业务分布持续漂移，还需要做持续监控、重训和再校准。分布偏移不是评估里的边角问题，而是机器学习系统走向生产后的主要失效来源之一。</p>
<div class="blog_h3"><span class="graybg">OOD</span></div>
<p>OOD 是 Out-of-Distribution 的缩写，意为分布外（Out-of-Distribution）。它强调的是：<span style="background-color: #c0c0c0;">当前输入已经落到训练分布覆盖较弱、甚至根本没有覆盖的区域</span>。与一般意义上的“有点噪声”不同，OOD 更像是模型被带到了一个不熟悉的世界里。</p>
<p>例如，一个文本分类模型训练时主要见到的是规范书面语，部署后却大量遇到拼写错误、口语、英文混杂、模板化投诉和新产品名称；一个视觉模型训练时主要见到晴天白昼图像，线上却开始接收夜间、雨雪和红外图像。这类输入即使形式上仍然属于同一任务，也可能已经超出训练分布支持范围。OOD 检测与分布外泛化因此成为真实系统里的关键问题：模型不只要尽量判对，还要在“不熟悉”时知道自己不熟悉。</p>
<div class="blog_h3"><span class="graybg">数据漂移</span></div>
<p>数据漂移（Data Drift）强调的是<span style="background-color: #c0c0c0;">线上数据分布会随着时间持续变化</span>。它和 OOD 高度相关，但语境更偏工程系统：不是某一个样本偶然跑出了训练分布，而是整体数据来源、用户群体、业务流程或采集方式正在逐步改变。</p>
<p>若输入分布 <span displaypfx="inline-" class="mathjax-container">\(P(X)\)</span> 发生变化，常称为数据漂移或协变量漂移；若标签分布 <span displaypfx="inline-" class="mathjax-container">\(P(Y)\)</span> 变化，常表现为类别比例变化；若 <span displaypfx="inline-" class="mathjax-container">\(P(Y|X)\)</span> 也改变，则更接近概念漂移。现实系统里，这些变化往往同时发生。例如促销活动带来全新的用户结构，新功能改变用户行为路径，标注口径调整导致同类样本的标签规则也跟着变化。数据漂移的工程含义很直接：离线验证通过，并不意味着模型可以长期稳定在线上工作，监控、告警、回灌和重训机制必须跟上。</p>
<div class="blog_h3"><span class="graybg">鲁棒性</span></div>
<p>鲁棒性（Robustness）指模型在噪声、扰动、输入变形和分布变化下，性能是否仍能维持稳定。它关心的不只是“在标准测试集上最高能到多少分”，更关心<span style="background-color: #c0c0c0;">输入一旦变脏、变偏、变怪，模型会不会立刻失效</span>。</p>
<p>鲁棒性与 OOD、数据漂移不是同一概念，但三者紧密相关。OOD 和数据漂移描述的是输入环境发生了什么变化；鲁棒性描述的是模型面对这些变化时的承受能力。一个鲁棒性差的模型，可能在干净样本上分数很高，却会被轻微拼写错误、格式扰动、图像模糊、特征缺失或采样偏移迅速击穿。真实生产系统里，鲁棒性通常比单次 benchmark 分数更接近“模型能否长期可用”这个问题。</p>
<div class="blog_h2"><span class="graybg">数据集工程</span></div>
<p>数据集工程（Dataset Engineering）决定了模型看到什么、以什么尺度看到、又会被哪些偏差误导。很多所谓“模型问题”，根源其实是数据问题：标签噪声、分布漂移、类别极不均衡或特征泄漏，都会直接扭曲训练结果。</p>
<div class="blog_h3"><span class="graybg">黄金/白银数据集</span></div>
<p>数据集工程里常见一个实用分层：黄金数据集（Gold Dataset）与白银数据集（Silver Dataset）。黄金数据集通常指由高质量人工标注、规则严格审核或专家确认得到的小而精数据，标签噪声低，适合做最终评测、关键验证集或高价值监督信号；白银数据集则通常来自启发式规则、弱监督（Weak Supervision）、模型打标、日志回收或大规模自动清洗，规模更大、成本更低，但噪声也更高。实际工程中，常见策略不是二选一，而是用白银数据集提供覆盖面和规模，用黄金数据集提供校准、纠偏与最终可信评估。</p>
<div class="blog_h3"><span class="graybg">数据划分</span></div>
<p>数据划分的目标，是把<span style="background-color: #c0c0c0;">学参数、做模型选择、汇报最终结果</span>这三件事严格隔离开。若同一批数据既用来训练参数，又用来调超参数，最后还拿来汇报效果，评估结果通常会乐观得不真实，因为模型已经间接“看过”了答案。</p>
<p>从统计学习角度看，这三类数据分别承担三种不同职责：训练集负责让模型学习参数；验证集负责帮助人或训练流程做工程决策；测试集负责模拟真正的未知数据，给出最后一次、尽量无偏的泛化评估。三者分工清楚，模型评估才有可信度。</p>
<div class="blog_h4"><span class="graybg">训练集</span></div>
<p>训练集（Training Set）用于更新模型参数。监督学习中，训练集包含输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 与标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>；模型在这批样本上计算损失（Loss）、反向传播梯度（Gradient）并更新参数，因此训练集直接决定“模型学到了什么”。它回答的问题是：<span style="background-color: #c0c0c0;">在已观测样本上，模型有没有学会输入与输出之间的对应关系</span>。</p>
<p>训练集通常应占数据的大头，因为参数学习需要足够多的样本来稳定估计模式。实践中常见比例是 70% 到 80%，但这不是固定规则：若数据总量非常大，训练集比例可以更高；若数据本来就少，则往往需要把更多精力放在交叉验证（Cross Validation）而不是死守固定比例。</p>
<p>训练集上的误差通常是三者里最低的，这并不说明模型已经具有泛化能力。一个模型完全可能在训练集上表现极好，却只是记住了样本中的噪声与偶然性。训练集成绩更多反映“拟合能力”，而不是“真实上线表现”。</p>
<div class="blog_h4"><span class="graybg">验证集</span></div>
<p>验证集（Validation Set）用于模型选择（Model Selection）和超参数调优（Hyperparameter Tuning）。它不直接参与参数更新，但会影响训练流程中的关键决策，例如学习率（Learning Rate）、正则化强度、模型深度、树的数量、batch size、阈值选择，以及是否执行 Early Stopping。</p>
<p>验证集回答的问题不是“模型能不能学会”，而是：<span style="background-color: #c0c0c0;">在若干候选配置里，哪一个更可能在新数据上表现最好</span>。因此，验证集像训练过程中的“模拟考试”：它不是最终成绩单，但会决定你在训练期间如何改模型、如何调参数、何时停止训练。</p>
<p>验证集通常占总数据的 10% 到 15% 左右。若数据量很小，单独留出一份验证集的代价会较高，此时更常见的做法是使用 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 折交叉验证，让每个样本轮流充当验证数据，以减少一次随机划分带来的偶然性。交叉验证的细节放在后面的“模型评估”部分展开。</p>
<div class="blog_h4"><span class="graybg">测试集</span></div>
<p>测试集（Test Set）用于最终评估模型的泛化能力（Generalization）。它应尽量只在方案冻结之后使用：模型结构、超参数、训练策略、阈值和后处理规则都不再修改时，才在测试集上做一次最终评估。它回答的问题是：<span style="background-color: #c0c0c0;">如果把模型部署到真实世界，它在新样本上的表现大致会怎样</span>。</p>
<p>测试集通常占总数据的 10% 到 15%。它的重要性不在于比例有多大，而在于它必须保持“未参与决策”。如果开发过程中反复查看测试集结果，并据此继续改模型，那么测试集就已经被污染，不再是独立评测，而变成了另一个隐性的验证集。</p>
<p>因此，测试集更像真正的“高考卷”或“盲测集”：它的价值在于最后一次、尽量无偏的评估，而不是参与训练流程本身。</p>
<div class="blog_h4"><span class="graybg">如何划分数据集</span></div>
<p>最常见的简单划分是训练集 / 验证集 / 测试集 = 70% / 15% / 15%，或 80% / 10% / 10%。这种划分适合样本量较大、类别分布较稳定的任务，因为单次随机切分已经足以给出相对稳定的训练与评估结果。</p>
<p>当数据量较小、类别极不平衡、或者不同子群体差异明显时，划分策略就必须更谨慎。分类任务常采用分层抽样（Stratified Split），确保训练、验证、测试三部分的类别比例大致一致；时间序列任务则必须按时间顺序切分，避免未来信息泄漏到过去；用户级、设备级、病人级任务常需要按实体分组切分，防止同一实体的样本同时出现在训练集和测试集中，造成过于乐观的结果。</p>
<p>因此，“如何划分”本身就是建模的一部分。划分方式若与真实部署场景不一致，即使指标很好，也可能只是评估设定过于宽松，而不是真正泛化能力强。</p>
<div class="blog_h4"><span class="graybg">数据泄露</span></div>
<p>数据泄露（Data Leakage）指测试集或验证集中的信息以直接或间接方式进入训练过程，从而导致模型评估结果虚高。它的危险不在于“代码报错”，而在于模型会表现得看似极好，却无法在真实新数据上复现。</p>
<p>最常见的数据泄露有几类。第一类是<span style="background-color: #c0c0c0;">先对全量数据做预处理，再切分数据</span>，例如先用全量数据计算标准化均值和方差，再划分训练 / 测试集；这样测试集的信息已经进入了训练流程。第二类是<span style="background-color: #c0c0c0;">用测试集反复调参</span>，例如每改一次模型就看一次测试集成绩，直到测试集最好看为止。第三类是<span style="background-color: #c0c0c0;">特征中混入未来或标签信息</span>，例如用预测时不可能知道的字段做输入，或把目标变量的某种变形偷偷带进特征。</p>
<p>避免数据泄露的原则只有一句：<span style="background-color: #c0c0c0;">任何依赖数据分布统计量、特征构造规则、模型选择决策或阈值选择的步骤，都只能在训练集内部完成，再把同样的变换应用到验证集和测试集</span>。标准化、特征选择、缺失值填补、目标编码（Target Encoding）、降维（PCA）和重采样（Resampling）都要遵守这一原则。</p>
<p>因此，数据集划分不只是“把数据分三份”这么简单，而是整个实验设计（Experimental Design）的一部分。只有训练集、验证集、测试集的职责边界清晰，交叉验证使用得当，且数据泄露被严格控制，模型指标才具有解释价值和可复现实验意义。</p>
<div class="blog_h3"><span class="graybg">归一化与标准化</span></div>
<p>归一化（Normalization）与标准化（Standardization）都在解决“不同特征量纲和尺度差异过大”问题，但含义不同。最常见的最小-最大归一化把数据映射到固定区间：</p>
<span displaypfx="" class="mathjax-container">\[x'=\frac{x-x_{\min}}{x_{\max}-x_{\min}}\]</span>
<p>它把特征压到 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span>，适合像像素值、比例值这类天然有上下界的量。标准化则是减去均值、再除以标准差：</p>
<span displaypfx="" class="mathjax-container">\[z=\frac{x-\mu}{\sigma}\]</span>
<p>标准化后的特征均值为 0、标准差为 1，更适合线性模型、距离模型和很多神经网络优化过程。类比来看，归一化像“把不同长度的尺子都缩到同一长度区间”；标准化像“先平移到共同中心，再按波动尺度统一单位”。</p>
<div class="blog_h3"><span class="graybg">特征工程</span></div>
<p>特征工程（Feature Engineering）是把原始数据加工成更利于模型学习的表示。它不是“多造点列”这么简单，而是在把领域知识编码进输入空间。例：时间戳可以拆成小时、星期、是否节假日；用户行为日志可以构造近 7 天点击次数、转化率、时间衰减统计；文本可以做 TF-IDF、n-gram 或实体抽取。</p>
<p>类比来看，特征工程像做菜前的备料：同样的原料，如果已经切片、去骨、配好比例，后续烹饪会顺畅得多。经典机器学习对特征工程高度依赖；深度学习则把一部分特征学习自动化了，但在表格数据、推荐、广告和风控里，特征工程仍然决定上限。</p>
<div class="blog_h3"><span class="graybg">类别不平衡处理</span></div>
<p>类别不平衡（Class Imbalance）指某些类别样本远多于另一些类别。欺诈检测、故障检测、医学筛查里最典型：正类往往极少。如果不处理，模型可能通过“永远预测多数类”获得看似不错的 Accuracy，却在关键少数类上彻底失效。</p>
<p>常见处理方法包括：重采样（过采样少数类、欠采样多数类）、类别加权（Class Weighting）、阈值调整（Threshold Tuning）和使用更合适的指标（如 Precision、Recall、PR-AUC）。例如在信用卡欺诈场景中，正类只占 0.1%，此时“全判正常”会有 99.9% Accuracy，但业务价值几乎为 0。</p>
<div class="blog_h2"><span class="graybg">超参数</span></div>
<p>超参数（Hyperparameters）是训练开始前由人或外部搜索过程设定的配置变量。它们与模型参数（Parameters）不同：模型参数如线性回归的权重、神经网络的矩阵和偏置，是通过训练数据学出来的；超参数则决定<span style="background-color: #c0c0c0;">模型该以什么结构、什么训练节奏、什么正则化强度去学习</span>。学习率、batch size、树深、dropout、LoRA rank 都属于超参数，而不是训练过程中直接被梯度更新出来的参数。</p>
<div class="blog_h3"><span class="graybg">什么是超参数</span></div>
<p>从作用层面看，超参数大致分成三类。第一类决定模型结构，例如树的最大深度、神经网络层数、隐藏维度、注意力头数；第二类决定优化过程，例如学习率、batch size、训练轮数、warmup 步数；第三类决定复杂度控制，例如正则化强度、dropout、weight decay、早停耐心值。它们共同定义了“模型允许学成什么样、以及训练过程会沿哪条轨迹逼近这个结果”。</p>
<p>因此，超参数优化并不是在调模型已经学到的权重，而是在搜索<span style="background-color: #c0c0c0;">哪一套训练配置更可能在验证集上泛化得最好</span>。这也是为什么超参数搜索天然依赖验证集，而不能依赖测试集。</p>
<div class="blog_h3"><span class="graybg">通用超参数</span></div>
<p>有些超参数跨很多模型家族都反复出现，它们更像训练流程级控制杆，而不是某个算法私有旋钮。最常见的一组可概括如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">超参数</td>
<td style="text-align: center;">主要控制什么</td>
<td style="text-align: center;">常见影响</td>
</tr>
</thead>
<tbody>
<tr>
<td>学习率（Learning Rate）</td>
<td>每步更新幅度</td>
<td>过大易震荡或发散，过小则收敛过慢或停在高误差区</td>
</tr>
<tr>
<td>batch size</td>
<td>每次梯度估计使用多少样本</td>
<td>影响吞吐、显存占用、梯度噪声和有效学习率范围</td>
</tr>
<tr>
<td>训练轮数 / 训练步数（Epochs / Steps）</td>
<td>训练总时长</td>
<td>过少易欠拟合，过多则更易过拟合</td>
</tr>
<tr>
<td>正则化强度（Regularization Strength）</td>
<td>复杂度惩罚有多强</td>
<td>过强会欠拟合，过弱则更易记忆训练集细节</td>
</tr>
<tr>
<td>weight decay</td>
<td>参数收缩强度</td>
<td>常用于控制神经网络权重规模与泛化</td>
</tr>
<tr>
<td>dropout</td>
<td>随机屏蔽单元的比例</td>
<td>抑制共适应，但过强会削弱表示能力</td>
</tr>
<tr>
<td>学习率调度（Scheduler）</td>
<td>训练过程中学习率如何变化</td>
<td>直接影响早期稳定性与后期收敛质量</td>
</tr>
<tr>
<td>warmup</td>
<td>前期学习率爬升过程</td>
<td>对 Transformer 和大 batch 训练尤为重要</td>
</tr>
<tr>
<td>早停耐心值（Early Stopping Patience）</td>
<td>验证集多久不提升才停止</td>
<td>影响训练预算与过拟合控制</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">模型特定超参数</span></div>
<p>另一类超参数只在特定模型家族里出现。它们往往直接对应某个算法的结构假设，因此不能简单迁移到别的模型上。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型家族</td>
<td style="text-align: center;">典型超参数</td>
<td style="text-align: center;">控制什么</td>
</tr>
</thead>
<tbody>
<tr>
<td>KNN</td>
<td><span displaypfx="inline-" class="mathjax-container">\(k\)</span>、距离度量</td>
<td>邻域大小与“相似”的定义</td>
</tr>
<tr>
<td>SVM</td>
<td><span displaypfx="inline-" class="mathjax-container">\(C\)</span>、kernel、<span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span></td>
<td>间隔惩罚与核函数形状</td>
</tr>
<tr>
<td>决策树 / 随机森林</td>
<td>max depth、min samples leaf、树数</td>
<td>树的复杂度与集成规模</td>
</tr>
<tr>
<td>Boosting / XGBoost / LightGBM</td>
<td>learning rate、树数、max depth、采样比例</td>
<td>弱学习器叠加节奏与复杂度</td>
</tr>
<tr>
<td>CNN</td>
<td>卷积核大小、通道数、stride、pooling 配置</td>
<td>局部感受野与空间降采样方式</td>
</tr>
<tr>
<td>RNN / LSTM</td>
<td>隐藏维度、层数、截断长度</td>
<td>时序记忆容量与反向传播范围</td>
</tr>
<tr>
<td>Transformer</td>
<td>层数、隐藏维度、头数、最大上下文长度</td>
<td>表示容量、并行结构与长程建模能力</td>
</tr>
<tr>
<td>PEFT / LoRA</td>
<td>rank、alpha、target modules、adapter dropout</td>
<td>低秩适配容量与写入位置</td>
</tr>
</tbody>
</table>
<p>这也是为什么“超参数”不能被理解成一张固定清单。不同模型真正敏感的旋钮并不相同。对树模型，max depth 和叶节点约束常是核心；对 Transformer，学习率、warmup、weight decay、batch 与上下文长度往往更关键；对 LoRA，rank 与挂载模块会直接决定可写入容量。</p>
<div class="blog_h3"><span class="graybg">超参数搜索</span></div>
<p>超参数搜索（Hyperparameter Search）指用验证集表现，在若干候选配置中选择更优组合。它本质上不是训练参数，而是在搜索<span style="background-color: #c0c0c0;">哪套训练配方更值得被固定下来</span>。搜索空间越大，找到更优组合的机会通常越高，但实验成本、验证集过拟合风险和复现难度也会同步上升。</p>
<div class="blog_h4"><span class="graybg">贪婪串行登山</span></div>
<p>贪婪串行登山（Greedy Sequential Hill Climbing）是一种非常实用的超参数搜索策略。它的核心规则是：<span style="background-color: #c0c0c0;">每次只调整一个超参数，在当前其余超参数固定不变的条件下，选择验证集上更优的方向走一步；确定后先固定该值，再去调下一个超参数</span>。在离散候选集上，它可以看作一种坐标式局部搜索。</p>
<p>例如先固定 dropout 和 batch size，只比较若干学习率；一旦找到当前最优学习率，就暂时锁定它，再去比较 dropout；然后再固定前两者去比较 batch size。这样做的优点是实验次数通常近似线性增长，适合“训练一次代价不低、超参数数量又不算很多”的场景。</p>
<div class="blog_h4"><span class="graybg">棘轮式锁定</span></div>
<p>贪婪串行登山常伴随一种棘轮式锁定（Ratchet-style Fixing）：某一轮一旦选定一个更优取值，就先不回头重开这个维度。这样做能显著缩小后续搜索空间，但代价也很明确：较早做出的局部最优决策，会限制后面组合空间的探索。</p>
<p>它最容易出问题的地方，是参数交互（Hyperparameter Interaction）。若两个超参数彼此强相关，例如学习率和 batch size、学习率和 warmup、LoRA rank 和 target modules，那么“在当前默认值下看起来更优”的选择，未必能和后续维度组成真正最优的整体组合。棘轮式锁定会把这类交互提前屏蔽掉。</p>
<div class="blog_h4"><span class="graybg">优点与局限</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>优点</td>
<td>简单、可解释、实验次数少，适合作为快速锁定大方向的工程基线</td>
</tr>
<tr>
<td>局限 1</td>
<td>容易停在局部最优，因为它不会接受“短期下降、长期更优”的探索路径</td>
</tr>
<tr>
<td>局限 2</td>
<td>默认把超参数近似看成可分离维度，但现实中经常存在强交互</td>
</tr>
<tr>
<td>局限 3</td>
<td>若反复依赖同一验证集做很多轮决策，更容易把验证集偶然性误判成真实提升</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">与其他搜索策略对比</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">策略</td>
<td style="text-align: center;">实验成本</td>
<td style="text-align: center;">能否捕捉参数交互</td>
<td style="text-align: center;">典型特点</td>
</tr>
</thead>
<tbody>
<tr>
<td>贪婪串行登山</td>
<td>较低</td>
<td>较弱</td>
<td>工程上快速、便宜、可解释，但更局部</td>
</tr>
<tr>
<td>网格搜索（Grid Search）</td>
<td>高，常随维度指数增长</td>
<td>强</td>
<td>穷举规则清楚，但高维时代很快失去性价比</td>
</tr>
<tr>
<td>随机搜索（Random Search）</td>
<td>可控</td>
<td>中等</td>
<td>在高维空间常比网格搜索更高效，是强基线</td>
</tr>
<tr>
<td>贝叶斯优化（Bayesian Optimization）</td>
<td>中等到较高</td>
<td>较强</td>
<td>利用历史试验结果自适应建议下一个点，适合昂贵实验</td>
</tr>
</tbody>
</table>
<p>因此，何时使用哪种策略，取决于训练代价与搜索空间形状。若一次训练就要数十分钟甚至数小时，且可调超参数并不多，贪婪串行登山往往已经足够作为第一轮工程方案；若参数交互明显、预算允许，随机搜索或贝叶斯优化通常更稳。无论使用哪一种方法，最关键的前提都不变：<span style="background-color: #c0c0c0;">搜索必须由验证集驱动，而测试集必须保持未参与决策</span>。</p>
<div class="blog_h2"><span class="graybg">模型评估</span></div>
<p>模型评估（Model Evaluation）回答的不是“模型能不能在训练集上做对”，而是“模型在新数据上是否可靠，以及错误代价如何”。不同任务对应的指标重点不同：分类关心类别区分，回归关心数值偏差，排序关心相对顺序。</p>
<div class="blog_h3"><span class="graybg">交叉验证</span></div>
<p>交叉验证（Cross Validation）在数据较少时特别重要。最常见的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 折交叉验证把数据分成 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 份：每次用其中 1 份做验证、其余 <span displaypfx="inline-" class="mathjax-container">\(K-1\)</span> 份训练，循环 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 次，最后对 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个验证结果取平均。</p>
<p>它的作用像“轮流把不同一份数据拿出来当模拟考试卷”，从而降低一次随机划分带来的偶然性。对小数据集而言，单次划分可能刚好“运气好或坏”；交叉验证则给出更稳定的泛化估计。</p>
<div class="blog_h3"><span class="graybg">校准</span></div>
<p>校准（Calibration）讨论的是：<span style="background-color: #c0c0c0;">模型给出的概率值，是否真的能当概率解释</span>。分类模型不只输出“判成哪一类”，还常输出一个置信分数，例如 <span displaypfx="inline-" class="mathjax-container">\(0.9\)</span>。若一个模型在所有“预测概率约为 0.9”的样本子集上，最终真的有约 90% 预测正确，那么它就是校准良好的；若它经常把只有 60% 把握的样本说成 90%，就属于过度自信（Overconfident）。</p>
<p>二分类中，若模型输出正类概率 <span displaypfx="inline-" class="mathjax-container">\(\hat p(x)\in[0,1]\)</span>，理想校准条件可写成：</p>
<span displaypfx="" class="mathjax-container">\[P(Y=1\mid \hat p(X)=p)=p\]</span>
<p>这里左边表示：在所有预测概率等于 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 的样本中，真实为正类的条件概率；右边的 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是模型自己报出的概率。两者相等时，概率输出就与真实频率一致。多分类场景下，常把模型最大类别概率当作置信度，并检验“报 80% 置信度的样本，是否真的大约 80% 正确”。</p>
<p>校准与准确率不是同一件事。一个模型可以分类很准，但概率不可靠；也可以概率尺度较准，但分类边界并不最优。前者常见于深层神经网络：argmax 分类结果不错，但 softmax 概率偏尖，置信度系统性偏高。涉及风险控制、医学筛查、自动驾驶、检索重排、多阶段决策时，概率是否可信往往和“分对多少”同样重要，因为阈值决策、人工复核和代价加权都依赖这个概率尺度。</p>
<p>校准的可视化工具通常是可靠性图（Reliability Diagram）。做法是把预测置信度分成若干区间，例如 <span displaypfx="inline-" class="mathjax-container">\([0.0,0.1),[0.1,0.2),\dots\)</span>，然后对每个区间分别计算平均置信度与真实准确率。若图上的点接近对角线 <span displaypfx="inline-" class="mathjax-container">\(y=x\)</span>，说明校准较好；若点普遍落在对角线下方，说明模型报得比实际更自信；若点在对角线上方，则说明模型偏保守。</p>
<p>常用数值指标是期望校准误差（Expected Calibration Error, ECE）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ECE}=\sum_{m=1}^{M}\frac{|B_m|}{n}\,\big|\mathrm{acc}(B_m)-\mathrm{conf}(B_m)\big|\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 是置信度分箱数， <span displaypfx="inline-" class="mathjax-container">\(B_m\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 个置信度区间中的样本集合， <span displaypfx="inline-" class="mathjax-container">\(|B_m|\)</span> 是该区间样本数， <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是总样本数， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{acc}(B_m)\)</span> 是该区间的实际准确率， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{conf}(B_m)\)</span> 是该区间的平均预测置信度。ECE 的含义很直接：把每个置信区间里“说得多准”和“实际多准”的差值取绝对值，再按样本占比加权平均。ECE 越小，表示整体校准越好。</p>
<p>另一类常见指标是 Brier Score。对二分类，它定义为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Brier}=\frac{1}{n}\sum_{i=1}^{n}(\hat p_i-y_i)^2\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\hat p_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的预测正类概率， <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{0,1\}\)</span> 是真实标签。它既惩罚分类错误，也惩罚概率刻度不准，因此兼顾区分能力与概率质量。与单纯 Accuracy 不同，Brier Score 会区分“错得有多离谱”：把一个负样本报成 <span displaypfx="inline-" class="mathjax-container">\(0.51\)</span> 和报成 <span displaypfx="inline-" class="mathjax-container">\(0.99\)</span>，代价并不相同。</p>
<p>工程上最常见的后处理方法是温度缩放（Temperature Scaling）。设原始 logits 为 <span displaypfx="inline-" class="mathjax-container">\(z_i\)</span>，则缩放后的 softmax 概率写成：</p>
<span displaypfx="" class="mathjax-container">\[p_i=\frac{\exp(z_i/T)}{\sum_j \exp(z_j/T)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T&gt;0\)</span> 是温度参数。 <span displaypfx="inline-" class="mathjax-container">\(T&gt;1\)</span> 会把分布拉平，降低过度自信； <span displaypfx="inline-" class="mathjax-container">\(T&lt;1\)</span> 会把分布压尖，提高置信度。温度参数通常在验证集上通过最小化负对数似然（Negative Log-Likelihood, NLL）来拟合，然后固定用于测试或部署阶段。它不会改变类别排序，因此常能在几乎不影响 Accuracy 的前提下改善概率校准。</p>
<div class="blog_h3"><span class="graybg">分类指标</span></div>
<div class="blog_h4"><span class="graybg">混淆矩阵（Confusion Matrix）</span></div>
<p>分类指标通常从混淆矩阵（Confusion Matrix）出发。设正类预测结果统计为真阳性 <span displaypfx="inline-" class="mathjax-container">\(TP\)</span>、假阳性 <span displaypfx="inline-" class="mathjax-container">\(FP\)</span>、真阴性 <span displaypfx="inline-" class="mathjax-container">\(TN\)</span>、假阴性 <span displaypfx="inline-" class="mathjax-container">\(FN\)</span>。不同指标本质上是在回答不同问题：是看“总共判对多少”，还是看“判成正类时有多准”，还是看“真实正类抓到了多少”。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/confusion-matrix.png"><img class="alignnone size-full wp-image-41785" src="https://blog.gmem.cc/wp-content/uploads/2026/03/confusion-matrix.png" alt="confusion-matrix" width="1024" height="776" /></a></p>
<div class="blog_h4"><span class="graybg">Accuracy</span></div>
<p>准确率（Accuracy）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Accuracy}=\frac{TP+TN}{TP+TN+FP+FN}\]</span>
<p>它衡量“总体上判对了多少比例”，适合类别相对平衡、不同错误代价接近的场景。但在类别极不平衡时会误导：例如癌症筛查里，99% 都是阴性时，模型全判阴性也可能有 99% Accuracy，却毫无检测价值。</p>
<div class="blog_h4"><span class="graybg">Precision</span></div>
<p>精确率（Precision）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Precision}=\frac{TP}{TP+FP}\]</span>
<p>它回答的是：“所有被模型判成正类的样本里，有多少真的为正。”当误报成本很高时，Precision 特别重要。例：垃圾邮件过滤里，如果把正常邮件误判成垃圾邮件代价很高，就要关心 Precision。</p>
<div class="blog_h4"><span class="graybg">Recall</span></div>
<p>召回率（Recall）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Recall}=\frac{TP}{TP+FN}\]</span>
<p>它回答的是：“所有真实正类里，有多少被模型找出来了。”当漏报成本很高时，Recall 更关键。例：医学筛查里漏掉患者可能比多做一次复检更危险，因此 Recall 往往比 Precision 更重要。</p>
<div class="blog_h4"><span class="graybg">F1 Score</span></div>
<p>F1 值（F1 Score）是 Precision 与 Recall 的调和平均：</p>
<span displaypfx="" class="mathjax-container">\[F_1=\frac{2\cdot \mathrm{Precision}\cdot \mathrm{Recall}}{\mathrm{Precision}+\mathrm{Recall}}\]</span>
<p>之所以用调和平均而不是普通平均，是因为它会惩罚“一高一低”的不平衡情况。若一个模型 Precision 极高但 Recall 很低，它并不能拿到高 F1。F1 适合正负样本不平衡、且希望兼顾漏报与误报的场景。</p>
<div class="blog_h4"><span class="graybg">AUC-ROC</span></div>
<p>AUC-ROC 衡量模型在不同分类阈值下区分正负样本的整体能力。ROC 曲线横轴是假阳性率（False Positive Rate），纵轴是真阳性率（True Positive Rate）。AUC 是曲线下面积，范围在 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span>；越接近 1，说明模型越能把正样本排在负样本前面。</p>
<p>它不依赖某一个固定阈值，因此适合比较“排序能力”。但在极端不平衡数据上，PR 曲线（Precision-Recall Curve）常更敏感，因为 ROC 容易被大量真阴性“冲淡”。</p>
<div class="blog_h3"><span class="graybg">回归指标</span></div>
<p>回归指标（Regression Metrics）衡量预测值与真实值之间的数值偏差。它们关注的不是“判对类别”，而是“偏差有多大、对大误差是否敏感、模型解释了多少波动”。以房价预测为例，预测 300 万和真实 320 万之间的差距，就是典型回归误差。</p>
<div class="blog_h4"><span class="graybg">MAE</span></div>
<p>平均绝对误差（Mean Absolute Error, MAE）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{MAE}=\frac{1}{N}\sum_{i=1}^{N}|\hat y_i-y_i|\]</span>
<p>它直接度量“平均差了多少个原始单位”，解释最直观。若房价单位是万元，MAE=12 就表示平均误差约 12 万元。由于使用绝对值，MAE 对离群点没有 MSE 那么敏感。</p>
<div class="blog_h4"><span class="graybg">MSE</span></div>
<p>均方误差（Mean Squared Error, MSE）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{MSE}=\frac{1}{N}\sum_{i=1}^{N}(\hat y_i-y_i)^2\]</span>
<p>平方会放大大误差，因此 MSE 对离群点更敏感。它常用于你希望“大错要被重罚”的场景。高斯噪声假设下，最小化 MSE 还对应最大似然估计，因此它不仅是工程指标，也是概率建模结果。</p>
<div class="blog_h4"><span class="graybg">RMSE</span></div>
<p>均方根误差（Root Mean Squared Error, RMSE）是</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{RMSE}=\sqrt{\mathrm{MSE}}\]</span>
<p>它保留了 MSE 对大误差更敏感的性质，同时把单位拉回原始量纲，因此更易解释。若房价 RMSE 为 20 万元，可以直接理解为“典型误差量级约 20 万元”。</p>
<div class="blog_h4"><span class="graybg">R²</span></div>
<p><span displaypfx="" class="mathjax-container">\[R^2\]</span>（决定系数，Coefficient of Determination）回答的是：<span style="background-color: #c0c0c0;">相比于最朴素的瞎猜基线，你的回归模型到底把预测提升了多少</span>。要理解它，只需要在脑子里放两条线：一条是“什么都不知道时只能猜平均值”的水平线，另一条是模型给出的预测曲线。</p>
<p>先看最朴素的基线。假设你要预测一批房子的价格，但你手里没有面积、地段、楼龄这些特征，别人却逼着你给出预测。此时最不容易挨打的办法，不是胡乱报一个数字，而是对所有房子都猜样本平均价 <span displaypfx="inline-" class="mathjax-container">\(\bar y\)</span>。在散点图上，这对应一条横向的水平线。</p>
<p>真实房价 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 会散落在这条平均线的上下。每个点到平均线的垂直距离 <span displaypfx="inline-" class="mathjax-container">\(y_i-\bar y\)</span>，就是“瞎蒙平均值”时犯下的误差。把这些误差平方后全部加起来，就得到</p>
<span displaypfx="" class="mathjax-container">\[\sum_i(y_i-\bar y)^2\]</span>
<p>这就是公式里的分母。它衡量的不是模型误差，而是这批数据本身原来就有多分散、多混乱。也可以把它理解为目标变量的<span style="background-color: #c0c0c0;">总波动</span>、总混沌程度，或者说“在完全不用特征时，世界原本有多少东西解释不了”。</p>
<p>现在再看你的模型。你训练出一个回归模型，它根据输入特征给出预测 <span displaypfx="inline-" class="mathjax-container">\(\hat y_i\)</span>。在图上，这不再是那条死板的水平线，而是一条试图穿过散点云中心的预测曲线。模型当然不可能完美，所以每个真实值 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 与预测值 <span displaypfx="inline-" class="mathjax-container">\(\hat y_i\)</span> 之间仍会有垂直误差，这个误差就是残差（Residual）。</p>
<p>把这些模型仍然没解释掉的误差平方后加起来，就得到</p>
<span displaypfx="" class="mathjax-container">\[\sum_i(\hat y_i-y_i)^2\]</span>
<p>这就是公式里的分子，也叫残差平方和（Residual Sum of Squares, RSS）。它代表模型已经尽力之后，世界上<span style="background-color: #c0c0c0;">依然残存的混沌</span>。分子越小，说明模型越贴近真实数据；分子越大，说明模型虽然复杂，但其实没把问题解释清楚。</p>
<p>于是</p>
<span displaypfx="" class="mathjax-container">\[R^2=1-\frac{\sum_i(\hat y_i-y_i)^2}{\sum_i(y_i-\bar y)^2}\]</span>
<p>这条式子就可以直接读成一句大白话：先看模型还剩下多少解释不了的波动，再除以最开始总共有多少波动，得到“模型搞不定的比例”；最后用 1 减掉它，剩下的就是<span style="background-color: #c0c0c0;">模型成功解释掉的波动比例</span>。</p>
<p>因此，若 <span displaypfx="inline-" class="mathjax-container">\(R^2=0.8\)</span>，意思不是“正确率 80%”，而是目标变量原本有 100 份波动，模型大约解释掉了其中 80 份，只剩 20 份还没解释；若 <span displaypfx="inline-" class="mathjax-container">\(R^2=0\)</span>，说明你的模型折腾半天，效果和“永远预测平均值”完全一样；若 <span displaypfx="inline-" class="mathjax-container">\(R^2&lt;0\)</span>，则表示模型比这个最朴素基线还差，常见原因是模型设错了、特征没信息，或实现上有 bug。</p>
<p>从老板视角看， <span displaypfx="inline-" class="mathjax-container">\(R^2\)</span> 的灵魂拷问其实只有一句：<span style="background-color: #c0c0c0;">相比直接拿平均值糊弄事，你这个复杂回归模型到底多解释了多少真实波动</span>。这也是为什么 <span displaypfx="inline-" class="mathjax-container">\(R^2\)</span> 特别适合回答“模型有没有真正利用特征学到东西”，但它不能替代 MAE、RMSE——因为 <span displaypfx="inline-" class="mathjax-container">\(R^2\)</span> 讲的是解释比例，而不是误差到底有多少个原始单位。</p>
<div class="blog_h2"><span class="graybg">优化算法</span></div>
<p>优化算法（Optimization Algorithms）解决的问题非常朴素：<span style="background-color: #c0c0c0;">模型参数该往哪个方向改，才能让损失函数持续下降</span>。只要训练目标能写成“最小化某个损失函数”，背后就需要一套更新参数的规则。线性回归、逻辑回归、神经网络、大语言模型训练，本质上都绕不开这个问题。</p>
<p>这里要先分清两件事。优化（Optimization）关心的是“训练损失能不能降下来”；泛化（Generalization）关心的是“模型在新样本上好不好”。一个优化器可能把训练集拟合得很好，但泛化依然一般；反过来，一个优化器如果连训练损失都压不下去，模型通常也谈不上有效。因此优化算法决定的是<span style="background-color: #c0c0c0;">你如何走向一个解</span>，而不是直接保证这个解一定最好。</p>
<div class="blog_h3"><span class="graybg">梯度下降</span></div>
<p>梯度下降（Gradient Descent）是最核心的一阶优化思想。设损失函数为 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span>，参数为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，则梯度 <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta L(\theta)\)</span> 给出“损失上升最快的方向”。既然梯度指向上坡，那么要让损失下降，就应沿着它的反方向更新参数：</p>
<span displaypfx="" class="mathjax-container">\[\theta_{t+1}=\theta_t-\eta\,\nabla_\theta L(\theta_t)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步的参数， <span displaypfx="inline-" class="mathjax-container">\(\eta\)</span> 是学习率（Learning Rate），决定每一步走多大； <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta L(\theta_t)\)</span> 是当前位置的斜率信息。学习率太大，容易一步跨过谷底甚至震荡发散；学习率太小，又会下降得极慢。它像蒙着雾下山：梯度告诉你脚下哪边更陡，学习率决定你每次迈多大步。</p>
<p>为什么很多模型不直接“解公式”，而要反复迭代？因为在深度学习里，参数维度极高，损失面又往往非凸（Non-convex），通常没有漂亮的闭式解（Closed-form Solution）。这时最现实的办法不是一次算出全局最优，而是利用局部斜率，一步一步把损失往下压。</p>
<p>当总损失是逐样本损失的平均时，梯度下降还可以写得更具体：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\ell(\theta;x_i)\]</span>
<span displaypfx="" class="mathjax-container">\[\nabla_\theta L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\nabla_\theta \ell(\theta;x_i)\]</span>
<p>这也回答了“为什么可以批量喂输入”：梯度对求和是线性的，<span style="background-color: #c0c0c0;">整体梯度等于逐样本梯度的平均</span>，所以可以并行算每个样本的梯度，再求平均后统一更新参数。</p>
<p>满足这种“逐样本求和/求平均”结构的损失函数其实非常多。典型例子包括：线性回归里的均方误差（MSE）、平均绝对误差（MAE），二分类里的 logistic loss / binary cross-entropy，多分类里的交叉熵（Cross-Entropy），语言模型训练里的负对数似然（Negative Log-Likelihood, NLL）。它们都可以写成“每个样本先各自算一份损失，再在整个数据集上取平均”的形式，因此天然适合 mini-batch 训练。</p>
<p>但并不是所有目标都能严格写成这种完全独立的逐样本平均。若损失显式依赖<span style="background-color: #c0c0c0;">样本之间的相对关系</span>，情况就会复杂一些。例如排序学习里的 pairwise/listwise loss、度量学习中的 triplet loss，以及对比学习里的 InfoNCE，都会让一个样本的损失依赖同一个 batch 中的其他样本。此时虽然仍然可以按 batch 计算梯度，但它已经不是“每个样本各算各的、最后简单平均”那么干净的分解了。</p>
<p>工程上还常见另一种情况：目标函数可以写成“逐样本平均损失 + 正则项（Regularization）”。例如</p>
<span displaypfx="" class="mathjax-container">\[L(\theta)=\frac{1}{N}\sum_{i=1}^{N}\ell(\theta;x_i)+\lambda\Omega(\theta)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)\)</span> 可以是 <span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_2^2\)</span> 这类参数惩罚项。前半部分仍然按样本分解，后半部分则是直接作用于参数本身，而不对应某一个单独样本。很多现代训练目标，本质上都是这两部分的组合。</p>
<p>几个常用训练量需要先分清：</p>
<ul>
<li>批大小（Batch Size）：一次更新使用多少个样本。</li>
<li>步（Step / Iteration）：一次参数更新。</li>
<li>轮（Epoch）：完整遍历一次训练集。</li>
</ul>
<p>若训练集大小为 <span displaypfx="inline-" class="mathjax-container">\(N\)</span>，batch size 为 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>，则每个 epoch 的步数约为 <span displaypfx="inline-" class="mathjax-container">\(\lceil N/B\rceil\)</span>。工程里经常会说“训练了多少 step”，因为真正发生参数变化的是 step，而不是 epoch 这个更粗的计数单位。</p>
<div class="blog_h4"><span class="graybg">批量梯度下降（BGD）</span></div>
<p>批量梯度下降（Batch Gradient Descent, BGD）每次都用<span style="background-color: #c0c0c0;">整个训练集</span>来计算一次精确梯度，再更新参数。这里的 batch 指的不是现代深度学习里常说的一个 mini-batch，而是“整批训练数据”。它的优点是方向最稳定、梯度方差最小；缺点是每一步都很贵，数据一大就几乎不可用。它更适合小数据集、凸优化问题，或教材里说明“梯度下降原理”时使用。</p>
<div class="blog_h4"><span class="graybg">随机梯度下降（SGD）</span></div>
<p>随机梯度下降（Stochastic Gradient Descent, SGD）每次只用一个样本的梯度做更新。它的方向噪声很大，看起来像“跌跌撞撞地下山”，但每一步极便宜、更新极频繁，因此在数据流式到来或样本极多时很有价值。</p>
<ul>
<li>优点：更新快、内存开销小、天然适合在线学习（Online Learning）与流式数据（Streaming Data）。</li>
<li>优点：梯度噪声有时反而是好事，更容易离开鞍点（Saddle Point）和较差的局部极小。</li>
<li>缺点：轨迹抖动大，若学习率控制不好，训练容易不稳定。</li>
</ul>
<p>它像店长根据每一位新顾客的反馈立刻调价：反应很快，但也容易被个别顾客带偏。</p>
<p>适用场景：当你需要<span style="background-color: #c0c0c0;">用最新样本尽快产生参数更新</span>时，SGD（batch size=1）是最直接的选择，典型包括：</p>
<ul>
<li>在线学习（Online Learning）/流式数据（Streaming Data）：样本持续到来，要求增量更新，而不是离线反复扫全量数据。</li>
<li>分布漂移（Distribution Shift）与非平稳（Non-stationary）环境：用户偏好、市场、策略对抗等持续变化，需要快速跟踪新分布。</li>
<li>低延迟更新需求：例如广告/推荐/风控的在线校准，需要“见到一条新反馈就更新一点”。</li>
<li>资源受限或样本极大：内存无法容纳大 batch 或无法频繁计算全量梯度时，用单样本更新换取更低的每步计算与存储成本。</li>
</ul>
<p>在现代深度学习里，若使用 GPU/TPU 训练，大多数时候会用小批量梯度下降来兼顾吞吐与稳定性；纯 SGD 更常出现在在线/增量训练与部分强化学习（Reinforcement Learning, RL）设置中。</p>
<div class="blog_h4"><span class="graybg">小批量梯度下降（Mini-batch SGD）</span></div>
<p>小批量梯度下降（Mini-batch SGD）是在 BGD 与 SGD 之间折中：每次用一个 batch 的平均梯度更新。它既能利用 GPU 的并行算力，又保留一定梯度噪声，因此现代深度学习几乎都在这个范式下训练。</p>
<p>batch 太小，梯度估计噪声会很大；batch 太大，虽然每步更稳定，但显存压力更高、更新频率更低，有时还会让优化和泛化都变钝。因此 batch size 不是“越大越好”，而是吞吐、稳定性、显存和泛化之间的折中。</p>
<div class="blog_h3"><span class="graybg">动量法（Momentum）</span></div>
<p>单纯的梯度下降在“峡谷形”损失面里很容易左右来回震荡：沿陡峭方向上下摆动，沿真正有用的谷底方向前进却很慢。动量法（Momentum）的直觉是：不要只看当前这一脚的斜率，而要把过去几步的方向累积成一种“惯性”。</p>
<span displaypfx="" class="mathjax-container">\[v_{t+1}=\beta v_t+(1-\beta)g_t,\quad \theta_{t+1}=\theta_t-\eta v_{t+1}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 是当前梯度， <span displaypfx="inline-" class="mathjax-container">\(v_t\)</span> 是累计出来的速度， <span displaypfx="inline-" class="mathjax-container">\(\beta\in[0,1)\)</span> 控制“记住过去多少信息”。 <span displaypfx="inline-" class="mathjax-container">\(\beta\)</span> 越大，方向越平滑、惯性越强；越小，则越接近普通梯度下降。类比来看，它像推一个有重量的小球下山：不会因为脚下的微小凸凹就频繁改道，而会沿长期更一致的下降方向滚动。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/momentum.jpg"><img class="alignnone size-full wp-image-41017" src="https://blog.gmem.cc/wp-content/uploads/2026/03/momentum.jpg" alt="momentum" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">AdaGrad / RMSProp</span></div>
<p>AdaGrad 与 RMSProp 属于自适应学习率（Adaptive Learning Rate）方法。它们的核心思想是：<span style="background-color: #c0c0c0;">不同参数的梯度尺度不同，不应该所有维度都用同一个固定步长</span>。历史上梯度很大的维度，后续步子要缩小；梯度稀疏或很小的维度，则可以走得更积极。</p>
<p>AdaGrad 累积历史平方梯度：</p>
<span displaypfx="" class="mathjax-container">\[s_{t+1}=s_t+g_t\odot g_t,\quad \theta_{t+1}=\theta_t-\eta\,\frac{g_t}{\sqrt{s_{t+1}}+\epsilon}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(g_t\odot g_t\)</span> 表示逐元素平方， <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 是数值稳定项。这样一来，历史上梯度一直很大的维度会被自动缩小学习率。AdaGrad 在稀疏特征（Sparse Features）任务里很有效，例如早期的文本与推荐场景；但它的问题是 <span displaypfx="inline-" class="mathjax-container">\(s_t\)</span> 只增不减，训练到后期学习率可能衰减得过头，参数几乎不再动。</p>
<p>RMSProp 用指数滑动平均替代“无限累积”，缓解这个问题：</p>
<span displaypfx="" class="mathjax-container">\[s_{t+1}=\rho s_t+(1-\rho)(g_t\odot g_t),\quad \theta_{t+1}=\theta_t-\eta\,\frac{g_t}{\sqrt{s_{t+1}}+\epsilon}\]</span>
<p>这样历史信息会逐步“遗忘”，使学习率缩放更关注近期梯度尺度。可以把 AdaGrad 理解成“终身记账”，而 RMSProp 更像“滚动记账”。</p>
<div class="blog_h3"><span class="graybg">Adam</span></div>
<p>Adam（Adaptive Moment Estimation）把动量法（Momentum）的一阶矩估计与 RMSProp 的二阶矩估计结合起来，因此它同时解决优化中的两个核心痛点：<span style="background-color: #c0c0c0;">方向感（往哪走）</span>与<span style="background-color: #c0c0c0;">节奏感（每步走多大）</span>。直觉上，可以把它理解为“带惯性的方向盘 + 自动变速箱”：方向由平均梯度决定，步长由梯度的尺度自动缩放。</p>
<div class="blog_h4"><span class="graybg">一阶矩：方向感（Momentum）</span></div>
<span displaypfx="" class="mathjax-container">\[m_{t+1}=\beta_1 m_t+(1-\beta_1)g_t\]</span>
<ul>
<li><span style="background-color: #c0c0c0;">索引约定</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 次更新先在 <span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span> 处计算 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span>，再把它并入动量得到 <span displaypfx="inline-" class="mathjax-container">\(m_{t+1}\)</span>；下标 <span displaypfx="inline-" class="mathjax-container">\(t+1\)</span> 表示“更新后状态”，不是未来信息。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(m_{t+1}\)</span>：并入 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 后的一阶矩（动量）估计。</li>
<li>右边第一项 <span displaypfx="inline-" class="mathjax-container">\(\beta_1 m_t\)</span>：把历史动量按系数 <span displaypfx="inline-" class="mathjax-container">\(\beta_1\)</span> 保留下来，表示“历史方向对新方向的贡献”。</li>
<li>右边第二项 <span displaypfx="inline-" class="mathjax-container">\((1-\beta_1)g_t\)</span>：把当前梯度按系数 <span displaypfx="inline-" class="mathjax-container">\(1-\beta_1\)</span> 注入动量，表示“当前观测对新方向的贡献”。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(g_t\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步的梯度，通常由当前 mini-batch 估计得到（<span displaypfx="inline-" class="mathjax-container">\(g_t=\nabla_\theta L(\theta_t)\)</span>）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(m_t\)</span>：一阶矩估计的滑动平均（动量项），可理解为“平均梯度方向”。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\beta_1\in[0,1)\)</span>：动量衰减系数，越大表示记忆越长、方向越平滑。</li>
</ul>
<p>两项系数满足 <span displaypfx="inline-" class="mathjax-container">\(\beta_1+(1-\beta_1)=1\)</span>，因此这是指数滑动平均（Exponential Moving Average, EMA，越近发生的事情，参考价值越大；越久远的事情，参考价值越小）。当 <span displaypfx="inline-" class="mathjax-container">\(m_0=0\)</span> 时，可把它展开为：</p>
<span displaypfx="" class="mathjax-container">\[m_{t+1}=(1-\beta_1)\sum_{k=0}^{t}\beta_1^{t-k}g_k\]</span>
<p>上式说明：越久远的梯度 <span displaypfx="inline-" class="mathjax-container">\(g_k\)</span> 权重按 <span displaypfx="inline-" class="mathjax-container">\(\beta_1^{t-k}\)</span> 指数衰减；经验上可把有效记忆长度理解为 <span displaypfx="inline-" class="mathjax-container">\(O\!\left(\frac{1}{1-\beta_1}\right)\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(\beta_1\)</span> 越大，平均窗口越长，方向越平滑但响应越慢；<span displaypfx="inline-" class="mathjax-container">\(\beta_1\)</span> 越小，平均窗口越短，方向更敏捷但更易受噪声影响。</p>
<div class="blog_h4"><span class="graybg">二阶矩：节奏感（RMSProp）</span></div>
<p>RMSProp（Root Mean Square Propagation）用梯度平方的指数滑动平均（Exponential Moving Average, EMA）来估计每个参数维度的“尺度”，再用该尺度对当前梯度做逐元素归一化，从而把更新步伐控制在更稳定的量级。</p>
<span displaypfx="" class="mathjax-container">\[v_{t+1}=\beta_2 v_t+(1-\beta_2)(g_t\odot g_t),\quad \tilde g_t=\frac{g_t}{\sqrt{v_{t+1}}+\epsilon}\]</span>
<ul>
<li><span style="background-color: #c0c0c0;">索引约定</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 次更新先在 <span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span> 处计算 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span>，再用它更新得到 <span displaypfx="inline-" class="mathjax-container">\(v_{t+1}\)</span>；用 <span displaypfx="inline-" class="mathjax-container">\(v_{t+1}\)</span> 缩放 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 表示“用本次更新后的尺度估计做归一化”，不涉及未来信息。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(v_t\)</span>：上一轮更新结束后的二阶原点矩估计（EMA 状态，“更新前的尺度缓存”）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(v_{t+1}\)</span>：梯度的二阶原点矩（Second Raw Moment）估计，逐（梯度）维近似 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\)</span>，刻画“历史梯度大小”的典型尺度。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(g_t\odot g_t\)</span>：当前梯度的逐元素平方（<span displaypfx="inline-" class="mathjax-container">\(\odot\)</span> 为 Hadamard 乘积），只保留幅度信息。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sqrt{v_{t+1}}\)</span>：均方根（Root Mean Square, RMS）尺度；平方使量纲变为 <span displaypfx="inline-" class="mathjax-container">\(g^2\)</span>，开方把量纲还原到 <span displaypfx="inline-" class="mathjax-container">\(g\)</span>，从而可与 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 相除。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\tilde g_t\)</span>：按 RMS 尺度归一化后的梯度；分式表示逐元素相除（每个参数维度各自缩放）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\beta_2\in[0,1)\)</span>：衰减系数，控制尺度估计的记忆长度；越大表示对历史尺度更“长记忆”。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>：数值稳定项，避免分母为 0 或过小。</li>
</ul>
<p>该缩放把“方向”和“尺度”解耦：方向仍由 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 给出；步长由 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{v_{t+1}}\)</span> 自适应调节。若某维长期梯度偏大，则 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{v_{t+1}}\)</span> 变大、该维更新被压小；若某维长期梯度偏小，则分母较小、该维相对步子更大。</p>
<p>若把 RMSProp 作为独立优化器使用，参数更新可写为 <span displaypfx="inline-" class="mathjax-container">\(\theta_{t+1}=\theta_t-\eta\,\tilde g_t\)</span>。在 Adam 中，同样的 RMS 缩放作用在一阶矩估计上：用 <span displaypfx="inline-" class="mathjax-container">\(\hat m_{t+1}\)</span> 替代 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 作为分子，并在下一节通过偏置修正得到 <span displaypfx="inline-" class="mathjax-container">\(\hat v_{t+1}\)</span> 作为分母。</p>
<div class="blog_h4"><span class="graybg">偏置修正：冷启动校正（Bias Correction）</span></div>
<p>因为 <span displaypfx="inline-" class="mathjax-container">\(m_0=v_0=0\)</span>，训练初期的滑动平均会系统性偏小（“没热起来”）。Adam 用偏置修正把它拉回合理尺度：</p>
<span displaypfx="" class="mathjax-container">\[\hat m_{t+1}=\frac{m_{t+1}}{1-\beta_1^{t+1}},\quad \hat v_{t+1}=\frac{v_{t+1}}{1-\beta_2^{t+1}}\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\hat m_{t+1},\hat v_{t+1}\)</span>：偏置修正后的估计，用来抵消初始化为 0 导致的早期偏小。</li>
<li>分母 <span displaypfx="inline-" class="mathjax-container">\(1-\beta^{t+1}\)</span>：校正“冷启动偏小”的缩放因子。由于初始化为 0，指数滑动平均在经历 <span displaypfx="inline-" class="mathjax-container">\(t+1\)</span> 次更新时只积累了约 <span displaypfx="inline-" class="mathjax-container">\(1-\beta^{t+1}\)</span> 的有效权重，因此会偏小；除以它相当于把估计值按同样比例放大回去。</li>
</ul>
<p>更严格地说：若假设 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g_t]=\mu\)</span> 近似稳定，则由递推可得 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[m_{t+1}]=(1-\beta_1^{t+1})\mu\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(\hat m_{t+1}=\frac{m_{t+1}}{1-\beta_1^{t+1}}\)</span> 是把它改成近似无偏估计。同理可得 <span displaypfx="inline-" class="mathjax-container">\(\hat v_{t+1}\)</span> 的修正。</p>
<div class="blog_h4"><span class="graybg">参数更新：方向 ÷ 尺度</span></div>
<span displaypfx="" class="mathjax-container">\[\theta_{t+1}=\theta_t-\eta\,\frac{\hat m_{t+1}}{\sqrt{\hat v_{t+1}}+\epsilon}\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\theta_t\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步参数（权重向量/张量）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\eta\)</span>：学习率（全局步长系数）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sqrt{\hat v_{t+1}}\)</span>：逐元素开方；整项 <span displaypfx="inline-" class="mathjax-container">\(\frac{\hat m_{t+1}}{\sqrt{\hat v_{t+1}}+\epsilon}\)</span> 表示逐元素相除。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>：数值稳定项，避免除以 0 或极小数。</li>
</ul>
<p>分子给方向，分母给节奏。一个极端例子能看出为什么 Adam 用 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\)</span> 而不是方差：若某维梯度连续很多步都是 <span displaypfx="inline-" class="mathjax-container">\(g_t=10\)</span>，方差为 0 会导致除法不稳定；但 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\approx 100\)</span> 给出稳定尺度，最终更新量级约为 <span displaypfx="inline-" class="mathjax-container">\(10/\sqrt{100}=1\)</span>（再加 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 保底）。</p>
<p>Adam 往往更易调参、早期收敛更快，因此在 Transformer、扩散模型和很多深度网络里是默认起点。但它并不是无条件最好：有些任务上，最终泛化仍可能不如精心调过的 SGD。实践中，配合权重衰减（Weight Decay）时，通常会使用 AdamW，把权重衰减从梯度自适应缩放里解耦出来，效果更稳。</p>
<div class="blog_h3"><span class="graybg">AdamW</span></div>
<p>AdamW（Adam with Decoupled Weight Decay）把权重衰减（Weight Decay）从 Adam 的自适应梯度缩放里<span style="background-color: #c0c0c0;">解耦</span>出来。原因是：在 Adam 这类自适应方法里，如果你把 L2 正则化写进损失（等价于把 <span displaypfx="inline-" class="mathjax-container">\(\lambda\theta\)</span> 加到梯度里），这个正则项也会被二阶矩 <span displaypfx="inline-" class="mathjax-container">\(\hat v_t\)</span> 的缩放影响，从而导致不同参数维度的“衰减强度”不再可控。</p>
<p>正则化本来应该是一个可控的、与梯度尺度无关的收缩力度；解耦后 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 的含义更稳定。</p>
<p>对比两种写法会更清楚：</p>
<ul>
<li>把 L2 正则化并入目标（常被口语化地称为“weight decay”，但在自适应优化器里并不等价）：先算 <span displaypfx="inline-" class="mathjax-container">\(g_t=\nabla_\theta L(\theta_t)+\lambda\theta_t\)</span>，再把这个 <span displaypfx="inline-" class="mathjax-container">\(g_t\)</span> 送入 Adam 的 <span displaypfx="inline-" class="mathjax-container">\(m_t,v_t\)</span> 与自适应步长。</li>
<li>AdamW（解耦权重衰减）：先算纯数据梯度 <span displaypfx="inline-" class="mathjax-container">\(g_t=\nabla_\theta L(\theta_t)\)</span> 并完成 Adam 的自适应更新，然后单独对参数做一次权重衰减。</li>
</ul>
<p>AdamW 的参数更新可写成：</p>
<span displaypfx="" class="mathjax-container">\[\theta_{t+1}=\theta_t-\eta\,\frac{\hat m_{t+1}}{\sqrt{\hat v_{t+1}}+\epsilon}-\eta\,\lambda\,\theta_t\]</span>
<ul>
<li>前半段 <span displaypfx="inline-" class="mathjax-container">\(-\eta\,\frac{\hat m}{\sqrt{\hat v}+\epsilon}\)</span>：Adam 的自适应梯度更新（由数据损失驱动）。</li>
<li>后半段 <span displaypfx="inline-" class="mathjax-container">\(-\eta\,\lambda\,\theta_t\)</span>：解耦的 weight decay（参数按比例收缩），其中 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是衰减强度。</li>
</ul>
<p>其中第一项是 Adam 的自适应更新，第二项是独立的权重衰减（等价于每步把权重按比例拉向 0）。这种分离让 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 更像一个真正可解释的“收缩强度”，在 Transformer 等模型上通常更稳定、更好调。工程实现里常见做法是：对 bias 与归一化层参数（如 LayerNorm 的 <span displaypfx="inline-" class="mathjax-container">\(\gamma,\beta\)</span>）不做 weight decay，以免对尺度/偏置项造成不必要的收缩。</p>
<div class="blog_h3"><span class="graybg">Muon</span></div>
<p>Muon 是一种面向神经网络隐藏层（Hidden Layer）权重矩阵（Weight Matrix）的优化器。它与 Adam/AdamW 的根本差异在于：AdamW 会为每个参数维度单独估计更新尺度，并对梯度/更新量做逐元素（Element-wise）归一化；Muon 则把一个二维权重张量视为一个整体，直接利用矩阵结构来塑造更新方向。因此，Muon 更像一种<span style="background-color: #c0c0c0;">矩阵感知（Matrix-aware）的优化器</span>。</p>
<p>它的核心做法不是改变“沿梯度下降”这个大方向，而是改变“更新张量应该具有什么几何形状”。典型写法可以概括为：先对梯度做动量（Momentum）累积，再把这个矩阵更新做一次正交化（Orthogonalization）后处理，然后才真正更新参数：</p>
<span displaypfx="" class="mathjax-container">\[M_{t+1}=\beta M_t+(1-\beta)G_t\]</span>
<span displaypfx="" class="mathjax-container">\[\Delta W_t=\mathrm{Orth}(M_{t+1}),\quad W_{t+1}=W_t-\eta\,\Delta W_t\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(G_t\)</span> 是当前梯度矩阵， <span displaypfx="inline-" class="mathjax-container">\(M_t\)</span> 是动量缓冲， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Orth}(\cdot)\)</span> 表示把更新矩阵变换到更“接近正交（Orthogonal）”的形状。工程实现里，这一步通常用 Newton-Schulz 迭代（Newton-Schulz Iteration）高效近似完成，因此 Muon 常被概括为“对动量更新做正交化”。</p>
<ul>
<li>优势：对线性层/MLP/注意力里的二维权重矩阵，Muon 往往能给出更结构化的更新方向；在一些大模型训练设定下，它的训练效率与收敛速度优于 AdamW。</li>
<li>边界：Muon 不是全参数通吃的默认方案。它通常只用于隐藏层的二维参数；embedding、bias、归一化参数、输出层等非二维或语义不同的参数，实践中常继续交给 AdamW。</li>
<li>工程特征：它强调更新矩阵的谱结构（Spectral Structure），而不是单个坐标的独立缩放；因此超参数分组与参数类型划分比 AdamW 更重要。</li>
</ul>
<p>可以把 AdamW 与 Muon 的差别记成一句话：<span style="background-color: #c0c0c0;">AdamW 解决“每个参数该走多大步”，Muon 进一步关心“整个矩阵应该以什么形状移动”</span>。因此 Muon 更像是隐藏层矩阵优化的专用工具，而不是对所有参数一视同仁的通用默认项。</p>
<div class="blog_h3"><span class="graybg">学习率调度</span></div>
<p>学习率（Learning Rate）往往是最敏感的超参数之一。即使优化器相同，只要学习率设错，训练就可能完全失败。学习率调度（Learning Rate Scheduling）/退火（Annealing）的核心思想是：前期用相对大的步长快速下降，后期逐渐减小步长做精细收敛。</p>
<p>常见调度方式包括：</p>
<ul>
<li>Step decay：按 epoch 或 step 乘以固定因子衰减，简单直接。</li>
<li>Linear decay：线性下降到较小值或 0，常用于大模型训练后段。</li>
<li>Warmup + decay：先小步热身，再进入正常学习率，最后逐步衰减；对 Transformer 和大 batch 训练尤其常见。</li>
<li>余弦退火（Cosine Annealing）：将学习率从较大值逐步衰减到较小值，把优化从“高温探索”过渡到“低温收敛”，在训练后期降低梯度噪声（gradient noise）与过冲（overshoot）风险，使参数能在极小值附近稳定细化。余弦曲线在起点与终点的一阶导数为 0，避免阶梯式衰减的突变，因而衰减更平滑、后期更柔和。</li>
</ul>
<p>Warmup 为什么有用？因为训练刚开始时，参数还处在一个非常“生”的区域，梯度统计不稳定，若一上来就用很大学习率，模型容易发散。先用较小步长热身几百或几千步，再拉到目标学习率，通常更稳。</p>
<p>余弦退火的典型写法（从 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> 衰减到 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\min}\)</span>）为：</p>
<span displaypfx="" class="mathjax-container">\[\eta(t)=\eta_{\min}+\frac{1}{2}(\eta_{\max}-\eta_{\min})\left(1+\cos\left(\pi\frac{t}{T}\right)\right)\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\eta(t)\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步使用的学习率。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> / <span displaypfx="inline-" class="mathjax-container">\(\eta_{\min}\)</span>：一个退火周期内的最大学习率与最小学习率。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(t\)</span>：当前步数（通常从 0 开始计），<span displaypfx="inline-" class="mathjax-container">\(T\)</span>：该周期的总步数。</li>
</ul>
<p>这个公式可以直接做两次代入来验证边界：当 <span displaypfx="inline-" class="mathjax-container">\(t=0\)</span> 时，<span displaypfx="inline-" class="mathjax-container">\(\cos(0)=1\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(\eta(0)=\eta_{\max}\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(t=T\)</span> 时，<span displaypfx="inline-" class="mathjax-container">\(\cos(\pi)=-1\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(\eta(T)=\eta_{\min}\)</span>。中间学习率按半个余弦周期平滑下降。</p>
<p>通过余弦（Cosine）提供了一个非常干净的<span style="background-color: #c0c0c0;">端点平滑（smooth endpoints）</span>性质：在起点和终点处变化率为 0。对上式求导可得</p>
<span displaypfx="" class="mathjax-container">\[\frac{d\eta}{dt}=-\frac{1}{2}(\eta_{\max}-\eta_{\min})\frac{\pi}{T}\sin\left(\pi\frac{t}{T}\right)\]</span>
<p>因此 <span displaypfx="inline-" class="mathjax-container">\(\sin(0)=\sin(\pi)=0\)</span>，学习率在周期开始与结束都不会出现突兀的“拐点”。相比之下，step decay 有不连续跳变，linear decay 在末端通常会突然到达下限并停止变化（出现不光滑的折点）。在非凸深度网络里，这种平滑退火往往更稳：前期保持较大步长便于探索，后期自然减小步长便于精细收敛。</p>
<p>工程上常见扩展是余弦重启（Cosine Annealing with Warm Restarts, SGDR）：把训练过程切成多个周期，每个周期把 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 重新从 0 开始计（并可逐周期拉长 <span displaypfx="inline-" class="mathjax-container">\(T\)</span>）。在标准 SGDR 中，<span style="background-color: #c0c0c0;">学习率在周期边界是不连续的</span>：它会在一个周期末端衰减到 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\min}\)</span>，并在下一个周期起点从 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> 重新开始（参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 不会重置）。</p>
<p>这种“重启”的作用不是为了制造噪声，而是把优化过程从“后期小步精修”短暂切回“较大步长探索”，以应对非凸问题中的平台区（plateau）与次优盆地（suboptimal basin）。学习率拉高会增大每步更新幅度与梯度噪声（gradient noise）的有效影响，帮助轨迹跳出当前区域并探索新的吸引域；而如果当前区域确实是更稳健的平坦极小值（flat minimal），后续退火通常会把参数再次拉回并在附近更精细地收敛。工程上常见做法是把 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span> 设在“不会破坏稳定性”的范围内；若重启瞬间仍担心不稳定，也可以在每次重启后加一个很短的 warmup，让学习率在少量步数内从较小值爬升到 <span displaypfx="inline-" class="mathjax-container">\(\eta_{\max}\)</span>。</p>
<p>没有一种调度策略对所有任务都最好。真正有效的做法是结合损失曲线、梯度稳定性、验证集表现和训练预算一起看：如果前期降不动，往往学习率偏小；如果剧烈震荡甚至发散，往往学习率偏大；如果后期长期卡在平台区，通常需要更细的衰减策略。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/sgdr.jpg"><img class="alignnone size-full wp-image-41059" src="https://blog.gmem.cc/wp-content/uploads/2026/03/sgdr.jpg" alt="sgdr" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">集成策略（Ensemble Learning）</span></div>
<p>集成学习（Ensemble Learning）的核心思想是：<span style="background-color: #c0c0c0;">不要把预测完全押在一个模型上，而是让多个模型以某种组织方式共同决定结果</span>。它背后的统计直觉并不神秘：如果多个模型的误差来源不完全相同，那么组合后的总误差通常会更小、更稳、更抗偶然扰动。</p>
<p>从误差分解视角看，集成学习最常见的两条路线分别在处理两种不同问题。Bagging 更擅长压低方差（Variance），适合“单个模型很容易被训练集波动带偏”的情形；Boosting 更擅长逐步降低偏差（Bias），适合“单个弱学习器表达力不够，但可以通过连续修正变强”的情形。树模型恰好非常适合这两条路线：单棵深树高方差，适合拿去做 Bagging；浅层回归树可作为局部纠错器，适合拿去做 Boosting。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">Bagging</td>
<td style="text-align: center;">Boosting</td>
<td style="text-align: center;">Stacking</td>
</tr>
</thead>
<tbody>
<tr>
<td>核心目标</td>
<td>降低方差，让模型更稳</td>
<td>降低偏差，让模型更准</td>
<td>学习如何组合不同模型，让互补性显式变成一个新模型</td>
</tr>
<tr>
<td>训练方式</td>
<td>多个基模型并行独立训练</td>
<td>多个弱学习器串行依次训练</td>
<td>先训练多个基模型，再训练一个元学习器做组合</td>
</tr>
<tr>
<td>每轮关注什么</td>
<td>同一个原始任务，但训练数据子集和特征视角不同</td>
<td>前一轮没有拟合好的误差、残差或负梯度</td>
<td>不同基模型的输出在什么条件下更可信</td>
</tr>
<tr>
<td>最终聚合</td>
<td>平均或投票</td>
<td>加权累加</td>
<td>由元学习器学习组合规则</td>
</tr>
<tr>
<td>典型代表</td>
<td>随机森林</td>
<td>GBDT、XGBoost、LightGBM、CatBoost</td>
<td>多模型堆叠集成</td>
</tr>
<tr>
<td>更像什么</td>
<td>多位评委独立打分后求平均</td>
<td>接力纠错，每个人只补前面没做好的部分</td>
<td>多位专家先各自判断，再由总负责人学习何时该信谁</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Bagging</span></div>
<p>Bagging（Bootstrap Aggregating）的核心做法是对训练集进行多次自助采样（bootstrap sampling），即反复执行有放回采样（sampling with replacement）生成多个训练子集，并在这些子集上分别训练基模型，以降低方差（Variance）。由于每个模型见到的数据子集略有差异，学到的决策边界不会完全一致；最后对预测结果做平均或多数投票，可抵消一部分过拟合噪声。</p>
<p>Bagging 的关键不在“模型一定很多”，而在<span style="background-color: #c0c0c0;">人为制造模型之间的差异性</span>。如果每个基模型看到的训练数据完全相同、特征也完全相同、优化过程也完全相同，那么把它们重复训练很多次并不会带来真正互补的信息。bootstrap 采样、特征子采样、不同随机初始化，本质上都在做同一件事：让多个模型不要犯完全一样的错。</p>
<p>例：如果单棵决策树很容易被训练集中的偶然样本带偏，那么训练 100 棵在不同 bootstrap 样本上的树，再投票，通常会比只用 1 棵树稳定得多。这也是随机森林的核心直觉。</p>
<div class="blog_h3"><span class="graybg">Boosting</span></div>
<p>Boosting 的思路与 Bagging 相反：它不是“并行训练很多彼此独立的模型”，而是“串行训练一串弱学习器（Weak Learners），让后一个模型专门修正前一个模型没做好的部分”。因此它更像“老师批改作业”：每一轮都盯着上轮最容易出错的题继续强化。</p>
<p>以加法模型视角看，Boosting 逐步构造</p>
<span displaypfx="" class="mathjax-container">\[F_M(x)=\sum_{m=1}^{M}\alpha_m h_m(x)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(h_m(x)\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 个弱学习器， <span displaypfx="inline-" class="mathjax-container">\(\alpha_m\)</span> 是它的权重。公式不是为了堆符号，而是在表达：最终模型是“很多个简单模型的加权和”，每一轮都往当前模型上加一小块“修正项”。</p>
<p>这类“接力纠错”最直观的理解，是把当前模型与真实目标之间的差距看成下一轮的新任务。回归里，这个差距常直接表现为残差；更一般地，在可微损失下，它表现为损失对当前预测的负梯度。于是每一轮不再去重学整个标签，而是只学“接下来该往哪里补”。这就是 GBDT 家族能持续逼近目标函数的根本原因。</p>
<p>这里还可以再区分两种常见实现。较早的 Boosting，如 AdaBoost，更强调<span style="background-color: #c0c0c0;">把之前分错的样本权重调高</span>，让后续弱学习器更多关注难样本；GBDT 这一支则更强调<span style="background-color: #c0c0c0;">直接拟合当前损失的残差或负梯度</span>。两者都属于“串行纠错”，只是把“错误信息”传给下一轮的方式不同。</p>
<p>Boosting 也解释了为什么这类模型通常使用<span style="background-color: #c0c0c0;">较浅的小树</span>作为基学习器。因为每一棵树的职责不是独立解决全问题，而是在当前模型基础上补一个局部修正。如果每棵树都长得很深、很强，单轮就可能把训练集吃得过满，后续串行叠加更容易过拟合；若每棵树只做小而稳的修正，再配合学习率与早停，整体通常更可控。</p>
<div class="blog_h3"><span class="graybg">Stacking</span></div>
<p>Stacking（堆叠集成）不只是平均多个模型输出，而是再训练一个元学习器（Meta-Learner）去学习“什么时候该信哪个模型”。例如一个基模型擅长处理稀疏文本特征，另一个擅长处理数值特征，Stacking 可以学会在不同样本上动态加权它们。</p>
<p>它像“专家会诊 + 总负责人”：若文本模型和表格模型各有专长，元模型就负责综合判断谁在当前病例上更可信。</p>
<div class="blog_h2"><span class="graybg">机器学习编程</span></div>
<p>机器学习编程（Machine Learning Programming）处理的核心问题是：当一个模型被写成代码并真正运行起来时，<span style="background-color: #c0c0c0;">谁负责组织训练流程，谁负责表达数学操作，谁又负责在具体硬件上把这些操作算快</span>。这三层通常分别对应编程框架（Framework）、算子（Operator）和内核（Kernel）。理解这三个层次，有助于把“模型公式”与“工程实现”连接起来。</p>
<p>它们不是彼此独立的三个名词，而是一条自上而下的执行链。研究者或工程师先在框架里定义模型结构与训练流程；框架再把模型拆成一系列算子，例如矩阵乘法、卷积、归一化、激活、softmax；每个算子最终还需要落到某个硬件相关的内核实现上，才能在 CPU、GPU、TPU 或其他加速器上真正执行。因此，<span style="background-color: #c0c0c0;">框架决定开发体验，算子决定计算图语义，内核决定实际运行效率</span>。</p>
<div class="blog_h3"><span class="graybg">框架（Framework）</span></div>
<p>框架（Framework）并不是单一层次的概念。在现代机器学习工程里，至少要区分两层：一层是基础框架（Foundational Framework），负责张量（Tensor）、自动求导（Automatic Differentiation）、算子调度与底层执行；另一层是高层框架（High-level Framework），负责把某一类模型、某一类训练范式或某一类工程流程封装成更直接的接口。前者提供“地基”，后者提供“脚手架”和“现成结构”。</p>
<p>因此，PyTorch、TensorFlow、JAX 这类系统更适合放在基础框架层；而 Transformers、DeepSpeed、ModelScope、Lightning 这类工具，则更适合放在高层框架层。它们之间不是替代关系，而是典型的上下层关系：<span style="background-color: #c0c0c0;">高层框架通常建立在基础框架之上，用更强的任务抽象、更少的模板代码和更完整的工程能力，把常见训练与推理流程直接组织起来</span>。</p>
<div class="blog_h3"><span class="graybg">常见框架简介</span></div>
<p>从工程使用频率看，开发者最常接触的是一组已经按职责分层的常见框架：高层框架负责组织训练与推理流程，基础框架负责真正执行张量计算，某些系统还进一步偏向执行优化与部署。把这些名字放回分层结构里理解，比孤立记忆框架名称更清楚。</p>
<div class="blog_h4"><span class="graybg">高层框架（High-level Framework）</span></div>
<p>高层框架的核心价值在于，它们不重新发明张量计算和自动求导，而是在基础框架之上增加更强的任务语义、训练编排、模型生态或分布式能力。很多工程里，开发者日常接触最多的其实是这一层。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">高层框架</td>
<td style="text-align: center;">主要依赖的基础框架</td>
<td style="text-align: center;">核心定位</td>
<td style="text-align: center;">替你封装了什么</td>
<td style="text-align: center;">最适合的场景</td>
<td style="text-align: center;">与基础框架的关系</td>
</tr>
</thead>
<tbody>
<tr>
<td>Transformers</td>
<td>以 PyTorch 为主，也支持 TensorFlow 与 JAX / Flax</td>
<td>预训练 Transformer 模型与任务头生态</td>
<td>模型定义、权重加载、tokenizer / processor、Trainer、pipeline、任务头</td>
<td>NLP、LLM、多模态模型的微调与推理</td>
<td>通常不是自己实现底层训练系统，而是调用底层框架完成张量计算与反向传播</td>
</tr>
<tr>
<td>DeepSpeed</td>
<td>主要建立在 PyTorch 之上</td>
<td>大模型训练与推理优化框架</td>
<td>ZeRO、参数分片、优化器状态管理、分布式训练编排、推理加速</td>
<td>超大模型训练、多卡 / 多机扩展、显存受限训练</td>
<td>本质上是对 PyTorch 训练过程的增强与重写，而不是替代 PyTorch</td>
</tr>
<tr>
<td>Unsloth</td>
<td>主要建立在 PyTorch 之上，并与 Transformers、PEFT、TRL 等 Hugging Face 生态深度协同</td>
<td>面向 LLM 微调与对齐的性能导向高层框架</td>
<td>快速加载与训练配方、QLoRA / DoRA / RL 配置、量化微调、长上下文训练、导出到 GGUF / Ollama / vLLM / Hugging Face</td>
<td>单卡或少卡进行 LLM 微调、偏好对齐、消费级显卡上的高性价比实验</td>
<td>它不替代 PyTorch 或 Transformers，而是在它们之上把“高效微调 + 导出部署”这条链路进一步封装并做性能优化</td>
</tr>
<tr>
<td>ModelScope</td>
<td>主要建立在 PyTorch 之上，也兼容其他学习框架与平台能力</td>
<td>模型社区 + SDK + 训练 / 推理工作流</td>
<td>模型获取、pipeline、训练入口、评测、部署衔接、领域模型集成</td>
<td>中文生态、多模态任务、快速调用开源模型并做微调</td>
<td>更像“模型平台层 + 高层开发框架”，下层训练仍要落到基础框架执行</td>
</tr>
<tr>
<td>PyTorch Lightning</td>
<td>PyTorch</td>
<td>训练流程组织框架</td>
<td>Trainer、设备放置、日志、checkpoint、验证循环、分布式训练模板</td>
<td>希望保留 PyTorch 灵活性，同时减少训练样板代码</td>
<td>把 PyTorch 代码组织得更规范，但底层模型与梯度仍然是 PyTorch</td>
</tr>
<tr>
<td>Accelerate</td>
<td>PyTorch</td>
<td>分布式训练与多设备执行抽象</td>
<td>多 GPU / 多机启动、混合精度、设备管理、统一训练脚本适配</td>
<td>想在尽量少改代码的前提下把 PyTorch 训练扩展到分布式环境</td>
<td>不取代 PyTorch 训练代码，而是让同一份 PyTorch 代码更容易跨设备运行</td>
</tr>
<tr>
<td>Keras 3</td>
<td>可运行在 TensorFlow、JAX、PyTorch 之上，推理还可对接 OpenVINO</td>
<td>高层模型开发接口</td>
<td>Layer / Model 抽象、训练接口、callback、分布式 API、生态组件</td>
<td>需要高层建模接口且希望在多后端之间切换</td>
<td>处于基础框架之上，强调统一建模接口，而不是直接取代底层后端</td>
</tr>
<tr>
<td>Sentence Transformers</td>
<td>主要建立在 Transformers 与 PyTorch 之上</td>
<td>Embedding 与 reranker 高层框架</td>
<td>文本向量化、相似度训练、检索 / rerank 训练器、评测工具</td>
<td>语义检索、向量召回、文本匹配、reranking</td>
<td>属于面向特定任务族的高层框架，下层依赖 Transformers 与 PyTorch</td>
</tr>
<tr>
<td>MMEngine / OpenMMLab</td>
<td>主要建立在 PyTorch 之上</td>
<td>通用训练引擎与视觉算法框架底座</td>
<td>Runner、Hook、Config、数据流、训练 / 验证 / 测试流程组织</td>
<td>检测、分割、姿态估计等视觉任务</td>
<td>用统一工程抽象组织 PyTorch 训练，尤其适合复杂视觉实验体系</td>
</tr>
</tbody>
</table>
<p>这一层最容易让人产生“它自己就能训练模型”的直觉，但从执行链条看，它们大多只是把训练过程组织得更高级。以 Transformers 为例，<span style="background-color: #c0c0c0;">Trainer 可以发起训练，但底层的张量、梯度、优化器、自动求导与设备执行，通常仍然由 PyTorch 负责</span>；Lightning、Accelerate、DeepSpeed 也是同样的逻辑，只是它们封装的侧重点不同。</p>
<div class="blog_h4"><span class="graybg">Unsloth</span></div>
<p>Unsloth 最适合放在高层框架这一层理解。它并不重新定义 Tensor、自动求导（Autograd）或底层计算图，而是在 PyTorch、Transformers、PEFT、TRL 这一整套既有生态之上，把大语言模型（Large Language Model, LLM）的高频训练流程重新组织成更偏性能导向的开发体验。它解决的核心问题不是“怎样从零实现神经网络”，而是<span style="background-color: #c0c0c0;">怎样在尽可能小的显存和尽可能少的工程样板下，把 LLM 微调、对齐、导出与部署这条链路跑通</span>。</p>
<p>从开发者视角看，Unsloth 的典型工作流通常仍然是“加载 Hugging Face 模型 → 挂接 PEFT 适配器 → 用监督微调（SFT）或强化学习（RL）训练 → 导出到下游推理栈”。它的价值在于把这条链上几个最痛的环节一起压平：第一，量化微调（如 LoRA / QLoRA）与长上下文训练更容易直接落地；第二，针对 GRPO 等对齐训练给出更直接的入口；第三，把训练后的模型或适配器导出到 GGUF、Ollama、vLLM、SGLang、Hugging Face 等下游环境的路径做得更短。换言之，Unsloth 不是另起炉灶，而是把已有生态串得更紧，并对其中最贵的显存、最长的上下文和最复杂的导出环节做专项优化。</p>
<p>它与其他高层框架的边界也需要分清。Transformers 的优势在于模型家族与任务头生态最通用；Accelerate 更像多设备执行抽象；DeepSpeed 更偏分布式训练、参数分片与大规模集群优化；Unsloth 则把重心压在“单机到小规模多卡场景下，如何更快、更省显存地完成 LLM 微调与对齐”。因此，它尤其适合消费级 GPU、本地实验、小团队快速验证、Notebook 驱动的训练流程，以及以 LoRA / QLoRA / RL 为主的大模型增量训练。若任务是超大规模集群预训练或需要复杂的跨机并行策略，DeepSpeed / Megatron 一类系统通常仍然更中心；若任务是通用模型调用与标准微调，Transformers 仍然是最基础的入口。</p>
<p>工程上可以把 Unsloth 看成“面向 LLM 微调的高性能工作台”。它一端连接 Hugging Face 模型与适配器生态，另一端连接本地推理、GGUF、Ollama、vLLM、SGLang 等部署路径，中间则用一套更偏性能调优的训练封装把它们粘起来。对初学者，它降低的是上手门槛：少改几处配置就能跑通量化微调或 RL；对熟悉生态的工程师，它降低的是试验成本：同样的显存预算下，能尝试更长上下文、更复杂的对齐流程，或更快地把训练结果导出到不同推理栈。它的本质依然是高层工程抽象，而不是底层深度学习框架的替代品。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">比较维度</td>
<td style="text-align: center;">Transformers</td>
<td style="text-align: center;">DeepSpeed</td>
<td style="text-align: center;">Unsloth</td>
</tr>
</thead>
<tbody>
<tr>
<td>核心目标</td>
<td>统一模型与任务接口</td>
<td>放大训练规模与显存效率</td>
<td>压低 LLM 微调 / 对齐门槛并提升单机效率</td>
</tr>
<tr>
<td>最擅长的问题</td>
<td>模型加载、任务头、Trainer、pipeline</td>
<td>ZeRO、分布式并行、大模型集群训练</td>
<td>QLoRA、GRPO、长上下文、快速导出部署</td>
</tr>
<tr>
<td>典型使用者</td>
<td>几乎所有 NLP / LLM 开发者</td>
<td>大模型平台与多机训练团队</td>
<td>本地实验者、个人开发者、小团队 LLM 微调工程师</td>
</tr>
<tr>
<td>与 PyTorch 的关系</td>
<td>调用其张量与训练能力</td>
<td>增强其训练与并行系统</td>
<td>在其之上重组 LLM 微调与导出流程</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">基础框架（Foundational Framework）</span></div>
<p>基础框架直接定义张量运算、计算图、自动求导、优化器、算子调度与设备执行能力。它们离硬件更近，也离“神经网络真正怎样被算出来”更近。高层框架能否存在，首先取决于这一层是否提供了足够稳定和强大的底座。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">基础框架</td>
<td style="text-align: center;">核心抽象</td>
<td style="text-align: center;">主要优势</td>
<td style="text-align: center;">最适合的场景</td>
<td style="text-align: center;">典型局限</td>
</tr>
</thead>
<tbody>
<tr>
<td>PyTorch</td>
<td>Tensor、<pre class="crayon-plain-tag">nn.Module</pre>、autograd、动态图（Dynamic Computation Graph）</td>
<td>灵活、直观、研究生态最强，训练与调试体验优秀</td>
<td>研究、论文复现、大模型训练、需要自定义训练逻辑的任务</td>
<td>如果完全手写训练循环，工程样板代码较多，部署链路常需额外工具配合</td>
</tr>
<tr>
<td>TensorFlow</td>
<td>Tensor、Layer / Model、自动求导、图执行与编译</td>
<td>训练与部署体系完整，服务化、端侧与工业链路成熟</td>
<td>企业级生产环境、需要完整训练到部署闭环的场景</td>
<td>研究阶段的编码与调试直观性通常不如 PyTorch</td>
</tr>
<tr>
<td>JAX</td>
<td>数组（Array）+ 函数变换 + XLA 编译</td>
<td>编译优化强，函数式表达清晰，适合大规模并行数值计算</td>
<td>需要强编译能力、自定义并行策略、科研数值实验的任务</td>
<td>函数式编程习惯要求更高，工程生态相对更偏高级用户</td>
</tr>
<tr>
<td>PaddlePaddle</td>
<td>Tensor、动态图 / 静态图、训练与产业工具链</td>
<td>中文生态与产业落地支持强，训练推理工具链完整</td>
<td>产业应用、教育场景、中文任务与本土化生态</td>
<td>国际社区规模与通用论文实现数量通常少于 PyTorch</td>
</tr>
</tbody>
</table>
<p>如果把机器学习工程比作建楼，那么基础框架提供的是钢筋、水泥、电路和承重结构。高层框架之所以能让开发速度显著提升，正是因为底层这些张量、梯度和算子能力已经由基础框架稳定提供。</p>
<div class="blog_h4"><span class="graybg">执行与部署系统（Execution and Deployment Stack）</span></div>
<p>除了高层框架和基础框架，还存在一类经常与“框架”混称、但职责不同的系统：执行与部署系统。它们的核心目标不是让用户更方便地写模型，而是让已经定义好的模型在特定硬件上更快、更省、更稳定地运行。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">系统</td>
<td style="text-align: center;">主要定位</td>
<td style="text-align: center;">最常见作用</td>
<td style="text-align: center;">典型场景</td>
<td style="text-align: center;">与前两层的关系</td>
</tr>
</thead>
<tbody>
<tr>
<td>ONNX Runtime</td>
<td>跨框架推理执行系统</td>
<td>加载 ONNX 图，做图优化、算子调度与多后端执行</td>
<td>统一部署、跨框架导出后的推理执行</td>
<td>位于模型定义之后，更接近运行时（Runtime）而不是训练框架</td>
</tr>
<tr>
<td>TensorRT</td>
<td>NVIDIA GPU 推理优化系统</td>
<td>图优化、层融合、量化与 kernel 自动选择</td>
<td>低延迟在线推理、高吞吐批量服务</td>
<td>通常接收上层框架导出的模型，再做更深的硬件侧优化</td>
</tr>
<tr>
<td>OpenVINO</td>
<td>Intel 硬件推理栈</td>
<td>模型转换、图优化、Intel CPU / iGPU / VPU 推理</td>
<td>Intel 服务器与边缘设备部署</td>
<td>更接近部署后端，而不是通用训练框架</td>
</tr>
<tr>
<td>TVM</td>
<td>深度学习编译栈</td>
<td>自动调优、代码生成、异构硬件适配</td>
<td>边缘部署、自定义芯片、性能工程</td>
<td>站在算子和内核之间，为不同硬件生成更优执行实现</td>
</tr>
</tbody>
</table>
<p>因此，讨论“框架”时最好先分清是哪一层：<span style="background-color: #c0c0c0;">高层框架负责把任务和流程组织起来，基础框架负责把张量与梯度真正算出来，执行与部署系统负责把已经定义好的模型在目标硬件上跑到更优</span>。这三层一旦混在一起，很多看似相近的名词就会失去边界。</p>
<div class="blog_h3"><span class="graybg">算子（Operator）</span></div>
<p>算子（Operator）是计算图（Computation Graph）的基本运算单元。它定义一个明确的数学变换：输入什么张量（Tensor）、输出什么张量、张量的形状（Shape）和数据类型（Data Type）如何变化，以及反向传播时梯度如何计算。框架层写出的模块、层、网络块，最终都会被拆解成一串更细粒度的算子。</p>
<p>从工程实现上看，算子这一层负责表达“数学语义”。例如一个线性层（Linear Layer）会被拆成 MatMul 与 Bias Add；一个自注意力（Self-Attention）模块会被拆成 Q/K/V 投影、MatMul、Scale、Mask、Softmax、再一次 MatMul、Dropout、LayerNorm 等。高层模块能否被编译优化，本质上取决于这些算子能否被识别、融合与高效执行。</p>
<p>常用算子可以分成四大类：线性代数与张量形状类、神经网络前向计算类、序列与索引操作类、训练与优化类。下面的表格按这一方式展开。</p>
<div class="blog_h4"><span class="graybg">线性代数与张量形状类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>MatMul / GEMM</td>
<td>矩阵乘法，完成线性投影与特征混合</td>
<td>二维或更高维张量，输出新的线性组合</td>
<td>全连接层、注意力投影、MLP</td>
<td>最核心的高密度算子之一，常直接决定训练吞吐</td>
</tr>
<tr>
<td>Batch MatMul</td>
<td>批量矩阵乘法，多个矩阵对并行相乘</td>
<td>三维及以上张量</td>
<td>多头注意力（Multi-Head Attention）</td>
<td>常与转置、缩放、mask 连用</td>
</tr>
<tr>
<td>Add / Bias Add</td>
<td>逐元素加法</td>
<td>两个可广播张量</td>
<td>残差连接、偏置项、特征融合</td>
<td>常被融合到前后算子中减少访存</td>
</tr>
<tr>
<td>Sub / Mul / Div</td>
<td>逐元素减法、乘法、除法</td>
<td>逐元素张量运算</td>
<td>归一化、门控、缩放</td>
<td>广播规则必须和张量形状匹配</td>
</tr>
<tr>
<td>Scale</td>
<td>用常数或向量对张量缩放</td>
<td>输入张量与标量 / 向量</td>
<td>attention 中的 <span displaypfx="inline-" class="mathjax-container">\(1/\sqrt{d_k}\)</span> 缩放</td>
<td>经常被编译器与前后算子融合</td>
</tr>
<tr>
<td>Transpose / Permute</td>
<td>重排维度顺序</td>
<td>输入张量到相同元素、不同布局的张量</td>
<td>NCHW / NHWC 转换，多头维度重排</td>
<td>逻辑上不改值，但常改变内存访问模式</td>
</tr>
<tr>
<td>Reshape / View</td>
<td>改变张量形状而不改变元素总数</td>
<td>同样的数据，不同 shape</td>
<td>展平、分头、合并头、batch 展开</td>
<td>若内存不连续，可能触发额外复制</td>
</tr>
<tr>
<td>Expand / BroadcastTo</td>
<td>按广播规则扩展维度</td>
<td>低维张量扩展为高维张量</td>
<td>偏置广播、mask 扩展</td>
<td>逻辑扩展不一定真实复制数据</td>
</tr>
<tr>
<td>Squeeze / Unsqueeze</td>
<td>删除或插入长度为 1 的维度</td>
<td>维度数变化，数据值不变</td>
<td>batch / channel 维调整</td>
<td>常用于接口对齐和算子拼接</td>
</tr>
<tr>
<td>Concat / Stack</td>
<td>拼接多个张量</td>
<td>多个同类型张量合并为一个</td>
<td>多特征合并、多分支网络</td>
<td>Concat 沿已有维度拼接，Stack 会新增维度</td>
</tr>
<tr>
<td>Split / Chunk</td>
<td>将一个张量拆成多个子张量</td>
<td>一个张量拆成若干块</td>
<td>Q/K/V 切分、多分支路径</td>
<td>与 Concat、Stack 常成对出现</td>
</tr>
<tr>
<td>ReduceSum / ReduceMean / ReduceMax</td>
<td>沿某些维度做聚合</td>
<td>高维张量压缩成低维张量</td>
<td>池化、loss 聚合、统计量计算</td>
<td>归约（Reduction）通常对并行实现要求较高</td>
</tr>
<tr>
<td>EinSum</td>
<td>用爱因斯坦求和规则表达复合张量运算</td>
<td>多个张量到一个张量</td>
<td>复杂线性代数、注意力原型实现</td>
<td>表达力强，但实际性能往往依赖后端是否能分解优化</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">神经网络前向计算类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Convolution</td>
<td>局部感受野加权求和</td>
<td>特征图与卷积核，输出新的特征图</td>
<td>CNN、视觉 backbone、语音模型</td>
<td>步幅、填充、分组卷积会显著影响性能与感受野</td>
</tr>
<tr>
<td>Depthwise / Group Convolution</td>
<td>按通道组或单通道卷积</td>
<td>特征图到特征图</td>
<td>MobileNet、轻量视觉网络</td>
<td>减少计算量，但对 kernel 实现要求更高</td>
</tr>
<tr>
<td>Pooling（Max / Avg）</td>
<td>局部下采样与聚合</td>
<td>特征图压缩为空间更小的特征图</td>
<td>CNN、时序聚合</td>
<td>降低分辨率与计算量，也带来信息损失</td>
</tr>
<tr>
<td>Adaptive Pooling</td>
<td>把输入压到固定输出尺寸</td>
<td>任意空间尺寸到固定尺寸</td>
<td>视觉分类头、全局池化</td>
<td>方便不同输入尺寸统一到下游全连接层</td>
</tr>
<tr>
<td>ReLU</td>
<td>负值截断为 0 的激活函数</td>
<td>逐元素非线性变换</td>
<td>MLP、CNN、分类头</td>
<td>实现简单，稀疏性强</td>
</tr>
<tr>
<td>GELU</td>
<td>平滑激活，保留小负值的连续变化</td>
<td>逐元素非线性变换</td>
<td>Transformer、LLM MLP</td>
<td>现代语言模型最常见的激活之一</td>
</tr>
<tr>
<td>SiLU / Swish</td>
<td>输入与 sigmoid 门控的乘积</td>
<td>逐元素非线性变换</td>
<td>高性能视觉与语言模型</td>
<td>平滑、效果稳定，常见于新型 backbone</td>
</tr>
<tr>
<td>Sigmoid</td>
<td>把实数映射到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span></td>
<td>逐元素概率化</td>
<td>门控单元、二分类输出、多标签任务</td>
<td>饱和区梯度小，深层网络中通常不作主激活</td>
</tr>
<tr>
<td>Tanh</td>
<td>把实数映射到 <span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span></td>
<td>逐元素非线性变换</td>
<td>早期 RNN、门控结构</td>
<td>零中心，但同样存在饱和问题</td>
</tr>
<tr>
<td>Softmax</td>
<td>把一组分数归一化为概率分布</td>
<td>类别分数到概率</td>
<td>多分类头、attention 权重</td>
<td>常与交叉熵和 mask 配合，数值稳定性关键</td>
</tr>
<tr>
<td>LayerNorm</td>
<td>对单个样本的最后若干维做归一化</td>
<td>输入张量到同 shape 张量</td>
<td>Transformer、LLM</td>
<td>不依赖 batch 统计量，适合变长序列</td>
</tr>
<tr>
<td>BatchNorm</td>
<td>利用 batch 统计量做归一化</td>
<td>输入张量到同 shape 张量</td>
<td>CNN、视觉任务</td>
<td>训练与推理行为不同，小 batch 时效果可能下降</td>
</tr>
<tr>
<td>RMSNorm</td>
<td>基于均方根做归一化</td>
<td>输入张量到同 shape 张量</td>
<td>许多现代大语言模型</td>
<td>比 LayerNorm 更简洁，计算更轻</td>
</tr>
<tr>
<td>Attention / SDPA</td>
<td>基于相似度对值向量加权聚合</td>
<td>Q、K、V 到上下文表示</td>
<td>Transformer、跨模态模型</td>
<td>高层看是算子族，底层常映射到 FlashAttention 等 kernel</td>
</tr>
<tr>
<td>Embedding Lookup</td>
<td>根据离散索引查表取向量</td>
<td>token id 到 embedding 向量</td>
<td>NLP、推荐系统、类别特征</td>
<td>本质是参数矩阵的索引读取，不是普通 MatMul</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">序列与索引操作类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Gather</td>
<td>按给定索引抽取元素或切片</td>
<td>源张量与索引张量</td>
<td>embedding、beam search、采样</td>
<td>访问模式离散，容易受内存带宽限制</td>
</tr>
<tr>
<td>Scatter / ScatterAdd</td>
<td>按索引写回或累加</td>
<td>目标张量、索引、更新值</td>
<td>图神经网络、稀疏更新</td>
<td>并发写冲突和原子操作代价常是性能瓶颈</td>
</tr>
<tr>
<td>Index Select</td>
<td>按某一维选取指定位置</td>
<td>张量与一维索引</td>
<td>子序列抽取、类别筛选</td>
<td>语义上比通用 gather 更窄，但常更清晰</td>
</tr>
<tr>
<td>Slice / Narrow</td>
<td>截取连续区间</td>
<td>大张量切出子张量</td>
<td>窗口注意力、局部特征抽取</td>
<td>若数据连续，可几乎零开销视图化</td>
</tr>
<tr>
<td>Mask Fill / Select</td>
<td>按布尔掩码选择或填充值</td>
<td>张量与 mask</td>
<td>attention mask、padding 屏蔽</td>
<td>对变长序列与非法位置处理非常关键</td>
</tr>
<tr>
<td>Where</td>
<td>按条件在两个值之间选择</td>
<td>条件张量与候选张量</td>
<td>条件计算、loss 屏蔽、数值裁剪</td>
<td>本质是逐元素条件分支</td>
</tr>
<tr>
<td>Argmax / Argmin</td>
<td>返回最大 / 最小值所在索引</td>
<td>张量到索引</td>
<td>分类预测、贪心解码</td>
<td>输出是位置而非概率，通常不可导</td>
</tr>
<tr>
<td>TopK</td>
<td>返回前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个值及其索引</td>
<td>张量到值与索引</td>
<td>检索、beam search、采样截断</td>
<td>常与排序、候选筛选结合</td>
</tr>
<tr>
<td>Sort / Argsort</td>
<td>排序并返回顺序</td>
<td>张量到排序结果</td>
<td>排序损失、候选重排</td>
<td>复杂度高，尽量只在必要路径中使用</td>
</tr>
<tr>
<td>Pad</td>
<td>在边界补零或补指定值</td>
<td>原张量到更大张量</td>
<td>卷积前处理、batch 对齐、序列补齐</td>
<td>padding 策略会影响有效计算比例</td>
</tr>
<tr>
<td>Pack / Unpack Sequence</td>
<td>压缩或还原变长序列表示</td>
<td>变长序列与紧凑表示之间转换</td>
<td>RNN、语音与时序模型</td>
<td>用于减少 padding 带来的无效计算</td>
</tr>
<tr>
<td>Position Encoding Add</td>
<td>注入位置信息</td>
<td>token 表示与位置编码</td>
<td>Transformer、序列模型</td>
<td>绝对位置、相对位置、RoPE 在实现形式上不同</td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">训练与优化类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">算子</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">常见输入 / 输出</td>
<td style="text-align: center;">典型出现位置</td>
<td style="text-align: center;">实现与使用要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>Dropout</td>
<td>训练时随机失活部分单元</td>
<td>输入张量到稀疏化后的张量</td>
<td>MLP、attention、分类头</td>
<td>训练和推理行为不同，推理阶段通常关闭</td>
</tr>
<tr>
<td>Cross-Entropy Loss</td>
<td>衡量预测分布与真实类别的差距</td>
<td>logits 与标签到标量损失</td>
<td>分类、语言模型、token 分类</td>
<td>实现中常与 log-softmax 融合提高稳定性</td>
</tr>
<tr>
<td>NLL Loss</td>
<td>负对数似然损失</td>
<td>对数概率与标签到损失</td>
<td>分类任务、序列建模</td>
<td>通常接在 log-softmax 之后</td>
</tr>
<tr>
<td>MSE Loss</td>
<td>均方误差</td>
<td>预测值与目标值到损失</td>
<td>回归、蒸馏、表示对齐</td>
<td>对异常值较敏感</td>
</tr>
<tr>
<td>L1 / SmoothL1 Loss</td>
<td>绝对误差或平滑绝对误差</td>
<td>预测值与目标值到损失</td>
<td>目标检测、鲁棒回归</td>
<td>比 MSE 对异常值更稳</td>
</tr>
<tr>
<td>KL Divergence</td>
<td>衡量两个分布之间的差异</td>
<td>两个概率分布到标量</td>
<td>知识蒸馏、VAE、分布对齐</td>
<td>输入通常需要是概率或对数概率</td>
</tr>
<tr>
<td>Backward / Gradient</td>
<td>沿计算图反向传播梯度</td>
<td>损失到各参数梯度</td>
<td>所有训练流程</td>
<td>框架自动求导的核心能力就体现在这里</td>
</tr>
<tr>
<td>Gradient Clip</td>
<td>限制梯度范数或幅值</td>
<td>梯度到裁剪后梯度</td>
<td>RNN、大模型训练</td>
<td>控制梯度爆炸，提升训练稳定性</td>
</tr>
<tr>
<td>Optimizer Step（SGD / Adam / AdamW）</td>
<td>根据梯度更新参数</td>
<td>参数、梯度、状态到新参数</td>
<td>每一步训练迭代</td>
<td>常被实现为 fused optimizer kernel 以减少开销</td>
</tr>
<tr>
<td>Weight Decay</td>
<td>对参数施加正则化收缩</td>
<td>参数到受约束更新</td>
<td>分类、语言模型、视觉模型</td>
<td>现代实现常与 AdamW 解耦</td>
</tr>
<tr>
<td>AllReduce</td>
<td>跨设备聚合梯度或统计量</td>
<td>多卡张量到同步后的张量</td>
<td>数据并行训练</td>
<td>严格说更接近通信算子，但在训练图中极常见</td>
</tr>
<tr>
<td>AllGather / ReduceScatter</td>
<td>跨设备收集或切分张量</td>
<td>多设备张量通信</td>
<td>张量并行、序列并行、ZeRO</td>
<td>大模型分布式训练不可缺少</td>
</tr>
</tbody>
</table>
<p>因此，算子层是连接“模型定义”和“底层执行”的语义中枢。看懂模型实际调用了哪些算子，基本就等于看懂了它在做哪些数学步骤，以及这些步骤能否被进一步融合和加速。</p>
<div class="blog_h3"><span class="graybg">内核（Kernel）</span></div>
<p>内核（Kernel）是算子的底层实现。它规定了某个算子如何映射到具体硬件：线程如何组织、数据如何分块、是否使用共享内存（Shared Memory）、是否调用向量化指令、是否走 Tensor Core、是否把多个小算子融合成一次执行。若说算子定义的是“做什么”，那么内核定义的就是“怎样在这台机器上把它做快”。</p>
<p>从性能工程角度看，内核层回答的是：<span style="background-color: #c0c0c0;">同一个数学算子，针对不同硬件、数据形状、精度格式和访存模式，哪种实现最优</span>。这也是为什么表面上同样是 MatMul、LayerNorm 或 Attention，不同框架和后端的速度会相差很大。</p>
<p>常见内核或内核族可以按下表理解：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">内核 / 内核族</td>
<td style="text-align: center;">主要服务的算子</td>
<td style="text-align: center;">典型硬件 / 后端</td>
<td style="text-align: center;">核心优化手段</td>
<td style="text-align: center;">最典型的收益</td>
<td style="text-align: center;">常见场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>cuBLAS GEMM kernel</td>
<td>MatMul、Linear、Batch MatMul</td>
<td>NVIDIA GPU</td>
<td>tile blocking、Tensor Core、流水线、寄存器复用</td>
<td>把矩阵乘法做到接近硬件峰值吞吐</td>
<td>全连接层、attention 投影、MLP</td>
</tr>
<tr>
<td>cuDNN Convolution kernel</td>
<td>Convolution、Pooling、部分归一化与激活</td>
<td>NVIDIA GPU</td>
<td>direct convolution、im2col + GEMM、Winograd、FFT 自动选择</td>
<td>按输入形状自动切换最优卷积路径</td>
<td>CNN、视觉 backbone、语音前端</td>
</tr>
<tr>
<td>oneDNN / MKL kernel</td>
<td>MatMul、Convolution、Normalization</td>
<td>x86 CPU</td>
<td>SIMD 向量化、cache blocking、线程并行</td>
<td>提升 CPU 推理与训练效率</td>
<td>服务器 CPU 推理、无 GPU 环境</td>
</tr>
<tr>
<td>NCCL communication kernel</td>
<td>AllReduce、AllGather、ReduceScatter、Broadcast</td>
<td>NVIDIA 多 GPU</td>
<td>环形通信（Ring）、树形通信（Tree）、链路拓扑优化</td>
<td>降低多卡同步开销</td>
<td>数据并行、张量并行、大模型训练</td>
</tr>
<tr>
<td>FlashAttention kernel</td>
<td>Scaled Dot-Product Attention</td>
<td>NVIDIA GPU 及其他支持相应实现的加速器</td>
<td>分块、在线 softmax、kernel fusion、减少 HBM 访存</td>
<td>把 attention 从显存瓶颈拉回到更接近计算瓶颈</td>
<td>Transformer、LLM、长序列建模</td>
</tr>
<tr>
<td>Fused LayerNorm / RMSNorm kernel</td>
<td>LayerNorm、RMSNorm、Bias Add、Residual Add</td>
<td>GPU / CPU 后端</td>
<td>多步逐元素运算融合为一次访存</td>
<td>显著降低 memory-bound 算子的开销</td>
<td>Transformer block、LLM 推理</td>
</tr>
<tr>
<td>Fused MLP kernel</td>
<td>Bias Add、GELU / SiLU、Dropout、Residual</td>
<td>GPU</td>
<td>把连续逐元素算子合并，减少中间张量写回</td>
<td>减少 kernel launch 次数与显存读写</td>
<td>MLP block、前馈网络</td>
</tr>
<tr>
<td>Triton custom kernel</td>
<td>任意自定义算子或 fused operator</td>
<td>GPU</td>
<td>开发者手写 tile、访存布局与并行策略</td>
<td>在通用库缺少最优实现时获得定制性能</td>
<td>大模型训练、研究型性能优化、自定义融合</td>
</tr>
<tr>
<td>TensorRT generated kernel</td>
<td>部署图中的卷积、MatMul、激活、归一化、量化路径</td>
<td>NVIDIA GPU</td>
<td>图优化、层融合、低精度选择、kernel autotuning</td>
<td>显著降低推理时延并提升吞吐</td>
<td>在线推理服务、边缘推理</td>
</tr>
<tr>
<td>XLA generated kernel</td>
<td>JAX / TensorFlow 图中的可融合算子子图</td>
<td>GPU、TPU 等</td>
<td>图级融合、静态形状分析、编译生成目标代码</td>
<td>让一串算子整体下沉为更大的执行单元</td>
<td>JAX 训练、TPU 训练、编译型执行场景</td>
</tr>
<tr>
<td>TVM generated kernel</td>
<td>自定义张量表达式对应的算子</td>
<td>CPU、GPU、边缘加速器</td>
<td>自动调度、自动搜索、代码生成</td>
<td>跨异构硬件获得针对性实现</td>
<td>端侧部署、自定义芯片适配</td>
</tr>
<tr>
<td>PagedAttention / KV-cache kernel</td>
<td>增量解码中的 attention 与缓存访问</td>
<td>LLM 推理后端</td>
<td>分页管理 KV cache、优化随机访问和 batch 合并</td>
<td>提升长上下文与多请求并发推理效率</td>
<td>大语言模型在线推理</td>
</tr>
</tbody>
</table>
<p>理解内核时最重要的一点是：<span style="background-color: #c0c0c0;">一个算子并不对应唯一实现</span>。同样是卷积，可以选 direct convolution、Winograd 或 FFT；同样是 attention，可以选普通分步实现、FlashAttention 或推理场景下的 paged attention。真正的差异往往不在数学定义，而在访存方式、融合策略、并行粒度和硬件利用率上。</p>
<p>因此，内核并不是建模阶段最先暴露给用户的对象，却往往决定模型最终的吞吐、时延、显存占用、能耗和成本。模型结构决定“上限在哪里”，内核质量决定“这个上限能兑现多少”。</p>
<div class="blog_h3"><span class="graybg">三者关系</span></div>
<p>把三者串起来看，一个卷积层或注意力层的执行路径通常是这样的：开发者先在 PyTorch 或 JAX 中写出一个模块；框架把它拆成若干算子，例如 MatMul、Softmax、LayerNorm 或 Convolution；运行时再为每个算子选择对应硬件上的 kernel，例如 cuBLAS 的 GEMM kernel、cuDNN 的 convolution kernel，或 Triton 写成的自定义 fused kernel。整个训练与推理过程由框架负责调度，算子负责表达数学语义，内核负责把语义变成高性能机器代码。</p>
<p>可以用一个具体例子来理解。若在 PyTorch 中定义一个卷积层，那么：</p>
<ol>
<li>PyTorch 作为框架负责接收模块定义、组织张量、记录梯度关系并调度执行。</li>
<li>卷积（Convolution）作为算子表示“输入特征图与卷积核做局部加权求和”这一数学操作。</li>
<li>底层可能调用 cuDNN 提供的卷积 kernel，在 GPU 上以高度优化的方式完成真正计算。</li>
</ol>
<p>同样地，在 Transformer 中写一层自注意力时，框架会组织前向与反向图；MatMul、Softmax、Mask、LayerNorm 等作为算子组成计算链；而 FlashAttention、fused LayerNorm、paged attention 这类高性能实现，则属于内核或 kernel-level optimization 的范畴。</p>
<p>因此，这三个层次的关系可以概括为：<span style="background-color: #c0c0c0;">框架管理整个建模与执行流程，算子定义模型中的数学步骤，内核负责把这些步骤在具体硬件上高效落地</span>。从研究到工程落地的能力鸿沟，往往正体现在能否同时理解这三层。</p>
<div class="blog_h1"><span class="graybg">经典机器学习</span></div>
<div class="blog_h2"><span class="graybg">选型指南</span></div>
<p>经典机器学习的模型选择，本质上是在<span style="background-color: #c0c0c0;">任务形式、数据规模、特征形态、可解释性要求、训练与推断成本</span>之间做匹配。只要先判断“有没有标签”“输出是什么类型”“样本量有多大”“特征是不是稀疏或非线性”“是否需要概率或规则解释”，大多数任务都可以迅速缩小到少数几个候选模型。</p>
<p>本章中的模型并不是按“先进程度”排序，而是按建模假设来区分。线性模型假设边界或关系接近线性；树模型更擅长表格数据中的非线性交互；概率模型更强调分布解释；近邻模型依赖局部相似性；聚类与降维模型关注无监督结构；HMM（Hidden Markov Model, HMM）和条件随机场（Conditional Random Field, CRF）则处理序列标签之间存在依赖的结构化预测问题。</p>
<p>下面的表格不是概览式罗列，而是直接服务于选型。每一行都回答五个问题：<span style="background-color: #c0c0c0;">什么情况下优先选它、它最依赖什么数据条件、它能解决什么核心诉求、为什么它在该场景里合适、以及什么情况下应当换模型</span>。在大多数工程场景里，先按这些表格缩小范围，再进入具体模型细节，效率最高。</p>
<div class="blog_h3"><span class="graybg">任务类型：分类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>逻辑回归</td>
<td>需要稳定强基线、需要概率输出、需要解释每个特征如何影响类别</td>
<td>特征可以线性分离到一定程度；高维稀疏特征尤其合适，如 one-hot、词袋、统计特征</td>
<td>训练快、概率自然、权重可解释，适合作为第一版可上线模型</td>
<td>类别边界高度非线性、特征交互复杂且没有显式特征工程时</td>
</tr>
<tr>
<td>支持向量机（SVM）</td>
<td>样本中小规模、类别边界较清晰、希望用最大间隔提升泛化稳健性</td>
<td>特征需要做尺度统一；核 SVM 更适合中小规模，不适合超大样本</td>
<td>对边界样本建模强，在线性不可分但结构仍较规整时往往优于纯线性模型</td>
<td>数据量很大、需要快速训练与部署、或者必须输出天然概率时</td>
</tr>
<tr>
<td>决策树</td>
<td>需要把模型直接解释成规则路径，或业务天然是“按阈值分流”的形式</td>
<td>表格特征、混合数值与类别特征都可；不强依赖标准化</td>
<td>规则可视化最直接，便于和业务规则、审计规则对齐</td>
<td>追求最稳泛化性能时；单树通常方差高，容易过拟合</td>
</tr>
<tr>
<td>随机森林</td>
<td>表格分类需要稳健基线、希望少调参、担心单棵树过拟合</td>
<td>适合中等规模表格数据；对噪声、缺失和特征尺度通常更宽容</td>
<td>比单树稳定，通常先于复杂 boosting 模型给出可靠结果</td>
<td>追求表格任务的极致精度，或需要很小的模型体积时</td>
</tr>
<tr>
<td>梯度提升树（GBDT）</td>
<td>表格分类精度优先，特征中存在明显非线性与交互效应</td>
<td>适合结构化特征，不要求线性关系；通常需要一定调参</td>
<td>能逐轮修正前一轮错误，在中等规模表格任务上常是强力基线</td>
<td>需要极快训练、极少调参，或数据已经极大到串行 boosting 成本明显偏高时</td>
</tr>
<tr>
<td>XGBoost</td>
<td>工业表格分类、竞赛任务、缺失值与正则化处理都很重要</td>
<td>适合中大规模结构化数据；对特征工程和超参数较敏感，但工程支持成熟</td>
<td>精度高、鲁棒、缺失值处理成熟，是很多表格分类任务的首选之一</td>
<td>极端大规模、极度强调训练速度与内存效率时，LightGBM 往往更优先</td>
</tr>
<tr>
<td>LightGBM</td>
<td>大规模表格分类、高维稀疏特征、需要更快训练与更低内存消耗</td>
<td>适合样本量大、特征维度高的结构化任务；对类别特征和稀疏特征较友好</td>
<td>训练快、工程效率高，在工业 CTR、风控、推荐特征场景很常见</td>
<td>数据量较小且噪声较大时；叶子生长策略若不控制，容易过拟合</td>
</tr>
<tr>
<td>朴素贝叶斯</td>
<td>需要极快文本分类基线、小样本启动、希望先验证特征是否有判别力</td>
<td>最适合词袋、词频、计数类高维稀疏特征；默认接受条件独立近似</td>
<td>训练和推断都极快，在垃圾邮件、主题粗分类等任务上常有效</td>
<td>强依赖复杂特征相关性、类别边界非线性明显、或需要高精度概率校准时</td>
</tr>
<tr>
<td>K 近邻（KNN）</td>
<td>样本规模小、相似样本应有相似标签、希望不做显式训练</td>
<td>距离度量必须有意义；所有特征应标准化，且维度不能过高</td>
<td>局部模式直观，适合做小数据原型验证或相似样本检索式分类</td>
<td>高维稀疏特征、大规模数据、低延迟在线推断场景</td>
</tr>
<tr>
<td>线性判别分析（LDA）</td>
<td>有监督分类同时希望压缩特征维度，且类别统计结构较稳定</td>
<td>更适合每类近似高斯、类内协方差可估计的情况；样本数不能太少</td>
<td>把“降维”和“分类判别”结合起来，适合先压缩再分类的流程</td>
<td>类别分布非常复杂、强非高斯、强非线性时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：回归</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>线性回归</td>
<td>需要连续值预测、希望建立可解释、可审计、可稳定复现的基线</td>
<td>目标与特征大致满足线性关系，或经过变换后接近线性</td>
<td>系数解释直观，便于分析“哪个因素把目标推高或拉低”</td>
<td>目标关系明显呈现复杂分段、交互或强非线性时</td>
</tr>
<tr>
<td>Lasso（L1 正则化）</td>
<td>特征很多、怀疑只有少数特征真正有效、希望模型自动做变量筛选</td>
<td>高维特征尤其适合；允许部分权重被压到 0</td>
<td>回归同时完成特征选择，适合构建更稀疏、更简洁的模型</td>
<td>大量强相关特征共同起作用时；它可能只保留其中部分特征</td>
</tr>
<tr>
<td>岭回归 / L2 正则化</td>
<td>特征共线性明显、担心普通线性回归权重不稳定</td>
<td>适合多个相关特征共同解释目标，而不希望稀疏淘汰其中一部分</td>
<td>通过收缩权重降低方差，使模型在相关特征场景下更稳定</td>
<td>首要目标是做特征筛选、希望很多系数直接变成 0 时</td>
</tr>
<tr>
<td>Elastic Net（L1 + L2 正则化）</td>
<td>既想做特征选择，又不希望在相关特征组上过于不稳定</td>
<td>高维、相关特征并存的回归任务最常见</td>
<td>综合 Lasso 与岭回归的优点，在稀疏性和稳定性之间折中</td>
<td>特征数量不多、模型目标非常简单时；其调参成本高于纯 L1 或 L2</td>
</tr>
<tr>
<td>决策树</td>
<td>目标值与输入关系近似分段函数，或业务逻辑天然围绕阈值展开</td>
<td>表格特征、混合类型特征都可；不需要严格线性假设</td>
<td>能学出“满足什么条件时预测值跳到哪个区间”的规则</td>
<td>希望预测曲线平滑、稳定，或希望泛化误差尽可能低时</td>
</tr>
<tr>
<td>随机森林</td>
<td>希望回归稳健、抗噪声、少调参，先拿到可靠效果</td>
<td>表格回归场景最常见；对特征尺度和局部异常较稳</td>
<td>综合多个树模型平均结果，通常比单树更不容易过拟合</td>
<td>要求外推能力强，或希望模型极度轻量、延迟极低时</td>
</tr>
<tr>
<td>梯度提升树（GBDT）</td>
<td>表格回归精度优先，目标和特征之间存在复杂非线性</td>
<td>适合异构表格特征；对异常值和长尾目标通常也较有韧性</td>
<td>在房价、评分、收益、风险等典型表格回归中常是强基线</td>
<td>算力非常紧、需要极低训练成本时</td>
</tr>
<tr>
<td>XGBoost / LightGBM</td>
<td>工业级表格回归、大规模特征、希望兼顾精度与工程效率</td>
<td>适合结构化数据；XGBoost 更稳健成熟，LightGBM 更强调速度与规模</td>
<td>常作为表格回归默认候选，能直接处理大量非线性和特征交互</td>
<td>数据关系本来就简单线性、并且强依赖系数解释时</td>
</tr>
<tr>
<td>K 近邻（KNN）</td>
<td>局部相似样本的目标值应当接近，希望用邻域平均直接预测</td>
<td>小数据、低维、有意义的距离度量是前提</td>
<td>局部平滑性强时实现简单有效，适合作为相似样本回归基线</td>
<td>高维、大样本、分布稀疏或在线延迟敏感时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：聚类</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>K-Means</td>
<td>需要快速把样本分成若干组，并且预期每组都围绕某个均值中心展开</td>
<td>簇大致是球形或凸形；欧氏距离有意义；需要先给定 <span displaypfx="inline-" class="mathjax-container">\(K\)</span></td>
<td>速度快、实现简单，适合作为无监督分群第一选择</td>
<td>簇形状复杂、密度差异大、离群点很多或无法预先估计簇数时</td>
</tr>
<tr>
<td>层次聚类</td>
<td>不仅想得到分组结果，还想知道各组是如何逐层合并成层级结构的</td>
<td>更适合中小规模数据；需要能接受 <span displaypfx="inline-" class="mathjax-container">\(O(N^2)\)</span> 级别距离矩阵成本</td>
<td>能输出树状图，适合做群体结构分析与多粒度解释</td>
<td>数据量很大、只关心最终聚类标签、不关心层级关系时</td>
</tr>
<tr>
<td>DBSCAN</td>
<td>希望识别任意形状簇，并把稀疏孤立点单独作为噪声剔除</td>
<td>密度尺度相对统一；距离度量有意义；参数 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 邻域半径和最小点数可估计</td>
<td>不需要预设簇数，能自然处理非凸簇和离群点</td>
<td>不同区域密度差异很大时；单一密度阈值难兼顾所有簇</td>
</tr>
<tr>
<td>HDBSCAN</td>
<td>簇的密度明显不一致，希望保留 DBSCAN 的密度思想但更自适应</td>
<td>数据存在多密度结构；仍需合理距离度量</td>
<td>比 DBSCAN 更能处理“有的簇很密、有的簇较松”的真实数据</td>
<td>只需要一个快速、简单、可复现的基础分群结果时</td>
</tr>
<tr>
<td>高斯混合模型（GMM）</td>
<td>希望得到软聚类结果，或者认为每个簇更像椭球形概率团块</td>
<td>连续特征较适合；默认每个簇可近似为一个高斯成分</td>
<td>不仅给簇标签，还给每个样本属于各簇的概率</td>
<td>簇形状非常复杂、非高斯、多峰结构难用少量高斯逼近时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：降维与可视化</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>主成分分析（PCA）</td>
<td>希望做线性降维、压缩冗余特征、去噪或为下游模型降成本</td>
<td>主要结构能由少数线性主方向解释；不依赖标签</td>
<td>保留最大方差方向，是最稳健、最常用的无监督降维起点</td>
<td>真正关心的是类别判别性而不是总体方差，或数据结构强非线性时</td>
</tr>
<tr>
<td>线性判别分析（LDA）</td>
<td>有标签并希望把不同类别拉开后再做分类或可视化</td>
<td>类别标签可靠；类内散度与类间散度都可稳定估计</td>
<td>直接围绕“可分性”找投影，比 PCA 更贴近分类目标</td>
<td>没有标签、类别边界强非线性、或类别数太少导致可降维空间有限时</td>
</tr>
<tr>
<td>t-SNE</td>
<td>想把高维嵌入压到二维或三维，只为看局部邻域是否形成簇</td>
<td>更适合可视化，不适合直接做可逆特征压缩</td>
<td>局部邻域展示能力强，适合分析表征是否把相似样本聚到一起</td>
<td>需要把降维结果直接送入生产模型，或需要保留严格全局距离关系时</td>
</tr>
<tr>
<td>UMAP</td>
<td>希望做更快的大规模可视化，兼顾局部结构与部分全局连通性</td>
<td>适合高维嵌入、文本向量、图表示等复杂表征</td>
<td>通常比 t-SNE 更快，也更容易保留大体流形结构</td>
<td>需要线性可解释主方向，或需要结果完全稳定可重复到坐标级别时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：异常检测</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>孤立森林（Isolation Forest）</td>
<td>通用表格异常检测，希望先得到一个稳健、扩展性好的异常分数</td>
<td>适合中大规模数据；不要求明确概率分布形式</td>
<td>通过随机切分隔离少数样本，通常是异常检测第一基线</td>
<td>异常定义依赖非常精细的局部密度差异时</td>
</tr>
<tr>
<td>局部异常因子（LOF）</td>
<td>异常并不是全局离群，而是“在本地邻域里显得稀疏”</td>
<td>距离度量必须合理；样本规模不宜过大</td>
<td>能发现那些整体位置不极端、但局部密度明显偏低的点</td>
<td>高维距离失真严重、或需要高吞吐在线检测时</td>
</tr>
<tr>
<td>单类支持向量机（One-Class SVM）</td>
<td>只有正常样本，目标是学习正常数据的封闭边界</td>
<td>特征需标准化；更适合中小规模；核方法对边界形状有帮助</td>
<td>适合“正常类定义清楚、异常类没有稳定样本”的场景</td>
<td>数据量很大、特征维度很高、参数难以稳定选择时</td>
</tr>
<tr>
<td>高斯混合模型（GMM）</td>
<td>希望把异常定义为低概率区域，并明确得到似然分数</td>
<td>连续数据更合适；分布能用若干高斯混合近似</td>
<td>异常判定有明确概率语义，适合风险评分和阈值分析</td>
<td>数据分布非常复杂、非高斯、或异常并不等价于低密度时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：序列标注与结构化预测</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>隐马尔可夫模型（HMM）</td>
<td>序列较短、转移规律明显、希望在较小数据和较强先验下完成基础序列建模</td>
<td>状态转移与观测发射假设大致成立；问题适合生成式描述</td>
<td>结构清晰、推断高效，适合作为经典序列建模入门和小规模基线</td>
<td>标签强依赖全局上下文、特征复杂、需要大量判别式特征时</td>
</tr>
<tr>
<td>条件随机场（CRF）</td>
<td>输出不是单点分类而是整条标签序列，并且相邻标签的合法性非常关键</td>
<td>一维链式序列最合适；上游特征或表示需要至少能提供较强局部证据</td>
<td>通过整体解码约束标签转移，使最终标签序列更一致、更符合任务结构</td>
<td>长距离语义主要由强表征模型决定、标签约束作用很弱，或任务更适合 span 建模时</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">任务类型：密度估计与概率建模</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">优先选择条件</td>
<td style="text-align: center;">数据与特征前提</td>
<td style="text-align: center;">最主要价值</td>
<td style="text-align: center;">不建议优先使用的情况</td>
</tr>
</thead>
<tbody>
<tr>
<td>朴素贝叶斯</td>
<td>希望快速得到后验概率分类器，并接受条件独立近似</td>
<td>高维稀疏离散特征最合适，如文本词频、词出现计数</td>
<td>概率形式直接、估计简单，适合快速建模和在线系统</td>
<td>特征依赖结构复杂、需要精细表达联合分布时</td>
</tr>
<tr>
<td>高斯混合模型（GMM）</td>
<td>希望显式建模连续数据分布，或需要软聚类与密度估计统一完成</td>
<td>连续数据、多峰分布、可近似为若干高斯成分</td>
<td>每个样本都能得到各成分责任概率，解释和阈值分析都较自然</td>
<td>分布极端复杂、重尾严重、或高斯成分数难合理确定时</td>
</tr>
<tr>
<td>隐马尔可夫模型（HMM）</td>
<td>不仅要建模观测分布，还要建模隐藏状态在时间上的转移机制</td>
<td>观测是序列，且状态依赖主要体现在相邻时刻</td>
<td>把“序列生成机制”和“时序转移规律”统一到一个概率模型里</td>
<td>长程依赖很强、局部马尔可夫假设明显不成立时</td>
</tr>
</tbody>
</table>
<p>若只是要一个工程上可执行的默认起点，可以进一步压缩成如下规则：表格分类与回归优先从随机森林、GBDT、XGBoost、LightGBM 和线性模型里选；文本高维稀疏分类优先看逻辑回归和朴素贝叶斯；无监督分群先看 K-Means，再根据簇形状和噪声情况转向 DBSCAN、HDBSCAN 或 GMM；需要可视化时先区分是要线性压缩还是只要展示结构，再在 PCA、LDA、t-SNE、UMAP 中选择；涉及标签序列依赖时再进入 HMM 与 CRF。</p>
<div class="blog_h2"><span class="graybg">线性模型</span></div>
<p>线性模型（Linear Models）的核心不是“世界一定是线性的”，而是先用一个可解释、可优化、常常足够强的基线去刻画输入与输出的关系。很多复杂模型也可以看作“在线性读出层之前先做更强的特征变换”。</p>
<div class="blog_h3"><span class="graybg">线性回归</span></div>
<p>线性回归（Linear Regression）在回归任务中建模“输入特征的加权求和如何产生连续输出”。它的价值在于：<span style="background-color: #c0c0c0;">可解释</span>（权重直接对应特征影响）与<span style="background-color: #c0c0c0;">可优化</span>（凸问题，训练稳定）。</p>
<div class="blog_h4"><span class="graybg">模型与符号</span></div>
<p>给定训练集 <span displaypfx="inline-" class="mathjax-container">\(\{(\mathbf{x}_i,y_i)\}_{i=1}^{N}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\in\mathbb{R}^d\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的特征向量， <span displaypfx="inline-" class="mathjax-container">\(y_i\in\mathbb{R}\)</span> 是对应标签。线性回归假设单样本预测为：</p>
<span displaypfx="" class="mathjax-container">\[\hat y_i=\mathbf{w}^\top \mathbf{x}_i+b\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\hat y_i\)</span>：对 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 的预测。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\in\mathbb{R}^d\)</span>：权重向量（每个维度对应一个特征）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(b\in\mathbb{R}\)</span>：偏置（Bias），用于整体平移。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top \mathbf{x}_i\)</span>：内积（Dot Product），表示“按特征加权求和”。</li>
</ul>
<p>把全部样本写成矩阵形式。令设计矩阵（Design Matrix）<span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}\in\mathbb{R}^{N\times d}\)</span> 的第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 行为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\)</span>，标签向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\in\mathbb{R}^{N}\)</span> 的第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量为 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span>，全 1 向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{1}\in\mathbb{R}^{N}\)</span>。则：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\mathbf{y}}=\mathbf{X}\mathbf{w}+b\mathbf{1}\]</span>
<p>直觉类比：它像“按因素打分再加总”。例如房价预测里，面积、地段评分、楼龄都可作为特征；权重正负决定影响方向，绝对值大小决定影响强弱。</p>
<div class="blog_h4"><span class="graybg">目标函数（最小二乘）</span></div>
<p>最常见的训练目标是最小化均方误差（Mean Squared Error, MSE）的总和（或平均）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{EN}}(\mathbf{w},b)=\frac{1}{N}\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\|_2^2+\lambda_1\|\mathbf{w}\|_1+\lambda_2\|\mathbf{w}\|_2^2\]</span>
<p>这个目标可以拆成三部分理解：第一项 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{N}\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\|_2^2\)</span> 是数据拟合误差，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\)</span> 是所有样本的残差向量；第二项 <span displaypfx="inline-" class="mathjax-container">\(\lambda_1\|\mathbf{w}\|_1\)</span> 倾向把一部分权重直接压到 0；第三项 <span displaypfx="inline-" class="mathjax-container">\(\lambda_2\|\mathbf{w}\|_2^2\)</span> 倾向把权重整体缩小得更平滑。这里 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 是样本数， <span displaypfx="inline-" class="mathjax-container">\(\lambda_1,\lambda_2\)</span> 是正则化强度。若两者都取 0，就退化为普通最小二乘。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(J(\mathbf{w},b)\)</span>：目标函数（Objective）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_2\)</span>：二范数（Euclidean Norm），向量的长度。</li>
</ul>
<p>平方项会对大残差施加更大惩罚，因此训练会优先修正“偏得很离谱”的样本。并且在高斯噪声假设下，最小二乘等价于最大似然估计（Maximum Likelihood Estimation, MLE）导出的解。</p>
<div class="blog_h4"><span class="graybg">解析解（正规方程）</span></div>
<p>把偏置吸收到特征中更方便：定义增广特征 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{x}}_i=[\mathbf{x}_i;1]\in\mathbb{R}^{d+1}\)</span>，增广参数 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{w}}=[\mathbf{w};b]\in\mathbb{R}^{d+1}\)</span>，增广矩阵 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}=[\mathbf{X},\mathbf{1}]\in\mathbb{R}^{N\times(d+1)}\)</span>。则 <span displaypfx="inline-" class="mathjax-container">\(\hat{\mathbf{y}}=\tilde{\mathbf{X}}\tilde{\mathbf{w}}\)</span>，目标为 <span displaypfx="inline-" class="mathjax-container">\(\min_{\tilde{\mathbf{w}}}\|\mathbf{y}-\tilde{\mathbf{X}}\tilde{\mathbf{w}}\|_2^2\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\)</span> 可逆，则正规方程（Normal Equation）给出闭式解：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\mathbf{w}}^*=\left(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\right)^{-1}\tilde{\mathbf{X}}^\top\mathbf{y}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{w}}^*\)</span> 表示最优增广参数，前 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维对应原权重 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span>，最后一维对应偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>。 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\)</span> 汇总了特征之间的相关结构， <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\mathbf{y}\)</span> 汇总了特征与标签之间的相关程度，因此这个闭式解本质上是在“用整体相关关系一次性解出最优线性系数”。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\in\mathbb{R}^{(d+1)\times(d+1)}\)</span>：Gram 矩阵，度量特征之间的相关性。</li>
<li><span displaypfx="inline-" class="mathjax-container">\((\cdot)^{-1}\)</span>：矩阵逆；不可逆时通常用伪逆（Pseudo-inverse）或加正则化处理。</li>
</ul>
<p>工程实现中，直接求逆并不推荐；更稳定的做法是解线性方程组或用 QR/SVD 分解。</p>
<div class="blog_h4"><span class="graybg">实例：把公式算一遍</span></div>
<p>用一维数据拟合 <span displaypfx="inline-" class="mathjax-container">\(y\approx wx+b\)</span>。给定三点 <span displaypfx="inline-" class="mathjax-container">\((x,y)\in\{(0,1),(1,3),(2,5)\}\)</span>。构造增广矩阵与标签：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\mathbf{X}}=\begin{bmatrix}0 &amp; 1\\ 1 &amp; 1\\ 2 &amp; 1\end{bmatrix},\quad \mathbf{y}=\begin{bmatrix}1\\ 3\\ 5\end{bmatrix}\]</span>
<p>这个增广矩阵的每一行对应一个样本；第一列是原始特征 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，第二列固定为 1，用来把偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 并入矩阵乘法。标签向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 则把三个样本的真实输出按顺序堆叠起来。</p>
<p>计算：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}=\begin{bmatrix}5 &amp; 3\\ 3 &amp; 3\end{bmatrix},\quad \tilde{\mathbf{X}}^\top\mathbf{y}=\begin{bmatrix}13\\ 9\end{bmatrix}\]</span>
<p>左边的矩阵可以看成所有样本在“特征与偏置”两个方向上的二阶统计量： <span displaypfx="inline-" class="mathjax-container">\(5\)</span> 来自 <span displaypfx="inline-" class="mathjax-container">\(0^2+1^2+2^2\)</span>， <span displaypfx="inline-" class="mathjax-container">\(3\)</span> 来自 <span displaypfx="inline-" class="mathjax-container">\(0+1+2\)</span>，右边向量 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\mathbf{X}}^\top\mathbf{y}\)</span> 则分别对应 <span displaypfx="inline-" class="mathjax-container">\(\sum_i x_i y_i\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\sum_i y_i\)</span>。这正是正规方程所需要的汇总量。</p>
<p>解线性方程 <span displaypfx="inline-" class="mathjax-container">\(\left(\tilde{\mathbf{X}}^\top\tilde{\mathbf{X}}\right)\tilde{\mathbf{w}}=\tilde{\mathbf{X}}^\top\mathbf{y}\)</span>，即：</p>
<span displaypfx="" class="mathjax-container">\[\begin{cases}5w+3b=13\\ 3w+3b=9\end{cases}\Rightarrow w=2,\ b=1\]</span>
<p>这组方程的未知量只有两个：斜率 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 和偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>。解出 <span displaypfx="inline-" class="mathjax-container">\(w=2\)</span> 表示特征每增加 1，预测值增加 2；解出 <span displaypfx="inline-" class="mathjax-container">\(b=1\)</span> 表示当 <span displaypfx="inline-" class="mathjax-container">\(x=0\)</span> 时，模型基线输出为 1。</p>
<p>因此预测为 <span displaypfx="inline-" class="mathjax-container">\(\hat y=2x+1\)</span>，恰好穿过三点。这个例子展示了：正规方程把“最小化平方误差”的优化问题，转换成一个线性方程组。</p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>需要可解释的特征贡献（权重）与可校验的线性关系。</li>
<li>表格数据（Tabular）中，关系接近线性或可通过特征变换/交互项线性化。</li>
<li>作为强基线：先用线性模型定位数据问题、特征质量与噪声水平，再决定是否需要更复杂模型。</li>
<li>高维稀疏特征（如 one-hot、文本 bag-of-words）下，配合正则化可获得稳定解。</li>
</ul>
<p>当特征很多、共线性（Collinearity）强或数据量相对不足时，普通最小二乘（Ordinary Least Squares, OLS）会出现高方差：训练集拟合很好、验证集误差上升。正则化（Regularization）通过惩罚参数规模，把“拟合训练误差”与“控制模型复杂度”写进同一个目标函数。</p>
<div class="blog_h3"><span class="graybg">Lasso（L1 正则化）</span></div>
<p>Lasso 把参数惩罚写成一范数（<span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> Norm）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{lasso}}(\mathbf{w},b)=\frac{1}{N}\left\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\right\|_2^2+\lambda\|\mathbf{w}\|_1\]</span>
<p>这个式子里，前半部分仍然是拟合误差，衡量预测和真实标签之间差多少；后半部分 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\mathbf{w}\|_1\)</span> 是稀疏惩罚，其中 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_1=\sum_j |w_j|\)</span> 会优先把不重要的权重压成 0。于是 Lasso 不只是“把权重变小”，而是经常顺带完成特征选择。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>：正则化强度（Regularization Strength）。越大表示越强的收缩。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_1=\sum_{j=1}^{d}|w_j|\)</span>：一范数，鼓励稀疏（Sparsity）。</li>
<li>通常不惩罚偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>；否则会把整体均值也一起往 0 拉。</li>
<li>Lasso 由于 <span displaypfx="inline-" class="mathjax-container">\(|\cdot|\)</span> 在 0 点不可导，一般没有像岭回归那样简洁的闭式解；常用坐标下降（Coordinate Descent）、近端梯度（Proximal Gradient）或 LARS 等算法求解。</li>
</ul>
<p>Lasso 的关键作用是让一部分权重被压到精确 0，而不是均匀缩小全部权重。它更像给参数设置了一个阈值：弱相关、贡献不足以抵消惩罚的特征，会被直接剔除。因此 Lasso 同时完成复杂度控制与特征选择（Feature Selection）。</p>
<p>几何上，在相同训练误差轮廓线下， <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 约束对应菱形/正八面体；这些尖角更容易与最优点相交在坐标轴上，因此常出现某些 <span displaypfx="inline-" class="mathjax-container">\(w_j=0\)</span> 的解。</p>
<ul>
<li>适合高维稀疏特征、希望自动筛特征的场景，例如广告、推荐、文本 one-hot 特征。</li>
<li>当特征高度相关时，Lasso 往往只保留其中少数几个，因此解的稳定性通常弱于岭回归。</li>
</ul>
<div class="blog_h3"><span class="graybg">岭回归 / L2 正则化</span></div>
<p>岭回归把参数惩罚写成二范数平方（<span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> Norm Squared）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{ridge}}(\mathbf{w},b)=\frac{1}{N}\left\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\right\|_2^2+\lambda\|\mathbf{w}\|_2^2\]</span>
<p>岭回归的结构与普通线性回归相同，只是在误差项之外额外加入了 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\mathbf{w}\|_2^2\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2^2=\sum_j w_j^2\)</span> 会连续惩罚过大的权重，因此它更擅长缓解共线性和过拟合，而不是像 Lasso 那样主动做稀疏选择。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>：正则化强度。越大表示越强的收缩。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2^2=\sum_{j=1}^{d}w_j^2\)</span>：二范数平方，连续惩罚大权重。</li>
<li>通常不惩罚偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>；否则会把整体均值也一起往 0 拉。</li>
</ul>
<p>从梯度角度看，惩罚项 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\mathbf{w}\|_2^2\)</span> 对单个参数 <span displaypfx="inline-" class="mathjax-container">\(w_j\)</span> 的梯度贡献是 <span displaypfx="inline-" class="mathjax-container">\(2\lambda w_j\)</span>。这就是 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则化在深度学习中被称为权重衰减（Weight Decay）的原因：它持续把权重按比例拉回 0。若取 <span displaypfx="inline-" class="mathjax-container">\(\lambda=1\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(w_j=10\)</span> 时，梯度为 <span displaypfx="inline-" class="mathjax-container">\(20\)</span>；优化器会施加强烈收缩，把这个大权重猛拉回去。当 <span displaypfx="inline-" class="mathjax-container">\(w_j=0.1\)</span> 时，梯度只有 <span displaypfx="inline-" class="mathjax-container">\(0.2\)</span>；往 0 拉的力量就很弱。也就是说，参数一旦变大， <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 惩罚立刻增强；参数已经很小时，它几乎不再干预。</p>
<p>岭回归仍是凸二次问题，并有闭式解（忽略/已中心化处理 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 的情况）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}^*=\left(\mathbf{X}^\top\mathbf{X}+N\lambda\mathbf{I}\right)^{-1}\mathbf{X}^\top\mathbf{y}\]</span>
<p>与普通最小二乘相比，这里多出来的 <span displaypfx="inline-" class="mathjax-container">\(N\lambda\mathbf{I}\)</span> 是沿对角线加入的一项稳定化修正。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{I}\)</span> 是单位矩阵，它不会改变特征之间的相对结构，但会让矩阵更容易求逆，因此在特征高度相关时更稳定。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{I}\in\mathbb{R}^{d\times d}\)</span>：单位矩阵（Identity Matrix）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}^\top\mathbf{X}+N\lambda\mathbf{I}\)</span>：对角线上加了 <span displaypfx="inline-" class="mathjax-container">\(N\lambda\)</span> 的稳定项，缓解共线性导致的病态（Ill-conditioning）。</li>
</ul>
<p>用一个最小可算的例子说明 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 如何改变解。考虑无偏置的一维回归 <span displaypfx="inline-" class="mathjax-container">\(\hat y=wx\)</span>，目标为：</p>
<span displaypfx="" class="mathjax-container">\[J(w)=\sum_{i=1}^{N}(y_i-wx_i)^2+\lambda w^2\]</span>
<p>这是单变量岭回归的简化形式。前一项把所有样本的平方误差加总，后一项 <span displaypfx="inline-" class="mathjax-container">\(\lambda w^2\)</span> 惩罚过大的斜率。它清楚展示了岭回归的核心：既要求拟合数据，又限制参数不要长得太大。</p>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 求导并令其为 0 得：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\mathrm{d}J}{\mathrm{d}w}=-2\sum_{i=1}^{N}x_i(y_i-wx_i)+2\lambda w=0\Rightarrow w^*=\frac{\sum_{i=1}^{N}x_i y_i}{\sum_{i=1}^{N}x_i^2+\lambda}\]</span>
<p>这个结果说明岭回归解和普通最小二乘解非常接近，只是分母里多了一个 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span>。它会把估计值往 0 方向收缩： <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越大，分母越大，得到的 <span displaypfx="inline-" class="mathjax-container">\(w^*\)</span> 就越保守。</p>
<p>可见 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 直接进入分母，把 <span displaypfx="inline-" class="mathjax-container">\(w^*\)</span> 持续拉向 0。岭回归不会像 Lasso 那样大量制造精确 0，而是把所有权重更平滑地压小，因此更像“把所有旋钮都往小一点拧”，让模型整体更保守、更稳健。</p>
<ul>
<li>适合特征高度相关、希望保留全部特征但降低方差的场景；常见于经济学、医学和一般表格数据。</li>
<li>当既希望稀疏，又希望在强相关特征间保持稳定时，Elastic Net（<span displaypfx="inline-" class="mathjax-container">\(L_1+L_2\)</span>）通常比纯 Lasso 更稳。</li>
</ul>
<div class="blog_h3"><span class="graybg">Elastic Net（L1 + L2 正则化）</span></div>
<p>Elastic Net 把 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 惩罚合并到同一个目标中：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ J_{\text{EN}}(\mathbf{w},b)=\frac{1}{N}\|\mathbf{y}-\mathbf{X}\mathbf{w}-b\mathbf{1}\|_2^2+\lambda_1\|\mathbf{w}\|_1+\lambda_2\|\mathbf{w}\|_2^2\]</span>
<p>它同时保留两类效应： <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 项负责产生稀疏性（Sparsity），把一部分弱特征压到 0； <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 项负责平滑收缩，在特征高度相关时提高解的稳定性。因此 Elastic Net 通常用于“既希望自动做特征选择，又不希望在强相关特征之间选得过于激进”的场景。</p>
<ul>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(\lambda_2=0\)</span> 时，退化为 Lasso。</li>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(\lambda_1=0\)</span> 时，退化为岭回归（Ridge Regression）。</li>
<li>当特征高度相关且维度很高时，Elastic Net 往往比纯 Lasso 更稳，也比纯岭回归更稀疏。</li>
</ul>
<div class="blog_h3"><span class="graybg">逻辑回归</span></div>
<p>逻辑回归（Logistic Regression）是二分类的标准基线：它先用线性函数产生打分，再把该打分通过 sigmoid（Logistic Function）映射到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>，从而得到条件概率模型。</p>
<div class="blog_h4"><span class="graybg">模型、概率与 logit</span></div>
<p>对二分类，令标签随机变量（Random Variable）<span displaypfx="inline-" class="mathjax-container">\(Y\in\{0,1\}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(1\)</span> 表示正类， <span displaypfx="inline-" class="mathjax-container">\(0\)</span> 表示负类。给定输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 后，先定义线性打分：</p>
<span displaypfx="" class="mathjax-container">\[z=\mathbf{w}^\top\mathbf{x}+b\]</span>
<p>再定义 sigmoid 函数：</p>
<span displaypfx="" class="mathjax-container">\[\sigma(z)=\frac{1}{1+e^{-z}}\]</span>
<p>于是，在给定输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的条件下，标签取正类的条件概率写成：</p>
<span displaypfx="" class="mathjax-container">\[p(Y=1\mid \mathbf{x})=\sigma(z)=\sigma(\mathbf{w}^\top\mathbf{x}+b)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mid\)</span> 表示“在给定……条件下”；左侧 <span displaypfx="inline-" class="mathjax-container">\(Y=1\)</span> 表示事件“标签随机变量取值为正类 1”。因此这个式子表示：在输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 已知时，事件 <span displaypfx="inline-" class="mathjax-container">\(Y=1\)</span> 发生的概率。</p>
<p>相应地，负类概率为：</p>
<span displaypfx="" class="mathjax-container">\[p(Y=0\mid \mathbf{x})=1-\sigma(z)\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\in\mathbb{R}^d\)</span>：特征向量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\in\mathbb{R}^d\)</span>：权重向量（Weight Vector）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(b\in\mathbb{R}\)</span>：偏置（Bias）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}\)</span>：线性打分；它也是 logit（对数几率，log-odds）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sigma(\cdot)\)</span>：把任意实数映射到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 的非线性函数。</li>
</ul>
<p>logit 的含义来自恒等式：</p>
<span displaypfx="" class="mathjax-container">\[\log\frac{p(Y=1\mid \mathbf{x})}{1-p(Y=1\mid \mathbf{x})}=z=\mathbf{w}^\top\mathbf{x}+b\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(\frac{p(Y=1\mid \mathbf{x})}{1-p(Y=1\mid \mathbf{x})}\)</span> 称为几率（Odds），表示“正类概率与负类概率之比”；再对它取对数，就得到 logit（对数几率，log-odds）：<span displaypfx="inline-" class="mathjax-container">\(\log\frac{p}{1-p}\)</span>。因此上式的含义不是再引入一个无关的新量，而是说明：逻辑回归把<span style="background-color: #c0c0c0;">正类概率的对数几率</span>建模为输入特征的线性函数。</p>
<p>换言之，在二分类逻辑回归里， <span displaypfx="inline-" class="mathjax-container">\(z=\mathbf{w}^\top\mathbf{x}+b\)</span> 就是线性部分的原始输出值，而这个原始输出值恰好等于 logit。它本身不是概率，可以取任意实数；经过 sigmoid 之后才变成 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 内的概率。多分类情形中，softmax 之前那一组线性输出通常统称为 logits。</p>
<p>因此 <span displaypfx="inline-" class="mathjax-container">\(w_j\)</span> 可以被解释为：特征 <span displaypfx="inline-" class="mathjax-container">\(x_j\)</span> 增加一个单位，会把对数几率增加 <span displaypfx="inline-" class="mathjax-container">\(w_j\)</span>（在其他特征不变时）。这就是逻辑回归的核心可解释性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/logistic.jpg"><img class="alignnone size-full wp-image-41171" src="https://blog.gmem.cc/wp-content/uploads/2026/03/logistic.jpg" alt="logistic" width="1216" height="874" /></a></p>
<div class="blog_h4"><span class="graybg">训练目标（NLL / Cross-Entropy）</span></div>
<p>给定训练集 <span displaypfx="inline-" class="mathjax-container">\(\{(\mathbf{x}_i,y_i)\}_{i=1}^{N}\)</span>，记 <span displaypfx="inline-" class="mathjax-container">\(p_i=\sigma(\mathbf{w}^\top\mathbf{x}_i+b)\)</span>。逻辑回归通过最大化似然（Likelihood）训练；等价地，它最小化负对数似然（Negative Log-Likelihood, NLL），也即二分类交叉熵（Binary Cross-Entropy）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ L(\mathbf{w},b)=-\sum_{i=1}^{N}\left(y_i\log p_i+(1-y_i)\log(1-p_i)\right)\]</span>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\log p_i\)</span>：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=1\)</span> 时，鼓励 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 变大。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\log(1-p_i)\)</span>：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=0\)</span> 时，鼓励 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 变小。</li>
</ul>
<p>加入 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则化时，常见形式为 <span displaypfx="inline-" class="mathjax-container">\(L(\mathbf{w},b)+\lambda\|\mathbf{w}\|_2^2\)</span>（通常不惩罚 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>）。该目标是凸的，因此不存在“坏局部最优”的训练不稳定问题。</p>
<div class="blog_h4"><span class="graybg">梯度：公式如何驱动参数更新</span></div>
<p>把样本堆叠成矩阵 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}\)</span>、标签向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span>，预测概率向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{p}\)</span>（第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量为 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span>）。则无正则项时梯度为：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_{\mathbf{w}}L=\mathbf{X}^\top(\mathbf{p}-\mathbf{y}),\quad \frac{\partial L}{\partial b}=\mathbf{1}^\top(\mathbf{p}-\mathbf{y})\]</span>
<p>这两个梯度式子都围绕同一个误差向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{p}-\mathbf{y}\)</span> 展开：它表示“模型给出的正类概率”和“真实标签”之间的偏差。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{X}^\top(\mathbf{p}-\mathbf{y})\)</span> 表示把这种偏差按特征方向汇总起来，从而告诉每个权重该往哪个方向改； <span displaypfx="inline-" class="mathjax-container">\(\mathbf{1}^\top(\mathbf{p}-\mathbf{y})\)</span> 则把所有偏差直接相加，用来更新偏置。</p>
<p>这两个式子揭示了训练机制：如果某个样本真实标签是 1 但模型给出小概率（<span displaypfx="inline-" class="mathjax-container">\(p_i-y_i&lt;0\)</span>），则梯度会推动 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 朝着增加该样本 logit 的方向更新；反之亦然。</p>
<div class="blog_h4"><span class="graybg">实例：单样本算概率、算损失、算一次梯度</span></div>
<p>考虑一维特征 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>，参数 <span displaypfx="inline-" class="mathjax-container">\(w=1,b=-1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(z=wx+b=1\)</span>，概率：</p>
<span displaypfx="" class="mathjax-container">\[p=\sigma(1)=\frac{1}{1+e^{-1}}\approx 0.731\]</span>
<p>若真实标签 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span>，该样本的负对数似然为：</p>
<span displaypfx="" class="mathjax-container">\[\ell=-\log p\approx 0.313\]</span>
<p>该样本对 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 的梯度为（单样本形式）：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \ell}{\partial w}=(p-y)x=(0.731-1)\cdot 2\approx -0.538\]</span>
<p>用学习率 <span displaypfx="inline-" class="mathjax-container">\(\eta\)</span> 做一次梯度下降： <span displaypfx="inline-" class="mathjax-container">\(w\leftarrow w-\eta\frac{\partial\ell}{\partial w}\)</span>。由于梯度为负，更新会把 <span displaypfx="inline-" class="mathjax-container">\(w\)</span> 增大，从而增大 <span displaypfx="inline-" class="mathjax-container">\(z\)</span>、提升 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，使模型更倾向把该样本判为正类。</p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>二分类且需要概率输出/可校准阈值（例如欺诈检测、流失预测、医学风险评分）。</li>
<li>高维稀疏特征（如 one-hot、文本 bag-of-words），逻辑回归常是强基线。</li>
<li>对可解释性、训练稳定性要求高的工程场景（可用权重做审计/特征诊断）。</li>
<li>当决策边界高度非线性且特征工程不足时，需要树模型或神经网络补上非线性。</li>
</ul>
<div class="blog_h3"><span class="graybg">支持向量机（SVM）</span></div>
<p>支持向量机（Support Vector Machine, SVM）是一类以<span style="background-color: #c0c0c0;">最大间隔分类</span>为核心原则的判别模型。对线性可分的二分类问题，它要寻找一个超平面（Hyperplane），使两类样本被正确分开，同时边界到两侧样本的最小距离尽可能大。这个最小距离对应的缓冲区称为间隔（Margin）。</p>
<p>也正因为优化目标是最大间隔，SVM 并不是“随便找一条能分开两类样本的直线/超平面”，而是要找那条对两类样本都留出最大安全缓冲区的边界。边界离两类样本都越远，模型对噪声、标注扰动与局部数据波动通常越稳。</p>
<p>从数学形式看，SVM 最终会落到二次规划（Quadratic Programming, QP）。所谓二次规划，就是：<span style="background-color: #c0c0c0;">目标函数是变量的二次函数，约束是线性等式或线性不等式</span>。SVM 的目标是最小化 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\mathbf{w}\|_2^2\)</span>，约束则是每个样本都必须被放到正确一侧并留出至少 1 的函数间隔，因此它正好属于这一类优化问题。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/svm.jpg"><img class="alignnone size-full wp-image-41179" src="https://blog.gmem.cc/wp-content/uploads/2026/03/svm.jpg" alt="svm" width="1408" height="768" /></a></p>
<p>从结构上看，SVM 把三件事连在一起：</p>
<ol>
<li>几何：先定义“什么叫分得开”，以及“什么叫分得最稳”。</li>
<li>优化：把“最稳的边界”写成一个可求解的凸二次规划。</li>
<li>对偶：把问题改写成只依赖样本内积的形式，从而自然导出核技巧（Kernel Trick）。</li>
</ol>
<div class="blog_h4"><span class="graybg">从几何直觉到“最大间隔”</span></div>
<p>对二分类任务，设标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{-1,+1\}\)</span>。线性分类器先计算一个打分函数（Score Function）：</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathbf{w}^\top\mathbf{x}+b\]</span>
<p>这里最好先把“函数写法”和“几何边界写法”区分清楚。像 <span displaypfx="inline-" class="mathjax-container">\(y=x\)</span> 这样的形式，强调的是“给定自变量（Independent Variable） <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，应变量（Dependent Variable） <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 如何变化”；但对分类器来说，更关键的问题不是“谁依赖谁”，而是<span style="background-color: #c0c0c0;">哪些点恰好落在边界上，以及点位于边界哪一侧</span>。因此同一条直线在几何里通常改写成隐式形式（Implicit Form） <span displaypfx="inline-" class="mathjax-container">\(x-y=0\)</span>。</p>
<p>一旦写成 <span displaypfx="inline-" class="mathjax-container">\(x-y=0\)</span>，就能直接看成二维超平面方程 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span>：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x,y)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=(1,-1)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(b=0\)</span>，便有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=x-y\)</span>。此时直线方向向量（Direction Vector）可以取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}=(1,1)^\top\)</span>，因为沿这条线移动时 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 同时增加；而 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{v}=1\cdot 1+(-1)\cdot 1=0\)</span>，说明 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 与直线切向方向正交，所以它正是这条直线的法向量（Normal Vector）。更一般地，对任意边界 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span>，系数向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 都垂直于边界，因此天然决定“正侧、负侧”和距离的度量方向。</p>
<p>再用它的符号做判别：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\mathrm{sign}(f(\mathbf{x}))=\mathrm{sign}(\mathbf{w}^\top\mathbf{x}+b)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 是超平面的法向量（Normal Vector）。沿 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 方向移动， <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})\)</span> 会增大；沿反方向移动， <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})\)</span> 会减小。因此：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})=0\)</span>：点就在分类超平面上。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})&gt;0\)</span>：点落在法向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 指向的那一侧。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})&lt;0\)</span>：点落在另一侧。</li>
</ul>
<p>超平面把空间分成正半空间（Positive Half-space）与负半空间（Negative Half-space）；分数的正负号直接给出样本位于哪一侧。</p>
<p>令 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}=\{\mathbf{x}:\mathbf{w}^\top\mathbf{x}+b=0\}\)</span> 表示超平面，令单位法向量（Unit Normal Vector）为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{n}=\mathbf{w}/\|\mathbf{w}\|_2\)</span>。对样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\)</span>，记 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_{\Pi,i}\)</span> 为它在超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span> 上的正交投影点（Orthogonal Projection），因此 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_{\Pi,i}+b=0\)</span>，且 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i-\mathbf{x}_{\Pi,i}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 平行。点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\)</span> 到超平面的带符号距离（Signed Distance）定义为该位移在单位法向量方向上的投影长度：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}_{\pm}(\mathbf{x}_i,\mathcal{H})=\mathbf{n}^\top(\mathbf{x}_i-\mathbf{x}_{\Pi,i})=\frac{\mathbf{w}^\top\mathbf{x}_i+b}{\|\mathbf{w}\|_2}\]</span>
<p>分子 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b\)</span> 衡量点在法向量方向上偏离超平面的代数量；分母 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2\)</span> 对法向量做归一化，去掉“同一个超平面可写成不同倍数方程”的尺度影响；符号保留样本位于超平面哪一侧的信息。</p>
<p>带符号距离区分了超平面的两侧，但正类样本与负类样本的正确侧相反。将标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{-1,+1\}\)</span> 乘入后，得到几何间隔（Geometric Margin）：</p>
<span displaypfx="" class="mathjax-container">\[\gamma_i=\frac{y_i(\mathbf{w}^\top\mathbf{x}_i+b)}{\|\mathbf{w}\|_2}\]</span>
<p>几何间隔是<span style="background-color: #c0c0c0;">用标签修正后的带符号距离</span>。因此：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\gamma_i&gt;0\)</span>：样本在正确一侧，被正确分类。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\gamma_i=0\)</span>：样本正好压在边界上。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\gamma_i&lt;0\)</span>：样本落到错误一侧，被误分类。</li>
</ul>
<p>SVM 的目标是让所有训练样本中最小的那个 <span displaypfx="inline-" class="mathjax-container">\(\gamma_i\)</span> 尽可能大，即最大化“最坏样本到边界的正确方向距离”：</p>
<span displaypfx="" class="mathjax-container">\[\gamma=\min_i \gamma_i\]</span>
<p>这就是最大间隔（Maximum Margin）的含义：不是让“平均样本”离边界远，而是让最危险、最靠近边界的样本也尽量安全。</p>
<div class="blog_h4"><span class="graybg">硬间隔 SVM（Hard-margin SVM）原始问题</span></div>
<p>先看线性可分（Linearly Separable）的情形。所谓线性可分，就是存在某个 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{w},b)\)</span>，使每个样本都在与自己标签一致的一侧。这件事可以统一写成：</p>
<span displaypfx="" class="mathjax-container">\[y_i(\mathbf{w}^\top\mathbf{x}_i+b)&gt;0,\quad \forall i\]</span>
<p>为什么这里是 <span displaypfx="inline-" class="mathjax-container">\(&gt;0\)</span>？因为它等价于“符号一致”：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=+1\)</span> 时，要求 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b&gt;0\)</span>，即正类样本必须落在正半空间；当 <span displaypfx="inline-" class="mathjax-container">\(y_i=-1\)</span> 时，要求 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b&lt;0\)</span>，即负类样本必须落在负半空间。若只等于 0，则样本恰好压在分类边界上，不属于严格可分，因为此时它没有任何安全间隔，符号判别也处在临界点。</p>
<p>不过， <span displaypfx="inline-" class="mathjax-container">\(y_i(\mathbf{w}^\top\mathbf{x}_i+b)\)</span> 还不是几何距离，因为把 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{w},b)\)</span> 同时乘以任意正常数 <span displaypfx="inline-" class="mathjax-container">\(t&gt;0\)</span>，超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span> 完全不变，但这个量会整体乘上 <span displaypfx="inline-" class="mathjax-container">\(t\)</span>。因此它只是一个未归一化的间隔量，通常称为函数间隔（Functional Margin）。</p>
<p>为了消掉这个缩放自由度，SVM 采用一个标准定标：强制所有样本的最小函数间隔等于 1，也就是要求</p>
<span displaypfx="" class="mathjax-container">\[y_i(\mathbf{w}^\top\mathbf{x}_i+b)\ge 1,\quad \forall i\]</span>
<p>这条约束把两类样本同时编码进来：当 <span displaypfx="inline-" class="mathjax-container">\(y_i=+1\)</span> 时，它变成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b\ge 1\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(y_i=-1\)</span> 时，它变成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}_i+b\le -1\)</span>。于是分类边界两侧又出现两条平行的“间隔边界”（Margin Boundaries）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}^\top\mathbf{x}+b=+1,\quad \mathbf{w}^\top\mathbf{x}+b=-1\]</span>
<p>因为平行超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=c_1\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=c_2\)</span> 之间的距离是 <span displaypfx="inline-" class="mathjax-container">\(|c_1-c_2|/\|\mathbf{w}\|_2\)</span>，所以这两条间隔边界之间的宽度为</p>
<span displaypfx="" class="mathjax-container">\[\frac{|(+1)-(-1)|}{\|\mathbf{w}\|_2}=\frac{2}{\|\mathbf{w}\|_2}\]</span>
<p>在这个定标下，离分类边界最近的样本几何间隔恰好是</p>
<span displaypfx="" class="mathjax-container">\[\gamma=\min_i \frac{y_i(\mathbf{w}^\top\mathbf{x}_i+b)}{\|\mathbf{w}\|_2}=\frac{1}{\|\mathbf{w}\|_2}\]</span>
<p>因此，最大化几何间隔就等价于最小化 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|_2\)</span>；为了得到标准的凸二次目标，通常写成：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2\quad \text{s.t.}\quad y_i(\mathbf{w}^\top \mathbf{x}_i+b)\ge 1\]</span>
<p>这就是硬间隔 SVM 的原始问题（Primal Problem）。现在“二次规划”这个术语也具体了：目标函数 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\mathbf{w}\|_2^2\)</span> 是关于参数的凸二次函数，而约束 <span displaypfx="inline-" class="mathjax-container">\(y_i(\mathbf{w}^\top \mathbf{x}_i+b)\ge 1\)</span> 对 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{w},b)\)</span> 是线性的，所以这是一个凸二次规划，并且可以求到全局最优解。</p>
<div class="blog_h4"><span class="graybg">拉格朗日函数与 KKT：为什么只剩少数“支持向量”</span></div>
<p>这一段只回答一个问题：训练集中明明有很多样本，为什么最后真正决定分类边界的，往往只有少数几个点。这个结论的严格来源是 KKT 条件，但它的直观含义并不抽象：<span style="background-color: #c0c0c0;">只有那些真正把最大间隔边界“卡住”的样本，才会在最优解里留下非零权重</span>。</p>
<p>硬间隔 SVM 的原始问题是：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\boldsymbol{w},b}\frac{1}{2}\|\boldsymbol{w}\|_2^2\quad \text{s.t.}\quad y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)\ge 1,\ \forall i\]</span>
<p>这里目标函数 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\boldsymbol{w}\|_2^2\)</span> 想把边界做得尽量“简单”，也就是让法向量尽量短，从而把间隔做大；而每个训练样本都在提出自己的硬约束：它不仅要被分对，还必须距离边界至少有 1 个单位的函数间隔。把这两股力量写到同一个式子里，就得到拉格朗日函数（Lagrangian）：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(\boldsymbol{w},b,\boldsymbol{\alpha})=\frac{1}{2}\|\boldsymbol{w}\|_2^2-\sum_{i=1}^{N}\alpha_i\big(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1\big),\quad \alpha_i\ge 0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本对应的拉格朗日乘子（Lagrange Multiplier）。在 SVM 里，可以把它直接理解为“第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本对当前分类边界施加了多大压力”。压力越大，说明这个样本越在真正影响边界的位置；压力为 0，说明这个样本虽然在训练集里，但最优边界并不需要它来支撑。</p>
<p>这个问题的读法可以想成一个“边界往外推、样本往回顶”的平衡过程：</p>
<ul>
<li><span style="background-color: #c0c0c0;">模型一侧</span> 想让 <span displaypfx="inline-" class="mathjax-container">\(\|\boldsymbol{w}\|_2\)</span> 尽量小，从而把间隔尽量做大。</li>
<li><span style="background-color: #c0c0c0;">约束一侧</span> 则由每个样本通过 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 施加压力：谁越接近边界、越可能破坏间隔，谁就越值得保留权重。</li>
</ul>
<p>于是，离边界很远的样本会发生什么，就变得非常直观。若某个样本满足</p>
<span displaypfx="" class="mathjax-container">\[y_i(\boldsymbol{w}^\top\boldsymbol{x}_i+b)&gt;1\]</span>
<p>说明它不仅分对了，而且还有额外安全余量。这个点对“边界能否继续外推”没有形成真正阻碍，因此最优时它对应的 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 会被压到 0。相反，若某个样本恰好满足</p>
<span displaypfx="" class="mathjax-container">\[y_i(\boldsymbol{w}^\top\boldsymbol{x}_i+b)=1\]</span>
<p>它就正贴在间隔边界上，是“再往外推一点就会出问题”的临界点。这样的样本才有资格在最优解中保留非零权重。</p>
<p>KKT 条件（Karush–Kuhn–Tucker Conditions）把这个直觉写成严格公式。第一条来自驻点条件（Stationarity）：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_{\boldsymbol{w}}\mathcal{L}=0\Rightarrow \boldsymbol{w}=\sum_{i=1}^{N}\alpha_i y_i \boldsymbol{x}_i\]</span>
<p>这条式子的含义非常重要。它说明最终的法向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 并不是由全部样本平均决定的，而是由训练样本的加权和决定的。这里：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个训练样本；</li>
<li><span displaypfx="inline-" class="mathjax-container">\(y_i\alpha_i\)</span> 是它的“带符号权重”；</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i=0\)</span>，该样本就完全不会出现在 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 的表达式里。</li>
</ul>
<p>第二条关键条件是互补松弛（Complementary Slackness）：</p>
<span displaypfx="" class="mathjax-container">\[\alpha_i\big(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1\big)=0\]</span>
<p>这条式子可以直接逐项阅读：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本对边界施加的压力；</li>
<li><span displaypfx="inline-" class="mathjax-container">\(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1\)</span> 是该样本相对于间隔边界的“松弛量”；当它大于 0 时，说明样本在安全区里；当它等于 0 时，说明样本正好贴边。</li>
</ul>
<p>由于这两个量的乘积必须等于 0，所以只可能出现两种情况：</p>
<ul>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)-1&gt;0\)</span>，说明该样本有安全余量，那么必须有 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i=0\)</span>。</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i&gt;0\)</span>，说明该样本仍在对边界施压，那么必须有 <span displaypfx="inline-" class="mathjax-container">\(y_i(\boldsymbol{w}^\top \boldsymbol{x}_i+b)=1\)</span>。</li>
</ul>
<p>这就是支持向量（Support Vector）的严格定义来源：<span style="background-color: #c0c0c0;">在硬间隔 SVM 中，只有恰好贴在间隔边界上的样本，才可能对应非零 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>；这些样本共同决定最终超平面的位置</span>。其余样本虽然被正确分类，但因为离边界还有余量，所以在最优解里不再起作用。</p>
<p>如果继续沿着这个结论往下看，就会发现 SVM 的稀疏性完全不是偶然现象，而是 KKT 的直接产物。训练集里可以有大量“安全样本”，但最优边界只需要被少数临界样本支撑起来。名字“支持向量”说的正是这件事：这些点不是普通数据点，而是真正在几何上把边界撑住的点。</p>
<div class="blog_h4"><span class="graybg">对偶问题（Dual）：把“求边界”改写成“给样本分权重”</span></div>
<p>前面已经看到：KKT 让 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 变成了“谁在真正顶住边界”的刻度。但如果这里只停在 KKT，还是会留下一个疑问：<span style="background-color: #c0c0c0;">对偶问题到底是怎么从原始问题里长出来的</span>？关键动作只有一步：先把 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 固定住，把拉格朗日函数当成关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 的函数来最小化；然后再回过头，只对 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 做最大化。</p>
<p>也就是说，原始问题里我们直接求“哪条边界最好”：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2\quad \text{s.t.}\quad y_i(\mathbf{w}^\top\mathbf{x}_i+b)\ge 1\]</span>
<p>而引入拉格朗日乘子之后，可以先看下面这个函数：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(\mathbf{w},b,\alpha)=\frac{1}{2}\|\mathbf{w}\|_2^2-\sum_{i=1}^{N}\alpha_i\Big(y_i(\mathbf{w}^\top \mathbf{x}_i+b)-1\Big),\quad \alpha_i\ge 0\]</span>
<p>对固定的 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span>，它就是一个关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 的凸函数。于是我们先做内层最小化：</p>
<span displaypfx="" class="mathjax-container">\[g(\alpha)=\min_{\mathbf{w},b}\ \mathcal{L}(\mathbf{w},b,\alpha)\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(g(\alpha)\)</span> 就叫对偶函数（Dual Function）。它表示：<span style="background-color: #c0c0c0;">假设每个样本的施压强度已经给定，边界那一侧最好的回应会是什么</span>。</p>
<p>这一步的好处是， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 可以被显式消掉。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 求驻点条件，有：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial \mathbf{w}}=0\Rightarrow \mathbf{w}=\sum_{i=1}^{N}\alpha_i y_i\mathbf{x}_i\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial b}=0\Rightarrow \sum_{i=1}^{N}\alpha_i y_i=0\]</span>
<p>这两条式子非常关键：第一条说明法向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 完全由训练样本线性组合出来；第二条说明正负两类样本的“总施压”必须平衡。把它们代回去，原来依赖 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 的问题就变成只依赖 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 的问题：</p>
<span displaypfx="" class="mathjax-container">\[\max_{\alpha}\ \sum_{i=1}^{N}\alpha_i-\frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<span displaypfx="" class="mathjax-container">\[\text{s.t.}\quad \alpha_i\ge 0,\quad \sum_{i=1}^{N}\alpha_i y_i=0\]</span>
<p>这就是 SVM 的对偶问题（Dual Problem）。现在可以看出它为什么叫“对偶”：它没有再直接问“<span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 应该是多少”，而是改问“每个样本应该分到多大的权重 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>，才能共同把最优边界顶出来”。</p>
<p>这样改写有两个直接收益。第一，支持向量为什么稀疏会变得一眼可见：若某个样本最终 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*=0\)</span>，它就自动从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=\sum_i \alpha_i y_i\mathbf{x}_i\)</span> 里消失。第二，对偶目标里出现的样本方式只剩内积 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\mathbf{x}_j\)</span>，这正是后面引入核函数（Kernel）的入口。</p>
<p>把最优权重 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*\)</span> 求出来后，分类器可以直接写成：</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathrm{sign}\left(\sum_{i=1}^{N}\alpha_i^* y_i\,\mathbf{x}_i^\top \mathbf{x}+b^*\right)\]</span>
<p>这个式子的含义很具体：新样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 会与训练样本做内积（Inner Product），也就是计算相似度；每个训练样本按自己的类别符号 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 和影响系数 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*\)</span> 投一票；最后把这些票加总，再加上偏置 <span displaypfx="inline-" class="mathjax-container">\(b^*\)</span>，看结果落在哪一侧。</p>
<p>由于绝大多数样本的 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i^*=0\)</span>，真正参与这次投票的通常只有支持向量。因此 SVM 的预测阶段常带有一个很强的稀疏性（Sparsity）：<span style="background-color: #c0c0c0;">不是所有训练样本都在持续发声，真正起作用的只是边界附近那一小部分点</span>。</p>
<p>偏置 <span displaypfx="inline-" class="mathjax-container">\(b^*\)</span> 可以用任一支持向量恢复。若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_k\)</span> 是一个支持向量，则它满足 <span displaypfx="inline-" class="mathjax-container">\(y_k(\mathbf{w}^{*\top}\mathbf{x}_k+b^*)=1\)</span>，因此</p>
<span displaypfx="" class="mathjax-container">\[b^*=y_k-\mathbf{w}^{*\top}\mathbf{x}_k\]</span>
<div class="blog_h4"><span class="graybg">核函数（Kernel）：把“线性边界”搬到更合适的空间</span></div>
<p>对偶问题真正重要的地方，不只是“把变量从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 换成了 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>”，而是它把 SVM 的全部数据依赖压缩成了样本之间的内积（Inner Product）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<p>这一步非常关键，因为它说明：<span style="background-color: #c0c0c0;">SVM 在对偶形式里并不需要直接看到样本坐标本身，它只需要知道样本彼此有多相似</span>。只要这种“相似度”还能写成某个空间里的内积，SVM 的训练与预测公式就都可以照搬。</p>
<p>于是核技巧（Kernel Trick）的引入就变得自然了。设有一个特征映射（Feature Map）<span displaypfx="inline-" class="mathjax-container">\(\phi(\mathbf{x})\)</span>，它把原始样本送到更高维、甚至无限维的特征空间（Feature Space）。如果我们真的显式去算这个高维向量，代价往往很大；但对偶形式只关心内积，因此只要能直接计算</p>
<span displaypfx="" class="mathjax-container">\[K(\mathbf{x}_i,\mathbf{x}_j)=\phi(\mathbf{x}_i)^\top\phi(\mathbf{x}_j)\]</span>
<p>就等价于“隐式地”在特征空间里做线性 SVM，而不必真的把 <span displaypfx="inline-" class="mathjax-container">\(\phi(\mathbf{x})\)</span> 写出来。这个直接在原空间里计算特征空间内积的技巧，就叫核函数（Kernel Function）或核技巧（Kernel Trick）。</p>
<p>这也解释了为什么 kernel 是从 dual 里长出来的，而不是额外拼上去的：若你还停留在原始问题里，眼前看到的仍是显式参数 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span>；而一旦进入对偶形式，表达式里只剩 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\mathbf{x}_j\)</span>，把它替换成别的“合法相似度”就成了最自然的一步。</p>
<p>于是决策函数从</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathrm{sign}\left(\sum_{i=1}^{N}\alpha_i^* y_i\,\mathbf{x}_i^\top \mathbf{x}+b^*\right)\]</span>
<p>变成</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=\mathrm{sign}\left(\sum_{i=1}^{N}\alpha_i^* y_i\,K(\mathbf{x}_i,\mathbf{x})+b^*\right)\]</span>
<p>要点是：<span style="background-color: #c0c0c0;">特征空间里仍然是线性超平面；只是映回原空间后，边界看起来变成了弯的</span>。因此 kernel 不是“把线性 SVM 换成非线性模型”，而是“先把数据换到更容易线性可分的表示里，再继续做线性 SVM”。</p>
<p>从直觉上看，核函数本质上是在重新定义“两个样本像不像”。线性核（Linear Kernel）比较原始方向是否一致；多项式核（Polynomial Kernel）强调特征之间的组合关系；RBF / Gaussian 核更强调局部邻近性，因此很容易形成局部、弯曲的决策边界。</p>
<ul>
<li>线性核（Linear Kernel）：<span displaypfx="inline-" class="mathjax-container">\(K(\mathbf{x},\mathbf{z})=\mathbf{x}^\top\mathbf{z}\)</span>。</li>
<li>多项式核（Polynomial Kernel）：<span displaypfx="inline-" class="mathjax-container">\(K(\mathbf{x},\mathbf{z})=(\mathbf{x}^\top\mathbf{z}+c)^d\)</span>。</li>
<li>RBF / Gaussian 核：<span displaypfx="inline-" class="mathjax-container">\(K(\mathbf{x},\mathbf{z})=\exp(-\gamma\|\mathbf{x}-\mathbf{z}\|_2^2)\)</span>。</li>
</ul>
<p>一个典型例子是“同心圆”二分类：在二维平面里，内圈和外圈无法用一条直线分开；但如果映射到包含半径平方等特征的空间，类别就可能被一个超平面分开。核方法的价值就在这里：<span style="background-color: #c0c0c0;">原空间里看到的是弯曲边界，特征空间里做的仍然是线性分类</span>。</p>
<div class="blog_h4"><span class="graybg">软间隔 SVM（Soft-margin SVM）：允许少量违约，但要付代价</span></div>
<p>现实数据通常含噪声、离群点（Outlier）或类别重叠。若仍然要求“所有点都必须在间隔之外”，硬间隔 SVM 很可能根本无解，或者被少数异常点强行拉歪。软间隔 SVM（Soft-margin SVM）因此引入松弛变量（Slack Variable）<span displaypfx="inline-" class="mathjax-container">\(\xi_i\ge 0\)</span>，允许个别样本违反间隔约束：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b,\xi}\ \frac{1}{2}\|\mathbf{w}\|_2^2+C\sum_{i=1}^{N}\xi_i\quad \text{s.t.}\quad y_i(\mathbf{w}^\top \mathbf{x}_i+b)\ge 1-\xi_i,\ \xi_i\ge 0\]</span>
<p>这个式子只表达一件事：边界仍然希望尽量大，但违约要交罚款，罚款强度由 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 决定。对单个样本， <span displaypfx="inline-" class="mathjax-container">\(\xi_i\)</span> 的含义可以直接按大小来读：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\xi_i=0\)</span>：样本分类正确，且在间隔边界上或之外。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(0&lt;\xi_i\le 1\)</span>：样本仍在正确一侧，但已经挤进了间隔内部。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\xi_i&gt;1\)</span>：样本跨过了分类边界，已经被误分。</li>
</ul>
<p><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 控制的是“对违约有多敏感”：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 大：更重视把训练集分对，边界更硬，对噪声也更敏感。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 小：更重视大间隔和整体稳定性，允许少量训练误差。</li>
</ul>
<p>软间隔下，支持向量的范围也更宽：不仅贴着间隔边界的点重要，落在间隔内部甚至被误分的点也会直接影响最优解。对应到对偶变量，常见情形是 <span displaypfx="inline-" class="mathjax-container">\(0&lt;\alpha_i&lt;C\)</span> 的点贴在间隔上，而 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i=C\)</span> 的点往往是间隔内样本或误分类样本。</p>
<p>把松弛变量消去后，软间隔 SVM 还可以写成更常见的合页损失（Hinge Loss）形式：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2+C\sum_{i=1}^{N}\max\big(0,\ 1-y_i(\mathbf{w}^\top \mathbf{x}_i+b)\big)\]</span>
<p>第一项限制模型复杂度，第二项惩罚分类违约。SVM 因此可以被看作“<span style="background-color: #c0c0c0;">大间隔 + 违约惩罚</span>”的组合，而支持向量则是这两股力量平衡后仍然留在最前线的样本。</p>
<div class="blog_h2"><span class="graybg">树模型与集成方法</span></div>
<p>这一类方法处理的核心问题是：当输入与输出之间存在明显非线性、阈值效应与高阶特征交互时，线性模型往往表达力不足，但工程上仍然希望模型具备较强可解释性、对表格数据友好、并且训练稳定。树模型通过递归切分（Recursive Partitioning）把输入空间划成若干局部区域；集成方法则进一步通过 Bagging 或 Boosting 提升泛化能力与精度。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模型</td>
<td style="text-align: center;">模型数量与训练逻辑</td>
<td style="text-align: center;">集成策略 / 学习机制</td>
<td style="text-align: center;">单棵树特点</td>
<td style="text-align: center;">主要在优化什么</td>
<td style="text-align: center;">过拟合风险</td>
<td style="text-align: center;">更适合什么场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策树</td>
<td>单棵独立模型，直接在原始数据上递归切分，没有集成过程</td>
<td>无；直接学习输入到输出的分段规则</td>
<td>树深完全由数据与约束参数决定；过深会记住噪声，过浅又会表达不足</td>
<td>没有显式“降偏差 / 降方差”分工，偏差和方差都要靠剪枝、树深和叶子约束一起控制</td>
<td>高；单树很容易把训练集中的偶然模式学进去</td>
<td>可解释性要求极高的简单任务、规则基线、集成模型的基学习器</td>
</tr>
<tr>
<td>随机森林</td>
<td>多棵树并行独立训练，树与树之间无依赖，可利用多核 CPU 并行</td>
<td>Bagging：bootstrap 采样 + 特征子采样 + 投票 / 平均</td>
<td>单棵树通常故意训练得较深，先把单树偏差压低，再依靠集成抵消高方差</td>
<td>主要降低方差，让整体模型更稳、更抗数据扰动</td>
<td>低；Bagging 和平均机制天然有正则化效果</td>
<td>中小规模表格数据、强 baseline、噪声较大且希望训练稳定的场景</td>
</tr>
<tr>
<td>GBDT / XGBoost</td>
<td>多棵树严格串行训练；后一棵树必须等前一棵树完成后，才能开始学习新的修正项</td>
<td>Boosting：拟合残差或负梯度，逐轮加权累加</td>
<td>单棵树通常较浅，只负责局部纠错；单树偏差高，但方差相对更低</td>
<td>主要降低偏差，通过接力纠错不断提升拟合能力</td>
<td>中等；若树数太多、学习率太大或树过深，容易持续把训练集吃满</td>
<td>结构化数据里追求高精度的任务，如风控、推荐、广告、竞赛</td>
</tr>
<tr>
<td>LightGBM</td>
<td>仍然属于串行 Boosting，但单棵树训练做了更激进的近似和工程优化</td>
<td>Boosting + 直方图分桶 + leaf-wise 生长 + GOSS / EFB</td>
<td>单棵树依然是浅到中等深度的纠错器，但 leaf-wise 会把最值得细分的局部继续挖深</td>
<td>继续降低偏差，同时极力优化训练速度与内存效率</td>
<td>中等偏高；若 leaf-wise 缺少足够约束，局部树会很快长深</td>
<td>大规模表格数据、高维稀疏特征、需要频繁重训的工业流水线</td>
</tr>
<tr>
<td>CatBoost</td>
<td>仍然属于串行 Boosting，但重点放在类别特征处理与训练偏移控制</td>
<td>Boosting + ordered target statistics + ordered boosting + symmetric tree</td>
<td>单棵树结构更规整，对称树推理快；模型内部原生吸收类别特征统计信息</td>
<td>继续降低偏差，同时尽量减少类别编码带来的泄露与偏移</td>
<td>中等；总体可控，但仍需学习率、树数和正则化配合</td>
<td>类别特征很多、希望少做手工编码、快速起一个强模型的表格任务</td>
</tr>
</tbody>
</table>
<p>这张表背后的逻辑可以压缩成一句话：<span style="background-color: #c0c0c0;">决策树是基础，随机森林主要靠并行平均降低方差，GBDT 家族主要靠串行纠错降低偏差，而 XGBoost、LightGBM、CatBoost 则是在 GBDT 主线上的工业级强化</span>。因此它们虽然都属于“树模型”，训练逻辑、误差控制方式和工程侧优势并不相同。</p>
<div class="blog_h3"><span class="graybg">决策树</span></div>
<p>决策树（Decision Tree）把预测过程写成一串逐层切分的规则：内部节点负责提问，边负责根据答案分流，叶节点负责输出最终结果。它的优势不在于公式复杂，而在于模型结构与业务规则天然同构：每一条从根到叶的路径，都对应一条可读的判断链。</p>
<div class="blog_h4"><span class="graybg">决策树、分类树、回归树的关系</span></div>
<p>决策树是总称；分类树（Classification Tree）与回归树（Regression Tree）是它在两类监督学习任务上的具体形式。三者共享同一种树结构，但目标变量、切分准则与叶子输出不同。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">概念</td>
<td style="text-align: center;">预测目标</td>
<td style="text-align: center;">叶子输出</td>
<td style="text-align: center;">常用切分准则</td>
</tr>
</thead>
<tbody>
<tr>
<td>决策树（Decision Tree）</td>
<td>树模型的总称</td>
<td>取决于具体任务</td>
<td>取决于具体任务</td>
</tr>
<tr>
<td>分类树（Classification Tree）</td>
<td>离散标签（如“流失/不流失”）</td>
<td>类别或类别概率</td>
<td>基尼不纯度（Gini）、熵（Entropy）、信息增益（Information Gain）</td>
</tr>
<tr>
<td>回归树（Regression Tree）</td>
<td>连续数值（如“价格”“时长”）</td>
<td>一个数值常数</td>
<td>平方误差（Squared Error）、MSE / SSE 下降</td>
</tr>
</tbody>
</table>
<p>因此不要把“决策树”和“分类树”当成并列概念。更准确的表述是：<span style="background-color: #c0c0c0;">分类树与回归树都是决策树；前者预测类别，后者预测数值</span>。</p>
<div class="blog_h4"><span class="graybg">结构与统一公式</span></div>
<p>设某个节点上落入的样本集合为 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，大小为 <span displaypfx="inline-" class="mathjax-container">\(|S|\)</span>。对某个候选切分（Split）<span displaypfx="inline-" class="mathjax-container">\(\phi\)</span>，例如“第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个特征 <span displaypfx="inline-" class="mathjax-container">\(x_j\le t\)</span>”，样本会被分成左右两个子节点：</p>
<span displaypfx="" class="mathjax-container">\[S_L(\phi)=\{(\mathbf{x}_i,y_i)\in S:\ x_{i,j}\le t\},\quad S_R(\phi)=S\setminus S_L(\phi)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(S_L\)</span> 是满足切分条件的样本集合，<span displaypfx="inline-" class="mathjax-container">\(S_R\)</span> 是剩余样本集合。树在每个节点都会尝试多个候选切分 <span displaypfx="inline-" class="mathjax-container">\(\phi\)</span>，并保留收益最大的那个。</p>
<p>为了避免分别记忆分类树与回归树的训练目标，可以先写成统一形式。设 <span displaypfx="inline-" class="mathjax-container">\(I(S)\)</span> 表示“节点 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 当前有多乱”或“在该节点上预测误差有多大”，则一次切分的收益可统一写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Gain}(S,\phi)=I(S)-\frac{|S_L(\phi)|}{|S|}I\!\left(S_L(\phi)\right)-\frac{|S_R(\phi)|}{|S|}I\!\left(S_R(\phi)\right)\]</span>
<p>这条式子的含义非常直接：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(I(S)\)</span>：切分前，这个节点有多混乱。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(I(S_L),I(S_R)\)</span>：切分后，左右子节点各自还有多混乱。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\frac{|S_L|}{|S|},\frac{|S_R|}{|S|}\)</span>：左右子节点占父节点样本的比例。要乘这个比例，是因为大子节点对总体误差的影响更大，小子节点不能和大子节点拥有同样权重。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Gain}(S,\phi)\)</span> 越大，说明这次切分越有效。</li>
</ul>
<p>分类树与回归树的差别，正体现在 <span displaypfx="inline-" class="mathjax-container">\(I(S)\)</span> 具体取什么。</p>
<div class="blog_h4"><span class="graybg">决策树：一个整体例子</span></div>
<p>在贷款审批（Loan Approval）场景中，决策树可以直接写成规则链。根节点先按负债收入比（Debt-to-Income Ratio）切分；若负债收入比过高，再看是否有逾期记录；若负债收入比正常，再看信用评分（Credit Score）与近 6 个月收入稳定性。最终某个叶节点可能对应“直接通过”，另一个叶节点对应“人工复核”，再另一个叶节点对应“拒绝”。这就是决策树最重要的工程价值：它不只是给出一个分数，还给出一条可追溯的判断路径。</p>
<div class="blog_h4"><span class="graybg">分类树：目标、公式与含义</span></div>
<p>分类树的目标是让每个叶节点里的标签尽量单一。若一个节点里几乎都是同一类样本，这个节点就“纯”；若各类样本混在一起，这个节点就“不纯”。</p>
<p>设类别集合为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{K}\)</span>，类别 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 在节点 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 中的占比记为：</p>
<span displaypfx="" class="mathjax-container">\[p_k(S)=\frac{\text{节点 }S\text{ 中标签为 }k\text{ 的样本数}}{|S|}\]</span>
<p>常用的不纯度（Impurity）有两种。</p>
<p>基尼不纯度（Gini Impurity）：</p>
<span displaypfx="" class="mathjax-container">\[G(S)=1-\sum_{k\in\mathcal{K}}p_k(S)^2\]</span>
<p>它可以读成“随机抽两个样本时，标签不一致的倾向有多强”。若节点里全是同一类，则某个 <span displaypfx="inline-" class="mathjax-container">\(p_k=1\)</span>、其余为 0，此时 <span displaypfx="inline-" class="mathjax-container">\(G(S)=0\)</span>，说明节点已经纯净；若二分类里两类各占一半，则 <span displaypfx="inline-" class="mathjax-container">\(G(S)=1-(0.5^2+0.5^2)=0.5\)</span>，说明混杂程度较高。</p>
<p>熵（Entropy）：</p>
<span displaypfx="" class="mathjax-container">\[H(S)=-\sum_{k\in\mathcal{K}}p_k(S)\log p_k(S)\]</span>
<p>熵衡量的是“不确定性”。若节点里全是同一类，则不需要再猜，熵为 0；若各类比例接近，说明不确定性高，熵也更大。</p>
<p>信息增益（Information Gain）就是“切分前的不确定性”减去“切分后的加权不确定性”：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{IG}(S,\phi)=H(S)-\frac{|S_L(\phi)|}{|S|}H\!\left(S_L(\phi)\right)-\frac{|S_R(\phi)|}{|S|}H\!\left(S_R(\phi)\right)\]</span>
<p>这条公式的每一部分都对应一个明确动作：</p>
<ul>
<li>第一项 <span displaypfx="inline-" class="mathjax-container">\(H(S)\)</span> 是切分前的混乱程度。</li>
<li>后两项是切分后左右子节点各自的混乱程度，并按样本占比加权求和。</li>
<li>两者相减，就是这次切分让节点“变纯”了多少。</li>
</ul>
<p>有些实现使用基尼下降而不是信息增益，本质上是同一件事：<span style="background-color: #c0c0c0;">选择那个能让子节点更纯、让标签更集中的切分</span>。</p>
<div class="blog_h4"><span class="graybg">分类树：实际例子——用户流失预警</span></div>
<p>设任务是预测“一个用户未来 30 天是否流失”。标签只有两类：流失 / 未流失，因此这是典型分类树问题。候选特征可以包括：最近 7 天登录次数、最近 30 天是否投诉、是否还有未使用优惠券、最近一次下单距今天数。</p>
<p>假设根节点先尝试切分“最近 7 天登录次数 &lt; 2”。这次切分后，左子节点里的用户大多已经很久不活跃，且流失比例显著升高；右子节点里的用户则活跃度更高、留存率更好。此时无论用基尼还是熵计算，左右节点的加权不纯度都会明显低于父节点，因此这会成为一个高质量切分。</p>
<p>继续往下，左子节点还可以再按“最近 30 天是否投诉”切分：低活跃且有投诉的用户，叶节点里可能出现“82% 最终流失”；低活跃但无投诉的用户，叶节点里可能是“61% 流失”。此时叶子不只给出类别，还可给出经验概率。工程上，这样的输出就能直接用于运营动作：高风险叶子推召回优惠券，中风险叶子推客服回访，低风险叶子不干预。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/descision-tree-1.jpg"><img class="alignnone size-full wp-image-41221" src="https://blog.gmem.cc/wp-content/uploads/2026/03/descision-tree-1.jpg" alt="descision-tree-1" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">回归树：目标、公式与含义</span></div>
<p>回归树处理的是连续数值目标，例如价格、时长、销量、能耗。它不追求“类别更纯”，而追求“同一个叶节点里的数值尽量接近”。</p>
<p>若某个叶节点 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 最终只输出一个常数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，那么在平方误差（Squared Error）下，最优输出不是中位数，而是均值：</p>
<span displaypfx="" class="mathjax-container">\[c^*(S)=\arg\min_c\sum_{(\mathbf{x}_i,y_i)\in S}(y_i-c)^2=\frac{1}{|S|}\sum_{(\mathbf{x}_i,y_i)\in S}y_i\]</span>
<p>之所以是均值，是因为平方误差会把所有偏差向两边拉平，而均值正是使平方偏差和最小的那个常数。</p>
<p>在这个叶节点上，最小平方误差和（Sum of Squared Errors, SSE）为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{SSE}(S)=\sum_{(\mathbf{x}_i,y_i)\in S}\left(y_i-c^*(S)\right)^2\]</span>
<p>它表示节点里所有样本值围绕叶子预测值 <span displaypfx="inline-" class="mathjax-container">\(c^*(S)\)</span> 的总波动。SSE 越大，说明这个节点里的样本值越分散，单用一个常数代表它们的效果越差。</p>
<p>因此一次切分的目标是让切分后的总误差尽量小，也可写成误差下降尽量大：</p>
<span displaypfx="" class="mathjax-container">\[\Delta(S,\phi)=\mathrm{SSE}(S)-\mathrm{SSE}\!\left(S_L(\phi)\right)-\mathrm{SSE}\!\left(S_R(\phi)\right)\]</span>
<p>若更喜欢看平均误差，也可以把它写成 MSE（Mean Squared Error）形式：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{MSE}(S)=\frac{1}{|S|}\mathrm{SSE}(S)\]</span>
<p>等价地，也可以最小化切分后的加权平均误差：<span displaypfx="" class="mathjax-container">\[\frac{|S_L|}{|S|}\mathrm{MSE}(S_L)+\frac{|S_R|}{|S|}\mathrm{MSE}(S_R)\]</span></p>
<p>两种写法完全等价：SSE 强调总误差，MSE 强调平均误差；本质都是寻找让子节点内部数值更集中的切分。</p>
<div class="blog_h4"><span class="graybg">回归树：实际例子——外卖配送时长预测</span></div>
<p>设任务是预测一笔订单从接单到送达需要多少分钟。这是连续数值目标，因此属于回归树。候选特征可以包括：配送距离、是否下雨、是否晚高峰、商家出餐速度、骑手当前手中订单数。</p>
<p>根节点可能先按“配送距离是否大于 3 公里”切分。因为近距离订单与远距离订单的时长分布差异很大，这一步通常能显著降低节点内部方差。对远距离子节点，再按“是否下雨”切分；下雨天路况更慢、波动更大。对近距离子节点，则可能按“是否处于午晚高峰”切分。</p>
<p>假设某个叶节点对应“距离 &gt; 3 公里、下雨、晚高峰”这类订单，这个叶节点里的训练样本平均送达时长是 47 分钟，那么该叶子的预测值就是 47。另一个叶节点若对应“距离 &lt; 2 公里、不下雨、非高峰”，其平均时长可能只有 18 分钟。回归树的预测逻辑不是拟合一条全局直线，而是把不同业务情境分段，再在每段内给出一个局部平均值。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/decision-tree-2.png"><img class="alignnone size-full wp-image-41227" src="https://blog.gmem.cc/wp-content/uploads/2026/03/decision-tree-2.png" alt="decision-tree-2" width="1469" height="1035" /></a></p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>需要把模型输出翻译成可审计规则，例如风控、审批、运营分层、客服分流。</li>
<li>数据以表格（Tabular）为主，且存在显著非线性、阈值效应或特征交互。</li>
<li>希望同时处理连续特征与离散特征，并保留较强可解释性。</li>
<li>注意：单棵树方差高、容易过拟合，通常需要限制最大深度、最小叶子样本数或配合集成方法。</li>
</ul>
<div class="blog_h3"><span class="graybg">随机森林</span></div>
<p>随机森林（Random Forest）是 Bagging（Bootstrap Aggregating）在树模型上的经典实现：用 bootstrap 采样生成多份训练子集，训练多棵通常偏差较低、但对训练数据扰动高度敏感（高方差）的决策树，再把它们的输出聚合，以显著降低整体方差并提升鲁棒性。</p>
<p>这里的“高方差（High Variance）”指模型的估计方差（Estimator Variance）或预测方差（Prediction Variance）：如果训练集稍有变化，单棵树学到的分裂结构与最终预测就可能明显变化。随机森林利用多棵树的平均/投票，把这种由数据扰动带来的波动相互抵消。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.jpg"><img class="alignnone size-full wp-image-41231" src="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.jpg" alt="random-forest" width="1287" height="816" /></a></p>
<div class="blog_h4"><span class="graybg">它和单棵树真正差在哪里</span></div>
<p>随机森林并不是“很多棵树简单叠起来”这么粗糙。单棵树的主要问题，是一旦在上层节点做出某个早期切分，后面的整条子树都会被这个局部选择锁定，因此对数据扰动极敏感。随机森林通过 bootstrap 采样与特征子采样，让每棵树在“数据视角”和“可用特征视角”上都略有不同，再把这些不同视角的判断平均起来，从而大幅削弱某一棵树早期错误切分的破坏力。</p>
<div class="blog_h4"><span class="graybg">算法与符号</span></div>
<p>给定训练集 <span displaypfx="inline-" class="mathjax-container">\(D=\{(\mathbf{x}_i,y_i)\}_{i=1}^{N}\)</span>。对 <span displaypfx="inline-" class="mathjax-container">\(m=1,\dots,M\)</span>：</p>
<ol>
<li>bootstrap 采样：从 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 有放回采样 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 次得到 <span displaypfx="inline-" class="mathjax-container">\(D_m\)</span>。所谓“有放回（Sampling with Replacement）”，是指每次抽到一个样本后，都先把它放回原数据集，再进行下一次抽样；因此同一个样本可能被重复抽中，而有些样本在这一轮里一次也没有被抽到。</li>
<li>训练一棵树 <span displaypfx="inline-" class="mathjax-container">\(T_m\)</span>：每个节点分裂时，只在随机选取的 <span displaypfx="inline-" class="mathjax-container">\(d'\)</span> 个特征上搜索最优切分（特征子采样，feature subsampling）。</li>
</ol>
<p>预测时，回归取平均，分类取多数投票：</p>
<span displaypfx="" class="mathjax-container">\[\hat y_{\text{reg}}(\mathbf{x})=\frac{1}{M}\sum_{m=1}^{M}T_m(\mathbf{x}),\quad \hat y_{\text{clf}}(\mathbf{x})=\mathrm{mode}\left(\{T_m(\mathbf{x})\}_{m=1}^{M}\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T_m(\mathbf{x})\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 棵树对样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的预测。回归任务把所有树的输出做平均，以减少波动；分类任务取众数 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{mode}(\cdot)\)</span>，也就是票数最多的类别。随机森林的稳定性正来自这种“多棵树共同决定”的聚合机制。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(M\)</span>：树的数量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(d'\)</span>：每次分裂考虑的特征数（常见经验：分类用 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{d}\)</span>，回归用 <span displaypfx="inline-" class="mathjax-container">\(d/3\)</span>）。</li>
</ul>
<div class="blog_h4"><span class="graybg">为什么有效：方差下降与“去相关”</span></div>
<p>若单棵树预测的方差为 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span>，不同树之间的相关系数近似为 <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span>（<span displaypfx="inline-" class="mathjax-container">\(0\le\rho\le 1\)</span>），则平均后的方差近似为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}\!\left(\frac{1}{M}\sum_{m=1}^{M}T_m(\mathbf{x})\right)\approx \sigma^2\left(\rho+\frac{1-\rho}{M}\right)\]</span>
<p>这个近似式子把随机森林为什么有效说得很清楚： <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 是单棵树自身的预测波动， <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span> 是树与树之间的相关性， <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 是树数。树越多， <span displaypfx="inline-" class="mathjax-container">\(\frac{1-\rho}{M}\)</span> 越小；树之间越不相似， <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span> 越低，最终平均后的波动就越小。</p>
<p>因此随机森林有两条主线：</p>
<ul>
<li>增加 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 降低 <span displaypfx="inline-" class="mathjax-container">\((1-\rho)/M\)</span> 项。</li>
<li>通过 bootstrap + 特征子采样降低相关性 <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span>，让集成真正“互补”。</li>
</ul>
<div class="blog_h4"><span class="graybg">OOB：不用额外验证集的误差估计</span></div>
<p>bootstrap 采样会重复抽到某些样本。对固定样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span>，一次采样中没被抽到的概率为 <span displaypfx="inline-" class="mathjax-container">\((1-\frac{1}{N})^N\approx e^{-1}\approx 0.368\)</span>。因此每棵树大约有 36.8% 的样本是袋外（Out-of-Bag, OOB）样本，可用它们评估该树对未见数据的表现，并对全森林给出近似验证误差。</p>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\((1-\frac{1}{N})\)</span> 表示“一次抽样没抽到某个固定样本”的概率，连续做 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 次后得到 <span displaypfx="inline-" class="mathjax-container">\((1-\frac{1}{N})^N\)</span>。当 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 足够大时，它逼近 <span displaypfx="inline-" class="mathjax-container">\(e^{-1}\)</span>，也就是约 36.8%。这就是为什么随机森林天然拥有一批“没参与这棵树训练”的 OOB 样本。</p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>表格数据（Tabular）的强默认基线：非线性、特征交互、缺失值与尺度不一致都较鲁棒。</li>
<li>对超参数不敏感、训练稳定；可用特征重要性（Feature Importance）做解释与特征筛查。</li>
<li>当需要极致精度时，GBDT 家族往往更强；当需要更快推理/更小模型时，线性/浅层模型更合适。</li>
</ul>
<div class="blog_h3"><span class="graybg">梯度提升树（GBDT）</span></div>
<p>梯度提升树（Gradient Boosting Decision Tree, GBDT）是一类按序叠加回归树的加法模型（Additive Model）。模型从简单的初始预测 <span displaypfx="inline-" class="mathjax-container">\(F_0\)</span> 出发，在每一轮加入一棵新树，用于修正当前模型尚未拟合好的部分，从而逐步降低训练集上的经验风险（Empirical Risk）。</p>
<p>每一轮新增的树都对应一个修正函数（Correction Function）。它不直接重新学习标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>，而是拟合当前模型输出与目标之间尚未被解释的差异。多轮修正连续叠加后，模型会从粗糙预测逐步逼近目标函数。</p>
<p>一个直观比喻是：GBDT 像一组按顺序接手的阅卷老师。第一位老师先给出一个粗略分数，后面的每一位老师都不重做整张卷子，只专门检查前面模型错得最明显的地方，并在这些地方补上修正意见。树一棵接一棵叠加后，最终预测会越来越接近真实值。</p>
<p>这里每棵小树都不是独立完成任务的“大模型”，而是一个局部纠错器。它关心的是当前模型还没解释好的误差：哪些样本被高估了，哪些被低估了，以及这些误差集中出现在哪些特征区域。“梯度”对应当前损失下降最快的修正方向，“提升”则表示把这些小修正持续累加成一个更强的整体模型。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/gbdt.jpg"><img class="alignnone size-full wp-image-41247" src="https://blog.gmem.cc/wp-content/uploads/2026/03/gbdt.jpg" alt="gbdt" width="1408" height="768" /></a></p>
<div class="blog_h4"><span class="graybg">目标：最小化经验风险</span></div>
<p>GBDT 背后的目标非常直接：寻找一个函数 <span displaypfx="inline-" class="mathjax-container">\(F\)</span>，使训练集上的总损失最小：</p>
<span displaypfx="" class="mathjax-container">\[ \min_F\ \sum_{i=1}^{N}\ell\big(y_i,F(\mathbf{x}_i)\big) \]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\ell(y,F(\mathbf{x}))\)</span> 是单样本损失函数，平方损失、对数损失等都可以放进来。难点在于：函数 <span displaypfx="inline-" class="mathjax-container">\(F\)</span> 不是一个普通标量参数，而是一个复杂的预测函数；如果一次性同时优化所有树的结构和叶子输出，组合空间过大，几乎不可直接求解。</p>
<p>因此 GBDT 采用前向分步加法（Forward Stagewise Additive Modeling）：不一次求整个 <span displaypfx="inline-" class="mathjax-container">\(F\)</span>，而是把它写成逐步累加的形式，只在第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮新增一个修正函数 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[ F_M(\mathbf{x})=F_0(\mathbf{x})+\nu\sum_{m=1}^{M}f_m(\mathbf{x}) \]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(F_M(\mathbf{x})\)</span> 表示：模型经过总共 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 轮提升后，对输入样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 输出的最终预测值。下标 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 表示“已经累计做了 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 次修正”，不是幂次，也不是某一棵单独的树。GBDT 的最终模型是许多轮小修正叠加后的总结果。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span>：一个输入样本的特征向量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(F_0(\mathbf{x})\)</span>：初始模型对样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的预测，常取常数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 使 <span displaypfx="inline-" class="mathjax-container">\(\sum_i \ell(y_i,c)\)</span> 最小。它可以理解为模型在还没有长出任何树之前给出的第一版粗略判断。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f_m(\mathbf{x})\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮新增的回归树在样本 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 上给出的修正值，负责弥补当前模型尚未拟合好的部分。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_{m=1}^{M}f_m(\mathbf{x})\)</span>：把前 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 轮所有修正树的输出加起来，得到总修正量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\nu\in(0,1]\)</span>：学习率（Shrinkage），控制每一步修正只走多大。它的作用不是改变方向，而是缩小步长，使训练更稳定、泛化更好。</li>
</ul>
<p>这个公式也可以按“底稿 + 反复批改”来理解： <span displaypfx="inline-" class="mathjax-container">\(F_0(\mathbf{x})\)</span> 是第一版预测，后面的每个 <span displaypfx="inline-" class="mathjax-container">\(f_m(\mathbf{x})\)</span> 都是在已有结果上补一小笔修正，最终的 <span displaypfx="inline-" class="mathjax-container">\(F_M(\mathbf{x})\)</span> 就是经历 <span displaypfx="inline-" class="mathjax-container">\(M\)</span> 次修正后的版本。</p>
<span displaypfx="" class="mathjax-container">\[ f_m=\arg\min_f\sum_{i=1}^{N}\ell\big(y_i,F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\big) \]</span>
<p>这条式子表示：在第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮，模型要寻找一个新的修正函数 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span>，使它加到旧模型 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}\)</span> 上之后，训练集的总损失尽可能小。这里的 <span displaypfx="inline-" class="mathjax-container">\(\arg\min_f\)</span> 可以读作“在所有候选函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 里，找出那个能让目标最小的函数”。因此，求出来的不是一个数，而是当前这一轮最合适的新树。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(F_{m-1}(\mathbf{x}_i)\)</span>：前 <span displaypfx="inline-" class="mathjax-container">\(m-1\)</span> 轮模型在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本上的当前预测值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x}_i)\)</span>：候选新树在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本上的修正值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\)</span>：把新树加进去之后，这个样本的新预测值。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\ell\big(y_i,F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\big)\)</span>：该样本在新预测下的损失。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_{i=1}^{N}\ell(\cdot)\)</span>：把所有样本的损失加总，得到这一轮希望尽量压低的整体目标。</li>
</ul>
<p>这一轮之所以写成 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}+f\)</span>，而不是重新求一个全新的 <span displaypfx="inline-" class="mathjax-container">\(F_m\)</span>，原因在于 GBDT 采用的是逐步修正策略：旧模型已经学到的部分先保留，新树只负责补上当前还没有拟合好的误差。这样每一轮只解决一个更小的局部问题，计算上更可行，也更符合“不断纠错”的直觉。</p>
<p>第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮只优化新增的修正函数 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span>，而把已有模型 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}\)</span> 视为固定量。这样做把原本难以整体求解的函数优化问题，拆成了一系列可逐步求解的局部优化问题。</p>
<div class="blog_h4"><span class="graybg">负梯度的来源</span></div>
<p>上面的子问题仍然不容易直接做，因为“最佳新树”本身还是一个复杂的函数搜索问题。GBDT 的关键近似是：在当前模型 <span displaypfx="inline-" class="mathjax-container">\(F_{m-1}\)</span> 附近，对损失做一阶展开，只看局部下降方向。</p>
<p>对单个样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span>，把新增修正记为 <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x}_i)\)</span>，则有：</p>
<span displaypfx="" class="mathjax-container">\[ \ell\big(y_i,F_{m-1}(\mathbf{x}_i)+f(\mathbf{x}_i)\big) \approx \ell\big(y_i,F_{m-1}(\mathbf{x}_i)\big) + \frac{\partial \ell(y_i,F(\mathbf{x}_i))}{\partial F(\mathbf{x}_i)}\Big|_{F=F_{m-1}} f(\mathbf{x}_i) \]</span>
<p>一阶展开表明，新增函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 的最优局部方向由损失对模型输出的负梯度决定。因此第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮的伪残差（Pseudo-residual）定义为：</p>
<span displaypfx="" class="mathjax-container">\[ r_i^{(m)}=-\left.\frac{\partial \ell\left(y_i,F(\mathbf{x}_i)\right)}{\partial F(\mathbf{x}_i)}\right|_{F=F_{m-1}} \]</span>
<p>接下来的动作就自然了：不用树直接拟合标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span>，而是用一棵回归树去拟合这些伪残差 <span displaypfx="inline-" class="mathjax-container">\(r_i^{(m)}\)</span>。这棵树学到的，就是“当前模型在输入空间不同区域里，应该往哪个方向、修正多大”的分段常数近似。</p>
<p>模型更新写成：</p>
<span displaypfx="" class="mathjax-container">\[ F_m(\mathbf{x})=F_{m-1}(\mathbf{x})+\nu f_m(\mathbf{x}) \]</span>
<p>“梯度提升”这一名称对应的正是上述更新方式：优化对象不是有限维参数向量，而是预测函数本身；每一轮更新都沿着损失在函数空间中的负梯度方向加入一个新的修正函数。</p>
<div class="blog_h4"><span class="graybg">平方损失下的残差形式</span></div>
<p>若损失取平方损失</p>
<span displaypfx="" class="mathjax-container">\[ \ell(y,F)=\frac{1}{2}(y-F)^2 \]</span>
<p>则对模型输出求导：</p>
<span displaypfx="" class="mathjax-container">\[ \frac{\partial \ell(y,F)}{\partial F}=F-y \]</span>
<p>因此第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮的负梯度为：</p>
<span displaypfx="" class="mathjax-container">\[ r_i^{(m)}=-\left(F_{m-1}(\mathbf{x}_i)-y_i\right)=y_i-F_{m-1}(\mathbf{x}_i) \]</span>
<p>这正是当前预测与真实值之间的残差（Residual）。所以在回归任务里，人们常把 GBDT 说成“不断拟合残差”；这并不是另一套经验规则，而是平方损失下负梯度公式的直接结果。</p>
<div class="blog_h4"><span class="graybg">不同损失下学到的是什么</span></div>
<p>“每轮拟合残差”只在平方损失下最直观。更一般地，GBDT 拟合的是当前损失对模型输出的负梯度，因此不同任务下，每轮新树学到的对象并不完全相同。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">任务 / 损失</td>
<td style="text-align: center;">当前轮拟合的量</td>
<td style="text-align: center;">含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>平方损失（MSE）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y-F(x)\)</span></td>
<td>真实值与当前预测的残差</td>
</tr>
<tr>
<td>绝对损失（MAE）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{sign}(y-F(x))\)</span></td>
<td>只关心修正方向，不强调误差幅度；对异常值更鲁棒</td>
</tr>
<tr>
<td>二分类对数损失（Log Loss）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y-p(x)\)</span></td>
<td>真实标签与当前预测概率之间的差</td>
</tr>
</tbody>
</table>
<p>因此，GBDT 不是被固定死在“回归残差拟合”这一种形式上，而是一套更一般的函数优化框架。平方损失下它表现为残差学习；分类任务下，它表现为不断修正当前概率估计。</p>
<div class="blog_h4"><span class="graybg">树作为局部修正器</span></div>
<p>因为树天然产生分段常数（Piecewise Constant）的局部修正。若某一轮中，一棵树把样本空间切成若干区域 <span displaypfx="inline-" class="mathjax-container">\(R_1,\dots,R_J\)</span>，那么它输出的是：</p>
<span displaypfx="" class="mathjax-container">\[ f_m(\mathbf{x})=\sum_{j=1}^{J}\gamma_j\mathbf{1}(\mathbf{x}\in R_j) \]</span>
<p>这意味着：模型不是对整个输入空间统一加一个修正，而是在每个局部区域里分别加一个常数修正 <span displaypfx="inline-" class="mathjax-container">\(\gamma_j\)</span>。这正适合处理表格数据里的阈值效应、非线性和特征交互。</p>
<div class="blog_h4"><span class="graybg">叶子输出的解析解</span></div>
<p>在平方损失下，若某个叶子覆盖的样本集合为 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，并用常数 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 拟合这一叶内的残差 <span displaypfx="inline-" class="mathjax-container">\(r_i\)</span>，则该叶子的局部目标是：</p>
<span displaypfx="" class="mathjax-container">\[ \min_{\gamma}\sum_{i\in S}(r_i-\gamma)^2 \]</span>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 求导并令其为 0：</p>
<span displaypfx="" class="mathjax-container">\[ \frac{d}{d\gamma}\sum_{i\in S}(r_i-\gamma)^2=-2\sum_{i\in S}(r_i-\gamma)=0 \]</span>
<span displaypfx="" class="mathjax-container">\[ \sum_{i\in S}r_i-|S|\gamma=0 \]</span>
<span displaypfx="" class="mathjax-container">\[ \gamma^*=\frac{1}{|S|}\sum_{i\in S}r_i \]</span>
<p>因此叶子输出之所以是均值，不是经验设定，而是平方损失下这个局部最小二乘问题的解析解。每一轮加入一棵树，本质上是在每个局部区域里补上一段“平均残差修正”。</p>
<div class="blog_h4"><span class="graybg">训练流程</span></div>
<ol>
<li>初始化 <span displaypfx="inline-" class="mathjax-container">\(F_0\)</span>，通常取使总体损失最小的常数模型。</li>
<li>对第 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 轮，计算所有样本的伪残差 <span displaypfx="inline-" class="mathjax-container">\(r_i^{(m)}\)</span>。</li>
<li>训练一棵回归树 <span displaypfx="inline-" class="mathjax-container">\(f_m\)</span> 去拟合 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{x}_i,r_i^{(m)})\)</span>。</li>
<li>把这棵树按学习率 <span displaypfx="inline-" class="mathjax-container">\(\nu\)</span> 缩小后加到当前模型上。</li>
<li>重复多轮，直到验证集误差不再下降，或达到预设树数。</li>
</ol>
<p><span style="background-color: #c0c0c0;">GBDT 的本质，是把复杂的函数优化问题拆成许多轮局部修正，并在每一轮用一棵小树逼近当前损失下降最快的方向。</span></p>
<div class="blog_h4"><span class="graybg">适用场景</span></div>
<ul>
<li>结构化表格任务中的强模型：广告、推荐、风控、运营排序、传统特征工程场景。</li>
<li>对特征尺度、非线性、缺失值较鲁棒；能自动学习高阶交互。</li>
<li>注意：对超参数更敏感（树深、学习率、树数、采样比例）；需要用验证集早停（Early Stopping）防止过拟合。</li>
</ul>
<div class="blog_h4"><span class="graybg">为什么它在表格数据上强、在别的场景不一定强</span></div>
<p>GBDT 家族在表格数据上长期强势，并不是偶然。表格任务里常见的模式，本来就很适合树模型表达：阈值效应、离散规则、局部非线性、特征交互、缺失值路径、不同子群体的分段行为。树的分裂结构天然能把“收入高且近期投诉过的老用户”和“收入低但活跃度高的新用户”这类组合规则直接切出来，而不需要像线性模型那样依赖大量手工交叉特征。</p>
<p>但同样的归纳偏置，在高维稀疏文本、图像、语音这类输入上就未必占优。因为这些任务的有效模式通常不是少数几个阈值切分，而是分布在海量维度上的连续结构、局部平滑模式或长程依赖。此时树模型虽然能做基线，但往往不如专门面向非结构化数据的线性稀疏模型、卷积网络或 Transformer。</p>
<div class="blog_h4"><span class="graybg">优点与局限</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>优点</td>
<td>表格数据精度高；对特征缩放不敏感；可处理非线性与高阶交互；通常还能给出特征重要性</td>
</tr>
<tr>
<td>局限 1</td>
<td>串行训练，训练速度通常慢于随机森林等并行集成</td>
</tr>
<tr>
<td>局限 2</td>
<td>对学习率、树数、树深、采样比例等超参数较敏感，容易出现“稍调不慎就欠拟合或过拟合”</td>
</tr>
<tr>
<td>局限 3</td>
<td>在超高维稀疏文本或图像等非结构化输入上，通常不是最自然的主力模型</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">XGBoost</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>XGBoost（Extreme Gradient Boosting）是在梯度提升树（Gradient Boosting Decision Tree, GBDT）基础上的工程化强化版本。它关注的是：在保持高表达能力的同时，让树的生长过程更稳定、目标函数更明确、复杂度控制更系统。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>XGBoost 仍然采用加法模型（Additive Model）：当前模型由多棵树的输出求和得到，第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮只学习一棵新树 <span displaypfx="inline-" class="mathjax-container">\(f_t\)</span> 作为修正项。它的关键强化点有两条：第一，把新增树的学习写成显式的带正则优化问题；第二，对该目标做二阶近似，同时利用梯度（Gradient）和海森（Hessian）信息计算叶子输出与分裂增益。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>设第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮预测为 <span displaypfx="inline-" class="mathjax-container">\(\tilde y_i^{(t)}=\tilde y_i^{(t-1)}+f_t(\boldsymbol{x}_i)\)</span>。XGBoost 在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮优化的目标可写为：</p>
<span displaypfx="" class="mathjax-container">\[\text{Obj}^{(t)}=\sum_{i=1}^{N} \ell(y_i,\tilde y_i^{(t)})+\Omega(f_t)+\text{const}\]</span>
<p>在这个目标里， <span displaypfx="inline-" class="mathjax-container">\(\ell(y_i,\tilde y_i^{(t)})\)</span> 衡量第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 轮更新后的预测误差， <span displaypfx="inline-" class="mathjax-container">\(\Omega(f_t)\)</span> 惩罚新树本身的复杂度， <span displaypfx="inline-" class="mathjax-container">\(\text{const}\)</span> 则表示与当前轮新树无关的常数项。也就是说，XGBoost 每一轮都在平衡“把误差降下去”和“不要把树长得太复杂”。</p>
<p>其中正则项通常取：</p>
<span displaypfx="" class="mathjax-container">\[\Omega(f)=\gamma T+\frac{\lambda}{2}\sum_{j=1}^{T}w_j^2\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 是叶子数， <span displaypfx="inline-" class="mathjax-container">\(\gamma T\)</span> 惩罚“树长出太多叶子”， <span displaypfx="inline-" class="mathjax-container">\(\frac{\lambda}{2}\sum_j w_j^2\)</span> 惩罚“每个叶子的输出值过大”。前者控制结构复杂度，后者控制数值幅度，两者合起来让模型更稳。</p>
<p>对损失在当前预测附近做二阶泰勒展开，定义：</p>
<span displaypfx="" class="mathjax-container">\[g_i=\frac{\partial \ell(y_i,\tilde y_i)}{\partial \tilde y_i}\Big|_{\tilde y_i=\tilde y_i^{(t-1)}},\qquad h_i=\frac{\partial^2 \ell(y_i,\tilde y_i)}{\partial \tilde y_i^2}\Big|_{\tilde y_i=\tilde y_i^{(t-1)}}\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(g_i\)</span> 是一阶导数，表示“当前这个样本朝哪个方向改，损失下降最快”； <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 是二阶导数，表示“这个方向有多陡、多稳定”。XGBoost 同时使用这两项信息，所以比只用一阶梯度的做法更精细。</p>
<p>则近似目标为：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\text{Obj}}^{(t)}\approx \sum_{i=1}^{N} \left(g_i f_t(\boldsymbol{x}_i)+\frac{1}{2}h_i f_t(\boldsymbol{x}_i)^2\right)+\Omega(f_t)\]</span>
<p>这条近似目标里， <span displaypfx="inline-" class="mathjax-container">\(f_t(\boldsymbol{x}_i)\)</span> 是新树在样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 上给出的修正值。线性项 <span displaypfx="inline-" class="mathjax-container">\(g_i f_t(\boldsymbol{x}_i)\)</span> 反映“沿当前方向修正是否有利”，二次项 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}h_i f_t(\boldsymbol{x}_i)^2\)</span> 反映“修正过大时会不会带来额外代价”。</p>
<p>若固定树结构，记落在叶子 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的样本集合为 <span displaypfx="inline-" class="mathjax-container">\(I_j\)</span>，并定义 <span displaypfx="inline-" class="mathjax-container">\(G_j=\sum_{i\in I_j}g_i\)</span>、<span displaypfx="inline-" class="mathjax-container">\(H_j=\sum_{i\in I_j}h_i\)</span>，则该叶子的最优输出为：</p>
<span displaypfx="" class="mathjax-container">\[w_j^*=-\frac{G_j}{H_j+\lambda}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(G_j\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个叶子里所有样本一阶梯度的总和， <span displaypfx="inline-" class="mathjax-container">\(H_j\)</span> 是二阶梯度总和。分子告诉模型这个叶子整体应该往哪个方向修正，分母则用二阶信息和正则项 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 把修正幅度稳住。</p>
<p>某次分裂把父叶拆成左右两叶后的增益（Gain）为：</p>
<span displaypfx="" class="mathjax-container">\[\text{Gain}=\frac{1}{2}\left(\frac{G_L^2}{H_L+\lambda}+\frac{G_R^2}{H_R+\lambda}-\frac{G^2}{H+\lambda}\right)-\gamma\]</span>
<p>这个增益式子比较的是“父叶不分裂”和“分成左右两叶”谁更划算。 <span displaypfx="inline-" class="mathjax-container">\(G_L,H_L\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(G_R,H_R\)</span> 分别是左右子叶的梯度统计量， <span displaypfx="inline-" class="mathjax-container">\(G,H\)</span> 是父叶统计量；最后减去 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span>，表示每多长一个叶子都要付复杂度代价。</p>
<p>这说明 XGBoost 的分裂同时考虑“收益”和“复杂度惩罚”，并以此控制结构扩张。</p>
<div class="blog_h4"><span class="graybg">相对经典 GBDT 的强化点</span></div>
<p>XGBoost 的工程优势并不只来自“实现更快”。它把几项在工业界非常关键的能力同时做扎实了：显式复杂度正则化、二阶梯度优化、行采样与列采样、对稀疏特征的处理、缓存友好的分裂搜索，以及成熟的早停和监控接口。也正因为如此，它长期被视作结构化数据建模的稳健默认选项。</p>
<ul>
<li>正则化更明确：通过叶子数惩罚与叶子权重 L2 正则显式控制树复杂度。</li>
<li>分裂选择更精细：同时使用一阶梯度与二阶梯度，不只看“该往哪边改”，还看“这一步有多稳”。</li>
<li>随机化更充分：支持样本子采样 <span displaypfx="inline-" class="mathjax-container">\(\text{subsample}\)</span> 和列采样 <span displaypfx="inline-" class="mathjax-container">\(\text{colsample\_bytree}\)</span>，既降计算量也降过拟合。</li>
<li>工程实现更成熟：对稀疏输入、缓存访问和大规模数据训练都做了专门优化。</li>
</ul>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>用当前模型计算每个样本的一阶梯度 <span displaypfx="inline-" class="mathjax-container">\(g_i\)</span> 与二阶梯度 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span>。</li>
<li>在候选特征与候选阈值上搜索分裂增益最大的切分。</li>
<li>固定树结构后，利用 <span displaypfx="inline-" class="mathjax-container">\(w_j^*\)</span> 计算每个叶子的最优输出。</li>
<li>把新树加入现有模型，继续下一轮迭代。</li>
<li>预测时把所有树的输出累加，再映射到分类或回归结果。</li>
</ol>
<div class="blog_h4"><span class="graybg">训练控制与早停</span></div>
<p>XGBoost 在工程实践里常配合较大的树数上限和验证集早停一起使用。做法通常不是先死板地决定“到底训 300 棵还是 800 棵树”，而是给一个偏大的上限，再观察验证集误差或任务指标何时不再提升。这样，树数不再是拍脑袋设定的固定值，而变成由验证集驱动的可学习训练预算。</p>
<p>这对 Boosting 家族尤其重要，因为学习率较小时，往往需要更多轮小步修正；若学习率较大，又更容易在后期进入过拟合区域。早停在这里的作用，是把“拟合能力很强”与“不要无限继续纠错”之间的边界交给验证集来判定。</p>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在点击率预估（CTR Prediction）中，用户、广告和上下文之间往往存在复杂交互。XGBoost 可以通过逐轮加树自动学习“什么样的用户在什么时间、看到什么样的广告更容易点击”这类组合规则，因此在工业级表格任务中长期保持强竞争力。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：表格数据表现强，目标函数清晰，正则化明确，工程生态成熟。</li>
<li>局限：超参数较多；树深、学习率、树数、采样比例之间存在明显耦合。</li>
<li>适用场景：风控、广告、推荐、排序与一般结构化表格建模。</li>
</ul>
<div class="blog_h3"><span class="graybg">LightGBM</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>LightGBM 是面向大规模数据与高维稀疏特征优化的 GBDT 实现。它关注的核心问题是：当样本量和特征维度都很大时，如何降低分裂搜索的时间与内存成本，同时尽量不牺牲精度。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>LightGBM 的两个关键设计是直方图分桶（Histogram Binning）和叶子优先生长（Leaf-wise Growth）。前者把连续特征离散到有限个桶上，后者每一步都继续分裂当前收益最大的叶子，从而在相同叶子预算下更快降低训练误差。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>在优化目标上，LightGBM 与 GBDT / XGBoost 一致，仍然是逐轮加入树来降低损失。它的区别主要体现在计算方式：若某个连续特征被离散到 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 个桶，则算法只需在桶边界上搜索分裂点，而不必在所有实数取值上逐一枚举。这样，每个节点维护的是桶级梯度统计量，而非逐样本的原始实数值。</p>
<p>叶子优先生长意味着：算法始终选择当前分裂增益最大的叶子继续向下扩展。它通常比按层生长（Level-wise）更激进，因而在同样叶子数限制下更容易得到较低训练误差，但也更容易在局部区域长出很深的树。</p>
<div class="blog_h4"><span class="graybg">速度来自哪些具体机制</span></div>
<p>LightGBM 的“快”并不是单一技巧带来的，而是多项近似与工程策略叠加的结果。</p>
<ul>
<li>直方图分桶：把连续特征先离散到有限个桶，例如默认 255 或 256 个桶。这样找切分点时，复杂度从“遍历大量原始取值”变成“遍历固定桶边界”。</li>
<li>Leaf-wise 生长：每一轮只扩展当前最值得继续分裂的叶子，在相同叶子预算下往往比 level-wise 更快压低误差。</li>
<li>GOSS（Gradient-based One-Side Sampling）：优先保留梯度较大的样本，因为这些样本更能代表当前模型最需要修正的区域；对梯度较小的样本只随机保留一部分，以减少计算量。</li>
<li>EFB（Exclusive Feature Bundling）：把互斥的稀疏特征打包到同一组表示里，降低高维稀疏输入的有效维度。</li>
</ul>
<p>其中 GOSS 和 EFB 的意义非常工程化。前者服务于“大样本下不必每轮都看全量样本”，后者服务于“高维稀疏特征下不必把所有稀疏列都单独维护”。也正因为如此，LightGBM 尤其适合推荐、广告、风控这类既大规模又高度稀疏的表格任务。</p>
<p>LightGBM 也因此更依赖约束配套。leaf-wise 生长虽然更激进，但如果不同时限制 <pre class="crayon-plain-tag">max_depth</pre>、<pre class="crayon-plain-tag">num_leaves</pre>、<pre class="crayon-plain-tag">min_data_in_leaf</pre> 这类参数，局部树会很快长得过深，训练误差降得很漂亮，验证集却未必跟着受益。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>将连续特征离散到有限个桶，并统计桶级梯度信息。</li>
<li>在每个节点上基于桶统计量搜索最优切分。</li>
<li>选择全局增益最大的叶子继续分裂。</li>
<li>使用最大深度、最大叶子数、最小叶子样本数等约束抑制过拟合。</li>
<li>预测时沿树路径到达叶子并累加所有树的输出。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在大规模推荐粗排任务中，输入特征通常极高维且高度稀疏。LightGBM 能在保留较强拟合能力的同时显著缩短训练时间，因此非常适合需要频繁重训的工业流水线。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：训练快、内存占用低，对大规模稀疏特征友好。</li>
<li>局限：叶子优先策略若约束不足，局部树会过深，过拟合风险更高。</li>
<li>适用场景：超大规模表格数据、稀疏特征建模、追求训练效率的工业场景。</li>
</ul>
<div class="blog_h3"><span class="graybg">CatBoost</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>CatBoost 是另一条非常重要的 Boosting 工业路线。它关注的核心问题不是“比 XGBoost 更通用”或“比 LightGBM 更快”，而是：<span style="background-color: #c0c0c0;">当数据里有大量高基数类别特征时，如何在不引入严重数据泄露和预测偏移的前提下，把这些特征真正用好</span>。广告、推荐、电商、用户画像、商品属性等任务里，这个问题尤其关键。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>CatBoost 的两项代表性设计是有序目标统计（Ordered Target Statistics）与有序提升（Ordered Boosting）。前者处理类别特征，后者处理训练偏移。它们共同服务于同一个目标：不要让模型在训练时偷看到本不该知道的目标信息。</p>
<div class="blog_h4"><span class="graybg">类别特征为什么难</span></div>
<p>对树模型而言，类别特征的难点不只是“字符串不能直接喂进去”，更在于很多类别列的取值空间极大，例如用户 ID、商品 ID、城市、品牌、广告位、渠道来源。若简单做 one-hot 编码，维度会迅速膨胀；若直接用目标均值编码，又很容易把标签信息泄露进训练过程，导致离线效果虚高、线上泛化变差。</p>
<p>CatBoost 的思路是：用类别对应的目标统计量来表示类别，但计算某个样本的统计量时，只允许使用它之前样本的信息，而不允许看见它自己以及它之后的样本标签。这样得到的类别表示虽然仍然利用了监督信号，却显著降低了数据泄露与目标泄露风险。</p>
<div class="blog_h4"><span class="graybg">有序目标统计与有序提升</span></div>
<p>设某个类别值为 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，其编码值可以理解为“在训练顺序中，当前位置之前出现过的同类样本的目标统计量，再配合一个全局先验做平滑”。这样一来，模型在看到当前样本时，只能利用“过去”信息，不能直接偷看当前标签。</p>
<p>同样的思路也延伸到 Boosting 训练本身。传统目标编码或普通 Boosting 在训练时，常会让同一批样本之间发生微妙的信息穿透，形成预测偏移（Prediction Shift）。CatBoost 通过 ordered boosting 尽量让每一步的残差估计更接近“真正未见样本上的估计误差”，从而让训练分布和推理分布更一致。</p>
<div class="blog_h4"><span class="graybg">对称树</span></div>
<p>CatBoost 还大量使用对称树（Symmetric Tree，也常称 Oblivious Tree）：同一层的所有节点共享同一个切分规则。这种树结构比一般决策树更受约束，表达上没那么自由，但推理路径规整、计算高效，也更利于工程实现和高吞吐推断。</p>
<p>从工程角度看，CatBoost 的价值就在于把“类别特征处理”从手工特征工程里拿回模型内部。很多场景里，团队不再需要先在外部纠结 one-hot、频次编码、目标编码、平滑规则和泄露控制，而是可以把类别列直接交给模型，让训练流程自己处理这套问题。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：对类别特征支持最好，默认配置往往更稳，推理效率高。</li>
<li>局限：当类别特征优势不明显时，未必总能胜过 XGBoost 或 LightGBM；训练生态也没有前两者那么普遍。</li>
<li>适用场景：类别特征很多、ID 类特征很多、需要尽量减少手工编码工作的表格任务。</li>
</ul>
<div class="blog_h2"><span class="graybg">概率模型</span></div>
<p>这一类方法处理的核心问题是：模型不仅要给出“预测是什么”，还要明确回答“这个结果有多可信、数据是如何生成的、隐藏结构是什么”。因此它们直接建模概率分布（Probability Distribution）、隐变量（Latent Variables）或序列依赖关系，在分类、密度估计、软聚类、序列推断与不确定性表达中具有统一优势。</p>
<div class="blog_h3"><span class="graybg">朴素贝叶斯</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>朴素贝叶斯（Naive Bayes）用于分类问题。给定特征 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，它要估计样本属于每个类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的后验概率 <span displaypfx="inline-" class="mathjax-container">\(p(y|\boldsymbol{x})\)</span>。该方法尤其适合高维稀疏、小样本或需要快速概率基线的任务。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>朴素贝叶斯的核心假设是：在给定类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的条件下，各个特征条件独立（Conditional Independence）。这个假设通常并不严格成立，但它把高维联合分布的估计问题，转化为多个一维条件分布的估计问题。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>由贝叶斯公式：</p>
<span displaypfx="" class="mathjax-container">\[p(y|\boldsymbol{x})=\frac{p(\boldsymbol{x}|y)p(y)}{p(\boldsymbol{x})}\]</span>
<p>这个贝叶斯公式把后验概率拆成三部分： <span displaypfx="inline-" class="mathjax-container">\(p(y|\boldsymbol{x})\)</span> 是看到特征后属于类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的概率； <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x}|y)\)</span> 是该类别生成这组特征的可能性； <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 是类别先验； <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x})\)</span> 则是对所有类别做归一化的总证据。</p>
<p>由于 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x})\)</span> 对所有类别相同，分类时只需比较：</p>
<span displaypfx="" class="mathjax-container">\[p(y|\boldsymbol{x})\propto p(y)\prod_{j=1}^{d}p(x_j|y)\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(\propto\)</span> 表示“成正比”。因为对同一个样本来说，分母 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x})\)</span> 对所有类别都相同，所以比较类别大小时只需看右边：先验 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 乘上每个特征条件概率 <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span> 的连乘积。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 是先验概率（Prior）， <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span> 是条件似然（Likelihood）。根据特征类型不同，可以得到高斯朴素贝叶斯、伯努利朴素贝叶斯、多项式朴素贝叶斯等变体。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>从训练集统计每个类别的先验概率 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span>。</li>
<li>估计每个类别下各特征的条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span>。</li>
<li>推断时计算各类别的对数后验分数 <span displaypfx="inline-" class="mathjax-container">\(\log p(y)+\sum_j \log p(x_j|y)\)</span>。</li>
<li>选择分数最大的类别作为预测输出。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在垃圾邮件检测中，若某些词在垃圾邮件中显著更常见，那么这些词对应的条件概率会被估得更大，从而把包含这些词的邮件判到垃圾类别。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：训练快、推断快、对高维稀疏特征友好。</li>
<li>局限：条件独立假设常被破坏；概率校准有时较弱。</li>
<li>适用场景：文本分类、垃圾邮件过滤、简单可靠的概率基线。</li>
</ul>
<div class="blog_h3"><span class="graybg">高斯混合模型（GMM）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>高斯混合模型（Gaussian Mixture Model, GMM）处理的是“数据由多个潜在高斯成分混合生成”的建模问题。与 K-Means 只输出硬划分不同，GMM 希望估计每个样本属于各个簇的概率，并允许不同簇有不同的协方差结构。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>GMM 引入潜变量 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 表示样本来自哪一个高斯成分。整体分布由多个高斯分布按混合系数加权得到，因此每个样本对各个簇的归属是软的（Soft Assignment），并以概率形式表达。</p>
<p>一个直观比喻是：把数据想成混在一起的几类人群，只能看到每个人的外在特征，却看不到他原本属于哪一类。每一类人群都有自己的“中心位置”和“分散形状”，对应一个高斯成分；整个数据集则像这些人群按不同比例叠在一起形成的总体分布。与 K-Means 把每个样本硬塞进某一个簇不同，GMM 会给出“这个样本更像第 1 类，也有一部分像第 2 类”这样的软归属结果。</p>
<p>因此，混合系数 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\)</span> 可以理解为各类人群在总体中的占比，均值 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}_k\)</span> 是每类人群的中心，协方差 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\Sigma}_k\)</span> 描述该群体沿不同方向的扩散方式，而责任度 <span displaypfx="inline-" class="mathjax-container">\(\gamma_{ik}\)</span> 则表示“样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 有多大程度属于第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 类”。这也是 GMM 比 K-Means 更灵活的原因：它允许边界模糊，也允许簇具有不同大小、方向和椭球形状。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>对样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，GMM 的概率密度写为：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{x})=\sum_{k=1}^{K} \pi_k \mathcal{N}(\boldsymbol{x}\mid \boldsymbol{\mu}_k,\boldsymbol{\Sigma}_k)\]</span>
<p>这条式子说明 GMM 的整体密度不是由单个高斯给出，而是 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个高斯成分的加权和。 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\)</span> 决定第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个成分在总体中的占比， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(\boldsymbol{x}\mid \boldsymbol{\mu}_k,\boldsymbol{\Sigma}_k)\)</span> 描述样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 在该成分下有多典型。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\)</span> 是混合系数，满足 <span displaypfx="inline-" class="mathjax-container">\(\pi_k\ge 0\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\sum_k \pi_k=1\)</span>。给定样本后，其属于第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个成分的责任度（Responsibility）为：</p>
<span displaypfx="" class="mathjax-container">\[\gamma_{ik}=p(z_i=k|\boldsymbol{x}_i)=\frac{\pi_k \mathcal{N}(\boldsymbol{x}_i\mid \boldsymbol{\mu}_k,\boldsymbol{\Sigma}_k)}{\sum_{j=1}^{K}\pi_j \mathcal{N}(\boldsymbol{x}_i\mid \boldsymbol{\mu}_j,\boldsymbol{\Sigma}_j)}\]</span>
<p>责任度 <span displaypfx="inline-" class="mathjax-container">\(\gamma_{ik}\)</span> 是“样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 属于成分 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的后验概率”。分子表示“成分 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 解释这个样本的能力”，分母则把所有成分对该样本的解释能力加总起来做归一化，因此所有 <span displaypfx="inline-" class="mathjax-container">\(\gamma_{ik}\)</span> 加起来等于 1。</p>
<p>GMM 的参数通常通过期望最大化（Expectation-Maximization, EM）求解。E 步计算责任度，M 步更新参数：</p>
<span displaypfx="" class="mathjax-container">\[N_k=\sum_{i=1}^{N}\gamma_{ik},\qquad \pi_k=\frac{N_k}{N}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(N_k\)</span> 不是成分 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的硬计数，而是软计数：每个样本只按自己的责任度贡献一部分。于是混合系数更新为 <span displaypfx="inline-" class="mathjax-container">\(\pi_k=\frac{N_k}{N}\)</span>，表示第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个成分在总体中的相对权重。</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{\mu}_k=\frac{1}{N_k}\sum_{i=1}^{N}\gamma_{ik}\boldsymbol{x}_i,\qquad \boldsymbol{\Sigma}_k=\frac{1}{N_k}\sum_{i=1}^{N}\gamma_{ik}(\boldsymbol{x}_i-\boldsymbol{\mu}_k)(\boldsymbol{x}_i-\boldsymbol{\mu}_k)^\top\]</span>
<p>均值更新式说明：每个样本按责任度大小对中心 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}_k\)</span> 做加权贡献；协方差更新式则是在该加权中心周围统计离散程度。责任度越大，样本对该成分的中心和形状影响越大。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>初始化混合系数、均值和协方差。</li>
<li>E 步：计算每个样本对各高斯成分的责任度。</li>
<li>M 步：根据责任度更新参数。</li>
<li>重复 E / M，直到对数似然收敛。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在用户分群中，若人群天然分成多个椭球状子群体，那么 GMM 不仅能给出簇划分，还能输出“某个用户属于每个群体的概率”，这比 K-Means 的硬分配更细致。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：软聚类、概率解释清晰、能建模不同协方差形状。</li>
<li>局限：对初始化敏感；高维时协方差估计代价高。</li>
<li>适用场景：软聚类、密度估计、带概率解释的聚类分析。</li>
</ul>
<div class="blog_h3"><span class="graybg">隐马尔可夫模型（HMM）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>隐马尔可夫模型（Hidden Markov Model, HMM）用于序列建模，尤其适合处理序列标注（Sequence Labeling）问题。序列标注的任务形式是：给定一个按时间或位置排列的输入序列，为序列中的每个位置分配一个标签。例如，在词性标注中，句子“我爱北京天安门”的每个词或字都需要对应一个词性标签；在语音识别中，一段连续声学信号需要对应到一串离散文字或音素标签。</p>
<p>HMM 是序列模型中的经典早期方法，在语音识别等领域长期具有重要地位。它的优势在于结构清晰、推断高效，能够以较低的计算成本处理序列决策；与此同时，它主要依赖局部状态转移和局部发射关系，所使用的上下文信息相对有限。</p>
<p>HMM 的建模方式是：观测序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_{1:T}\)</span> 可以看到，但生成这些观测的状态序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{z}_{1:T}\)</span> 看不到。这里的隐藏状态通常对应词性、命名实体类别或发音状态等潜在标签。围绕这一模型，核心问题通常包括计算某段观测序列的概率、恢复最可能的隐藏状态路径，以及估计模型参数。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>HMM 假设隐藏状态满足一阶马尔可夫性（First-order Markov Property）：当前状态只依赖前一个状态；同时每个观测只依赖当前状态。一个直接的比喻是：无法直接看到远方朋友每天所处的天气，但可以持续看到他发布的活动，例如“去游泳”“去逛街”或“在家睡觉”。在这个比喻里，天气是隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span>，活动是观测 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span>。HMM 的生成逻辑正是<span style="background-color: #c0c0c0;">先生成隐藏状态序列，再由每个状态发射对应观测</span>。</p>
<p>如果今天是晴天，明天仍然晴天的概率通常较高；如果今天下雨，朋友在家休息的概率通常高于去游泳。这分别对应 HMM 中的状态转移概率 <span displaypfx="inline-" class="mathjax-container">\(p(z_t|z_{t-1})\)</span> 和发射概率 <span displaypfx="inline-" class="mathjax-container">\(p(x_t|z_t)\)</span>。因此，HMM 把序列建模为一个“剧本模拟器”：先按转移规律生成一条看不见的天气轨迹，再按照每一天的天气生成看得见的活动记录。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">HMM 概念</td>
<td style="text-align: center;">比喻中的对应物</td>
<td style="text-align: center;">含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span></td>
<td>每天的天气</td>
<td>真实存在，但观察者不能直接看到。</td>
</tr>
<tr>
<td>观测 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span></td>
<td>朋友圈里的活动</td>
<td>可以直接看到，用来反推隐藏状态。</td>
</tr>
<tr>
<td>初始分布 <span displaypfx="inline-" class="mathjax-container">\(p(z_1)\)</span></td>
<td>第一天各种天气出现的概率</td>
<td>序列从什么状态开始。</td>
</tr>
<tr>
<td>转移概率 <span displaypfx="inline-" class="mathjax-container">\(p(z_t|z_{t-1})\)</span></td>
<td>天气从今天到明天的变化规律</td>
<td>例如“晴天后仍是晴天”的概率较高。</td>
</tr>
<tr>
<td>发射概率 <span displaypfx="inline-" class="mathjax-container">\(p(x_t|z_t)\)</span></td>
<td>某种天气下出现某种活动的概率</td>
<td>例如下雨时更可能“在家睡觉”。</td>
</tr>
<tr>
<td>隐藏状态序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{z}_{1:T}\)</span></td>
<td>整段天气变化轨迹</td>
<td>模型希望恢复的潜在过程。</td>
</tr>
<tr>
<td>观测序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_{1:T}\)</span></td>
<td>整段活动记录</td>
<td>模型的输入数据。</td>
</tr>
</tbody>
</table>
<p>这个比喻也解释了 HMM 的优势与局限。它简单、快速、可解释，因为联合概率能够拆成局部转移和局部发射的乘积，并可用动态规划高效推断。但它的条件独立假设也很强：某一天的活动只由当天的天气决定，不直接依赖前后活动。在词性标注等任务中，一个词的标签往往同时受左右上下文影响，这种假设就会限制表达能力。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>HMM 的联合分布写为：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{x}_{1:T},\boldsymbol{z}_{1:T})=p(z_1)\prod_{t=2}^{T}p(z_t|z_{t-1})\prod_{t=1}^{T}p(x_t|z_t)\]</span>
<p>这条联合分布把一整条序列拆成三部分： <span displaypfx="inline-" class="mathjax-container">\(p(z_1)\)</span> 是初始状态概率， <span displaypfx="inline-" class="mathjax-container">\(\prod_{t=2}^{T}p(z_t|z_{t-1})\)</span> 是状态转移链， <span displaypfx="inline-" class="mathjax-container">\(\prod_{t=1}^{T}p(x_t|z_t)\)</span> 是每个状态发射观测的概率。正因为有这种乘积分解，HMM 才能用动态规划高效求解。</p>
<p>若记初始分布为 <span displaypfx="inline-" class="mathjax-container">\(\pi\)</span>，转移矩阵为 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，发射分布为 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>，则前向算法（Forward Algorithm）的核心量为：</p>
<span displaypfx="" class="mathjax-container">\[\alpha_t(j)=p(x_{1:t},z_t=j)\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(\alpha_t(j)\)</span> 的含义是：看到前 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个观测，同时当前时刻状态恰好是第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个状态的联合概率。它把“历史观测信息”和“当前落在哪个状态”压缩成一个可递推的中间量。</p>
<p>其递推为：</p>
<span displaypfx="" class="mathjax-container">\[\alpha_t(j)=\left[\sum_i \alpha_{t-1}(i)A_{ij}\right]B_j(x_t)\]</span>
<p>递推式可以分成两步看：先对所有上一步状态 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 求和，用 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}\)</span> 累积“从 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 转到 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的可能性”；再乘上 <span displaypfx="inline-" class="mathjax-container">\(B_j(x_t)\)</span>，表示当前状态 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 发射出观测 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 的概率。</p>
<p>最可能状态路径可由维特比算法（Viterbi Algorithm）求解；参数估计通常采用 Baum-Welch 算法，即 HMM 上的 EM。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>定义状态空间、转移概率和发射概率。</li>
<li>若参数已知，用前向 / 后向算法做概率计算，用维特比算法做解码。</li>
<li>若参数未知，用 Baum-Welch 在训练集上迭代估计参数。</li>
<li>根据任务输出状态路径、序列概率或边缘状态分布。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在词性标注中，观测是单词序列，隐藏状态是词性标签。HMM 会同时利用“某个词在某种词性下更常见”的发射规律和“词性之间如何转移”的序列规律完成整体解码。</p>
<p>例如，对序列“我 / 爱 / 北京 / 天安门”，模型看到的是词本身，看不到的是背后的标签序列。HMM 会比较多种候选路径，如“代词（Pronoun）→ 动词（Verb）→ 专有名词（Proper Noun）→ 专有名词（Proper Noun）”与其他不合理组合的概率。由于“我”更容易由代词状态发射，“爱”更容易由动词状态发射，而“代词后接动词、动词后接名词性成分”又符合常见转移规律，这条标签路径就会获得更高概率。这个过程可以理解为：模型一边根据词本身猜标签，一边检查整条词性路径是否顺畅。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/hmm.jpg"><img class="alignnone size-full wp-image-41251" src="https://blog.gmem.cc/wp-content/uploads/2026/03/hmm.jpg" alt="hmm" width="1408" height="768" /></a></p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：序列结构清晰，动态规划推断高效，可解释性强。</li>
<li>局限：一阶马尔可夫假设较强，表达能力有限。</li>
<li>适用场景：基础序列标注、时间状态切换建模、中小规模序列任务。</li>
</ul>
<div class="blog_h3"><span class="graybg">条件随机场（CRF）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>条件随机场（Conditional Random Field, CRF）主要用于结构化预测（Structured Prediction），尤其是序列标注。它处理的问题是：在给定输入序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的条件下，如何联合预测输出标签序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span>，并显式建模标签之间的依赖关系。</p>
<p>CRF 可以看作对 HMM 的进一步发展：它不再试图解释输入序列如何被生成，而是直接在给定输入的条件下预测最合理的标签结构。通过引入全局特征和更灵活的上下文依赖，CRF 缓解了 HMM 仅依赖局部独立假设所带来的信息不足。在深度学习广泛进入 NLP 之前，CRF 长期是各类标注任务中的核心方法。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>CRF 的核心是在给定观测序列的条件下，对整条标签序列进行全局评分，而不是显式描述数据生成过程。仍以天气和活动的比喻来理解：已知一整周的活动记录后，CRF 会把各种可能的天气序列都拿来比较，判断哪一种与这组活动整体最一致。它建模的是条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{y}|\boldsymbol{x})\)</span>，而不是 HMM 那样的联合分布 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{x},\boldsymbol{y})\)</span>。</p>
<p>这种“全局把控”体现在两个层面。第一，CRF 的打分对象是完整的标签路径，而不是每个位置互相独立的局部决策；第二，打分依据可以是灵活定义的特征函数（Feature Function）。例如，若连续三天都出现“游泳”，对应“连续晴天”的标签组合就应得到更高分；若一周中出现“滑雪”这类活动，与“夏天”一致的天气标签组合就应被显著压低。特征函数可以同时利用当前位置、前后邻域以及相邻标签之间的组合关系。</p>
<p>因此，CRF 可以视为一个<span style="background-color: #c0c0c0;">面向整条序列的打分系统</span>：输入固定，模型比较不同标签序列的相对合理性，并选择得分最高的一条。它的优势是能够充分利用复杂上下文和标签依赖，在序列标注任务中通常比 HMM 更准确；代价是训练和推断都更重，需要计算整条序列上的归一化与动态规划。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>线性链 CRF 的条件分布通常写为：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{y}|\boldsymbol{x})=\frac{1}{Z(\boldsymbol{x})}\exp\left(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\right)\]</span>
<p>这个式子最好分三层看。先看最里面的 <span displaypfx="inline-" class="mathjax-container">\(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\)</span>：它表示对整条标签序列逐位置累积特征得分。再看外面的指数 <span displaypfx="inline-" class="mathjax-container">\(\exp(\cdot)\)</span>：它把“总得分”变成一个始终为正的数，而且总得分越大，这个数就越大。最后再除以 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span>，把这些正数正规化成概率。因此，CRF 的计算顺序可以理解为：<span style="background-color: #c0c0c0;">先打分，再变成正权重，最后归一化成概率</span>。</p>
<p>为了看清楚 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 在做什么，可以先临时把分子记成一个“未归一化分数”：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{p}(\boldsymbol{y}|\boldsymbol{x})=\exp\left(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\right)\]</span>
<p>这里特意加波浪号，是为了强调它<span style="background-color: #c0c0c0;">还不是概率</span>。原因很简单：把所有可能标签序列的 <span displaypfx="inline-" class="mathjax-container">\(\tilde{p}(\boldsymbol{y}|\boldsymbol{x})\)</span> 加起来，结果一般不会恰好等于 1。它只是每条路径的相对权重，表达“这条标签路径有多合理”。</p>
<p>这时配分函数（Partition Function）就出现了：</p>
<span displaypfx="" class="mathjax-container">\[Z(\boldsymbol{x})=\sum_{\boldsymbol{y}} \exp\left(\sum_{t=1}^{T}\sum_k \lambda_k f_k(y_{t-1},y_t,\boldsymbol{x},t)\right)\]</span>
<p>之所以你会觉得它“和上面那个公式一样”，是因为它确实就是<span style="background-color: #c0c0c0;">把上式分子对所有可能的标签序列整体求和</span>。上面的 <span displaypfx="inline-" class="mathjax-container">\(p(\boldsymbol{y}|\boldsymbol{x})\)</span> 针对的是某一条固定标签序列 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span>；而这里的 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 不是看某一条路径，而是把所有可能路径都算一遍，再全部加起来。所以它不是“另一条几乎一样的公式”，而是“分子在全标签空间上的总和”。</p>
<p>于是条件概率就变成：</p>
<span displaypfx="" class="mathjax-container">\[p(\boldsymbol{y}|\boldsymbol{x})=\frac{\tilde{p}(\boldsymbol{y}|\boldsymbol{x})}{Z(\boldsymbol{x})}\]</span>
<p>现在这个式子就容易理解了：某条路径的概率，等于“这条路径自己的权重”除以“所有路径权重的总和”。这和 softmax 的归一化逻辑完全一致，只不过 softmax 是在有限个类别上归一化，而 CRF 是在所有可能的标签序列上归一化。</p>
<p>配分函数这个名字来自统计物理，但在这里不需要物理背景也能理解：它本质上就是一个<span style="background-color: #c0c0c0;">归一化常数</span>。没有它，模型只能说“路径 A 比路径 B 更合理”；有了它，模型才能进一步说“路径 A 的概率是多少”。也正因为 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 需要把所有可能标签路径都考虑进去，CRF 训练时才必须借助动态规划，而不能暴力枚举。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(f_k(y_{t-1},y_t,\boldsymbol{x},t)\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个特征函数，描述当前位置、相邻标签和输入之间的某种局部模式。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda_k\)</span>：该特征的权重；越大表示模型越重视这个模式。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>：固定输入序列；在讨论 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 时，它不变。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span>：某一条候选标签序列；计算 <span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span> 时要对所有可能的 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span> 求和。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_{t=1}^{T}\sum_k \lambda_k f_k(\cdot)\)</span>：整条标签序列的总得分。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\exp(\cdot)\)</span>：把总得分映射成正权重，并放大高分路径与低分路径之间的差异。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(Z(\boldsymbol{x})\)</span>：所有候选标签路径未归一化权重的总和，用于把相对权重变成概率。</li>
</ul>
<p>训练时最大化条件对数似然，解码时用维特比算法寻找最优标签序列。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>定义特征函数，描述输入与标签、标签与标签之间的关系。</li>
<li>用前向后向算法计算配分函数与梯度。</li>
<li>通过梯度法优化参数 <span displaypfx="inline-" class="mathjax-container">\(\lambda_k\)</span>。</li>
<li>推断时用维特比算法输出最优标签序列。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在命名实体识别（NER）中，局部分类器可能会把某个词单独判成人名，但 CRF 会进一步考虑相邻标签是否合法，从而减少孤立的局部误判。这里“是否合法”指的不是语法合法，而是<span style="background-color: #c0c0c0;">标签序列是否符合该任务定义下允许出现的邻接模式</span>。例如，在常见的 BIO 标注体系中， <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 表示 Begin，即实体开始； <span displaypfx="inline-" class="mathjax-container">\(I\)</span> 表示 Inside，即实体内部； <span displaypfx="inline-" class="mathjax-container">\(O\)</span> 表示 Outside，即不属于任何实体。也有一些任务使用 BIOES 标注体系，其中 <span displaypfx="inline-" class="mathjax-container">\(E\)</span> 表示 End， <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 表示 Single。与 BIO 相比，BIOES 会把实体结束位置和单字实体显式标出来，因此边界信息更细。于是，标签 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER\)</span> 表示一个人名实体的开始， <span displaypfx="inline-" class="mathjax-container">\(I\text{-}PER\)</span> 表示该人名实体的内部， <span displaypfx="inline-" class="mathjax-container">\(O\)</span> 表示不属于任何实体。于是， <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER\rightarrow I\text{-}PER\)</span> 是常见且合法的相邻转移， <span displaypfx="inline-" class="mathjax-container">\(O\rightarrow B\text{-}PER\)</span> 也合法；但 <span displaypfx="inline-" class="mathjax-container">\(O\rightarrow I\text{-}PER\)</span> 通常不合法，因为一个实体内部标签不能无缘无故直接开始，前面必须先有对应的开始标签。同样， <span displaypfx="inline-" class="mathjax-container">\(B\text{-}LOC\rightarrow I\text{-}PER\)</span> 这类“实体类型突然不一致”的连接也通常应被压低分数。</p>
<p>例如，在句子“张三 在 北京 工作”中，若任务要识别人名（PER）和地点（LOC），一个合理的 BIO 标注序列可能是“张三 / 在 / 北京 / 工作”对应 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER, O, B\text{-}LOC, O\)</span>。这里首先需要区分两件事：<span style="background-color: #c0c0c0;">某个位置更像哪一种实体类型</span>，以及<span style="background-color: #c0c0c0;">这些局部判断连起来是否构成一条合理的标签序列</span>。前者主要来自输入本身提供的证据，例如“张三”在词形上很像中文人名，“北京”本身强烈像地点名词，而“在 北京 工作”这种上下文也会继续加强“北京是地点”的判断。传统 CRF 会把这些信息写成特征函数，例如“当前词是否常见于人名词表”“当前词是否带有地名后缀”“左邻词是否是介词‘在’”“右邻词是否是动作词‘工作’”等；每个特征都会给某个候选标签加分或减分。</p>
<p>CRF 的作用是在这些局部类型证据之上，再做一次全局一致性的联合解码。换言之，实体类型 A 还是 B，并不是和 CRF 完全无关；但也不是由 CRF 凭空决定的。更准确地说，<span style="background-color: #c0c0c0;">实体类型的语义判断主要来自输入特征，CRF 负责把这些局部判断放到整条序列里统一协调</span>。例如，如果“北京”这个位置单看局部证据时，对 LOC 的分数高于 PER，那么 CRF 会倾向保留“地点”这一判断；但它还会进一步检查，当前位置前后的标签连接是否自然。如果某条候选路径把“张三”切成 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER, O\)</span>，或者把“北京”接成 <span displaypfx="inline-" class="mathjax-container">\(B\text{-}PER\rightarrow I\text{-}LOC\)</span>，即使某个局部位置的分数不低，整条路径仍会因为边界断裂或类型转移不一致而被整体压低。于是 CRF 做的不是单点分类，而是“局部类型打分 + 全局路径约束”的联合决策。</p>
<p>从工程实现上看，这个分工在不同年代的模型里表现形式不同。在传统 CRF 中，“局部类型打分”通常来自人工设计的特征模板，例如当前词、前后词、词性、字形、是否出现在人名词典或地名词典中；CRF 再把这些手工特征组合成整条序列的全局分数。在 BiLSTM-CRF 或 BERT-CRF 这类现代模型中，局部证据不再主要依赖手工模板，而是先由 BiLSTM 或 BERT 生成上下文化表示（Contextual Representation），再由线性层给出每个位置对各标签的局部分数，最后仍由 CRF 层负责建模标签转移和整条路径解码。也就是说，上游编码器主要回答“这个位置像什么类型”，CRF 层主要回答“这些位置判断拼在一起是否构成一条最合理的标签序列”。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-1.jpg"><img class="alignnone size-full wp-image-41263" src="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-1.jpg" alt="crf-1" width="1408" height="768" /></a></p>
<p>类似的全局打分思想也可以推广到依存句法分析（Dependency Parsing, DEP）这类结构化任务。句子中的每个词都需要找到自己的中心词（head），模型的目标不是孤立地决定“这个词连向谁”，而是评估整棵依存树是否合理。例如，在“她 喜欢 自然语言处理”中，“喜欢”通常更可能作为中心谓词，“她”依附到“喜欢”形成主谓关系，“自然语言处理”整体依附到“喜欢”形成宾语关系。若某个局部决策把“她”错误地连到“自然语言处理”，单看两个词的局部相似度未必很低，但放到整棵树的全局结构中就会显得不协调。CRF 的价值正体现在这里：它通过全局归一化和结构约束，偏好整体验证一致的输出结构，而不是一组彼此冲突的局部最优决策。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-2.jpg"><img class="alignnone size-full wp-image-41257" src="https://blog.gmem.cc/wp-content/uploads/2026/03/crf-2.jpg" alt="crf-2" width="1408" height="768" /></a></p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：适合结构化输出，能显式编码标签依赖。</li>
<li>局限：训练与推断成本高于普通独立分类器。</li>
<li>适用场景：序列标注、分词、命名实体识别等条件结构化预测任务。</li>
</ul>
<div class="blog_h2"><span class="graybg">近邻模型</span></div>
<p>这一类方法处理的核心问题是：在缺少可靠全局函数形式时，是否可以直接依赖“局部相似样本通常有相似输出”这一假设完成预测。近邻模型不急于学习一个显式参数化函数，而是把相似性度量（Similarity Metric）本身作为建模中心：先找邻居，再由邻居投票、平均或加权得到结果。</p>
<div class="blog_h3"><span class="graybg">K 近邻（KNN）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>K 近邻（K-Nearest Neighbors, KNN）用于分类与回归。给定一个待预测样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，它要根据训练集中与其最相似的样本来决定输出。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>KNN 的基本假设是局部平滑性（Local Smoothness）：在特征空间中彼此接近的样本，往往具有相近的标签或数值。它不显式学习参数化模型，训练集本身就是局部比较的参照集。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定距离函数 <span displaypfx="inline-" class="mathjax-container">\(d(\boldsymbol{x},\boldsymbol{x}')\)</span> 与邻居数 <span displaypfx="inline-" class="mathjax-container">\(K\)</span>，若 <span displaypfx="inline-" class="mathjax-container">\(N_K(\boldsymbol{x})\)</span> 表示 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个最近邻，则分类时可写为：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\mathrm{mode}\left(\{y_i:(\boldsymbol{x}_i,y_i)\in N_K(\boldsymbol{x})\}\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(N_K(\boldsymbol{x})\)</span> 是样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个最近邻集合，花括号中收集的是这些邻居的标签， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{mode}(\cdot)\)</span> 则返回出现次数最多的类别。也就是说，KNN 分类本质上就是“看最近的邻居们大多数是谁”。</p>
<p>回归时常取邻居标签平均：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\frac{1}{K}\sum_{(\boldsymbol{x}_i,y_i)\in N_K(\boldsymbol{x})}y_i\]</span>
<p>回归版 KNN 只是把“多数投票”换成“数值平均”。 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{K}\sum\)</span> 表示把最近 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个邻居的输出做均值，因此预测值会受到局部邻域中所有数值样本的共同影响。</p>
<p>若距离使用欧氏距离，则默认所有特征尺度可比，因此标准化（Standardization）通常是必要前处理。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>训练阶段几乎不做参数学习，只保存全部训练样本。</li>
<li>推断时计算待测样本到训练样本的距离。</li>
<li>选出最近的 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个样本。</li>
<li>分类取多数投票，回归取平均或距离加权平均。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在简单的手写数字识别中，如果一张新图片与训练集中大量“3”的图像都很接近，而与“8”的图像明显更远，那么 KNN 会依据局部邻域投票把它判为 3。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：概念简单、无需复杂训练、局部非线性表达能力强。</li>
<li>局限：推断成本高；对特征尺度和无关特征敏感；维度灾难会削弱距离判别力。</li>
<li>适用场景：中小规模数据、快速基线、基于相似度的简单分类与回归。</li>
</ul>
<div class="blog_h2"><span class="graybg">聚类</span></div>
<p>聚类处理的核心问题是：在没有标签的前提下，如何仅根据样本之间的几何关系、密度结构或层次关系，把数据自动分成若干组。这里不存在唯一正确的“簇”定义：K-Means 假设簇围绕中心分布，层次聚类强调多粒度组织结构，DBSCAN / HDBSCAN 则把簇理解为高密度连通区域。因此，聚类算法的选择本质上是在选择“什么样的结构应被视为同一类”。</p>
<div class="blog_h3"><span class="graybg">K-Means</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>K-Means 处理的是无监督聚类问题：给定一组没有标签的样本，希望把它们分成 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个簇，使同一簇内样本尽量接近，不同簇之间尽量分开。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>K-Means 用“每个簇由一个中心点代表”的方式近似数据分布。算法不断重复两件事：把样本分配给最近的中心；再用簇内样本均值更新中心。它本质上是在最小化簇内平方误差。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/k-means.jpg"><img class="alignnone size-full wp-image-41287" src="https://blog.gmem.cc/wp-content/uploads/2026/03/k-means.jpg" alt="k-means" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>目标函数为：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\{c_k\},\{z_i\}} \sum_{i=1}^{N} \|\boldsymbol{x}_i-c_{z_i}\|_2^2\]</span>
<p>这个目标里， <span displaypfx="inline-" class="mathjax-container">\(c_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个簇中心， <span displaypfx="inline-" class="mathjax-container">\(z_i\)</span> 表示样本 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 被分到哪个簇， <span displaypfx="inline-" class="mathjax-container">\(\|\boldsymbol{x}_i-c_{z_i}\|_2^2\)</span> 是样本到所属簇中心的平方距离。K-Means 想做的，就是让所有样本离各自中心都尽可能近。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(c_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个簇中心， <span displaypfx="inline-" class="mathjax-container">\(z_i\in\{1,\dots,K\}\)</span> 表示样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}_i\)</span> 属于哪个簇。固定簇分配时，最优中心是该簇样本均值：</p>
<span displaypfx="" class="mathjax-container">\[c_k=\frac{1}{|S_k|}\sum_{\boldsymbol{x}_i\in S_k} \boldsymbol{x}_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(S_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个簇里所有样本的集合， <span displaypfx="inline-" class="mathjax-container">\(|S_k|\)</span> 是该簇样本数。这个更新式说明簇中心就是簇内样本的算术平均，因此 K-Means 的“中心”确实是均值意义上的代表点。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>初始化 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个簇中心。</li>
<li>分配步骤：把每个样本分到最近中心。</li>
<li>更新步骤：用各簇样本均值更新中心。</li>
<li>重复迭代直到簇分配稳定或目标下降很小。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在用户分群中，若特征是消费频次与客单价，K-Means 往往会自动形成“高频低客单”“低频高客单”“中频中客单”等若干均值中心明确的群体。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：实现简单、可扩展、训练速度快。</li>
<li>局限：需要预先指定 <span displaypfx="inline-" class="mathjax-container">\(K\)</span>；对初始化与离群点敏感；不适合非凸簇。</li>
<li>适用场景：簇近似球形、需要快速聚类或作为预处理的任务。</li>
</ul>
<div class="blog_h3"><span class="graybg">层次聚类</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>层次聚类（Hierarchical Clustering）输出的是一个由粗到细的聚类层次结构，因此簇数可以在观察树状图后再决定。它适合需要观察“簇是如何逐步合并或拆分”的任务。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>凝聚式层次聚类（Agglomerative Clustering）从每个样本单独成簇开始，每一步合并当前最相近的两个簇；分裂式层次聚类则从一个大簇开始不断拆分。实践中更常见的是凝聚式版本。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/h-clustering.jpg"><img class="alignnone size-full wp-image-41293" src="https://blog.gmem.cc/wp-content/uploads/2026/03/h-clustering.jpg" alt="h-clustering" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>关键在于定义簇间距离。常见联接准则（Linkage Criteria）包括：</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{single}}(A,B)=\min_{\boldsymbol{x}\in A,\boldsymbol{y}\in B} d(\boldsymbol{x},\boldsymbol{y})\]</span>
<p>单链距离只看两簇之间最近的那一对点，因此它很容易把“靠得最近的局部桥梁”连起来，适合发现链式结构，但也更容易被噪声点串联。</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{complete}}(A,B)=\max_{\boldsymbol{x}\in A,\boldsymbol{y}\in B} d(\boldsymbol{x},\boldsymbol{y})\]</span>
<p>完全链距离只看两簇之间最远的那一对点，因此它会避免把跨度过大的簇合并到一起，更偏好紧凑、直径较小的簇。</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{average}}(A,B)=\frac{1}{|A||B|}\sum_{\boldsymbol{x}\in A,\boldsymbol{y}\in B} d(\boldsymbol{x},\boldsymbol{y})\]</span>
<p>平均链则对两簇之间所有点对的距离取平均。这里 <span displaypfx="inline-" class="mathjax-container">\(|A||B|\)</span> 是点对总数，因此它不只看最近点或最远点，而是综合考虑两簇整体的平均接近程度。</p>
<p>不同联接方式对应不同簇形偏好：单链更容易形成链式簇，完全链更偏向紧凑簇，平均链则居中。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>初始化每个样本为单独一个簇。</li>
<li>计算簇间距离矩阵。</li>
<li>反复合并距离最近的两个簇，并更新距离矩阵。</li>
<li>得到树状图（Dendrogram）后，在某个高度切开即可获得聚类结果。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在文档聚类中，层次聚类不仅能区分“体育”“财经”“科技”等大类，还能进一步展示每一大类内部的细粒度层级关系。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：无需预先指定簇数；可以输出多层次聚类结构。</li>
<li>局限：计算与存储成本较高；早期合并错误通常无法回退。</li>
<li>适用场景：中小规模数据、需要树状关系解释的聚类分析。</li>
</ul>
<div class="blog_h3"><span class="graybg">DBSCAN</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>DBSCAN（Density-Based Spatial Clustering of Applications with Noise）用于识别任意形状的高密度簇，并显式发现噪声点。它尤其适合处理非球形簇与含离群点数据。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>DBSCAN 通过局部密度定义簇。若一个点周围半径 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 内有足够多的邻居，则它是核心点（Core Point）；核心点可以把周围密度可达（Density-Reachable）的样本不断扩展成一个簇。既不够密、又不属于任何核心点邻域的样本被视为噪声。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/dbscan.jpg"><img class="alignnone size-full wp-image-41305" src="https://blog.gmem.cc/wp-content/uploads/2026/03/dbscan.jpg" alt="dbscan" width="1024" height="559" /></a></p>
<p>&nbsp;</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>记 <span displaypfx="inline-" class="mathjax-container">\(N_{\epsilon}(\boldsymbol{x})\)</span> 为点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 邻域。若：</p>
<span displaypfx="" class="mathjax-container">\[|N_{\epsilon}(\boldsymbol{x})|\ge \text{minPts}\]</span>
<p>判断核心点只需看两件事： <span displaypfx="inline-" class="mathjax-container">\(N_{\epsilon}(\boldsymbol{x})\)</span> 是点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 在半径 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 内的邻域集合， <span displaypfx="inline-" class="mathjax-container">\(|N_{\epsilon}(\boldsymbol{x})|\)</span> 是邻域里有多少点。只要这个数量不小于 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span>，就说明该点周围密度足够高，可以作为簇扩张的核心。</p>
<p>则 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 是核心点。若某点落在某个核心点邻域中但自身并非核心点，则为边界点（Border Point）；不属于任何簇的点为噪声（Noise）。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>遍历未访问样本，计算其 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 邻域。</li>
<li>若邻居数少于 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span>，则暂记为噪声或边界候选。</li>
<li>若为核心点，则以它为起点递归扩展所有密度可达的点。</li>
<li>重复直到所有样本都被标记。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在地理位置聚类中，餐馆、商圈、交通枢纽附近的点位通常形成形状复杂的密集区域。DBSCAN 可以识别这些非凸热点，同时把零散孤立点保留为噪声。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：无需预设簇数；可识别任意形状簇；对离群点鲁棒。</li>
<li>局限：对 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span> 敏感；难以同时适配不同密度簇。</li>
<li>适用场景：空间聚类、热点区域发现、非凸簇与带噪声数据。</li>
</ul>
<div class="blog_h3"><span class="graybg">HDBSCAN</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>HDBSCAN（Hierarchical DBSCAN）用于缓解 DBSCAN 的单一密度阈值问题。它面对的核心困难是：真实数据中的簇密度常常并不一致，用一组固定的 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\text{minPts}\)</span> 很难同时兼顾所有簇。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>HDBSCAN 会在多个密度尺度上构建层次结构，再从中选出稳定簇（Stable Clusters）作为结果。这让它比 DBSCAN 更适合处理密度差异显著的数据。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/hdbscan-intuition.png"><img class="alignnone size-full wp-image-41327" src="https://blog.gmem.cc/wp-content/uploads/2026/03/hdbscan-intuition.png" alt="hdbscan-intuition" width="3045" height="12714" /></a></p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定参数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，先定义核心距离（Core Distance）为点到其第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 近邻的距离，再定义互可达距离（Mutual Reachability Distance）：</p>
<span displaypfx="" class="mathjax-container">\[d_{\text{mreach},k}(\boldsymbol{x},\boldsymbol{y})=\max\big(\text{core}_k(\boldsymbol{x}),\text{core}_k(\boldsymbol{y}),d(\boldsymbol{x},\boldsymbol{y})\big)\]</span>
<p>互可达距离把三个量取最大值：点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的核心距离、点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}\)</span> 的核心距离，以及它们之间的原始距离。这样做的效果是：低密度区域的点会被“拉远”，从而更清楚地暴露不同密度簇之间的结构边界。</p>
<p>随后算法在互可达图上构建最小生成树（Minimum Spanning Tree），再转成聚类层次，并依据簇稳定性选择最终输出。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>计算所有点的核心距离。</li>
<li>基于互可达距离构造图并求最小生成树。</li>
<li>从图中得到随密度变化的层次聚类结构。</li>
<li>在压缩树（Condensed Tree）上选择稳定簇作为结果。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在用户行为嵌入空间中，有些兴趣群体很紧密，有些较松散。HDBSCAN 可以同时保留这两类簇，而不需要强行用同一个密度阈值描述它们。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：对多密度簇更稳健，仍保留噪声识别能力。</li>
<li>局限：实现更复杂，计算与内存开销通常高于 DBSCAN。</li>
<li>适用场景：嵌入聚类、用户分群、不同密度簇共存的数据。</li>
</ul>
<div class="blog_h2"><span class="graybg">升维</span></div>
<p>升维（Feature Expansion / Lifting）处理的问题，与降维正好互补。降维试图把高维表示压缩到更低维空间，以减少冗余、噪声与计算量；升维则试图把原始特征映射到一个更高维的表示空间，使原本难以表达、难以分离或难以拟合的结构，在新空间里变得更容易处理。它的目标不是“把维度变大本身”，而是<span style="background-color: #c0c0c0;">通过增加表示自由度，把非线性关系改写为更容易由简单模型处理的形式</span>。</p>
<div class="blog_h3"><span class="graybg">背景和问题定义</span></div>
<p>许多经典机器学习模型本体是线性的，例如线性回归、逻辑回归、线性支持向量机（Support Vector Machine, SVM）。它们直接在原始输入空间中学习一个线性决策函数或线性预测函数。若数据关系本身高度非线性，那么模型能力可能不足。升维的思路因此是：先把输入映射为一个更高维的新表示，再在新表示上使用线性模型。模型形式仍然简单，但由于工作空间改变了，整体表达能力会显著提升。</p>
<p>更一般地，若原始输入为 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\in \mathbb{R}^d\)</span>，则升维映射可写成：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\boldsymbol{x}}=\phi(\boldsymbol{x}),\qquad \phi: \mathbb{R}^d\to \mathbb{R}^D,\qquad D&gt;d\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 是原始输入； <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span> 是升维后的新特征； <span displaypfx="inline-" class="mathjax-container">\(\phi(\boldsymbol{x})\)</span> 是特征映射； <span displaypfx="inline-" class="mathjax-container">\(D&gt;d\)</span> 表示新的表示空间维度高于原空间维度。若后续模型写成 <span displaypfx="inline-" class="mathjax-container">\(f(\boldsymbol{x})=\boldsymbol{w}^\top \tilde{\boldsymbol{x}}+b\)</span>，那么它虽然在新空间中仍然是线性的，但在原始输入 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 上通常已经对应一个非线性函数。</p>
<div class="blog_h3"><span class="graybg">核心思想</span></div>
<p>升维的直观含义，是把原来“纠缠在一起”的关系展开。例如在二维平面里，某些数据可能无法被一条直线分开；但若把 <span displaypfx="inline-" class="mathjax-container">\((x_1,x_2)\)</span> 映射成 <span displaypfx="inline-" class="mathjax-container">\((x_1,x_2,x_1^2,x_2^2,x_1x_2)\)</span> 这样的更高维特征，原本的弯曲边界就可能对应高维空间中的一个超平面。于是复杂性并没有消失，而是被转移到了特征映射 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}=\phi(\boldsymbol{x})\)</span> 上。</p>
<p>这也是经典机器学习里一个非常常见的套路：<span style="background-color: #c0c0c0;">先做特征构造或特征展开，再用结构简单、优化稳定的线性模型</span>。因此，升维常常并不以“升维”这个名字出现，而是以多项式特征、基函数展开、核方法、one-hot 编码、N-gram 稀疏特征、随机特征等形式出现。</p>
<div class="blog_h3"><span class="graybg">公式和详细解释</span></div>
<p>升维后的线性模型通常写成：</p>
<span displaypfx="" class="mathjax-container">\[f(\boldsymbol{x})=\boldsymbol{w}^\top \tilde{\boldsymbol{x}}+b\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span> 是由原始输入 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 构造出来的高维特征向量； <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\in \mathbb{R}^D\)</span> 是新空间中的参数； <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是偏置。关键点在于：模型在 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span> 空间里仍然是线性的，但如果 <span displaypfx="inline-" class="mathjax-container">\(\phi(\boldsymbol{x})\)</span> 含有平方项、交叉项、基函数或核映射，那么 <span displaypfx="inline-" class="mathjax-container">\(f(\boldsymbol{x})\)</span> 相对于原始输入 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 就会表现出非线性。</p>
<p>以二次多项式特征为例，若原始输入为 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}=(x_1,x_2)\)</span>，则可构造：</p>
<span displaypfx="" class="mathjax-container">\[\tilde{\boldsymbol{x}}=(x_1,x_2,x_1^2,x_2^2,x_1x_2)\]</span>
<p>此时线性模型</p>
<span displaypfx="" class="mathjax-container">\[f(\boldsymbol{x})=w_1x_1+w_2x_2+w_3x_1^2+w_4x_2^2+w_5x_1x_2+b\]</span>
<p>在参数上仍然是线性的，但在输入上已经能够表达二次曲面或二次决策边界。这正是升维最核心的数学作用：<span style="background-color: #c0c0c0;">把原空间中的非线性关系，改写成高维空间中的线性关系</span>。</p>
<div class="blog_h3"><span class="graybg">常见升维方式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">方式</td>
<td style="text-align: center;">升维形式</td>
<td style="text-align: center;">核心作用</td>
<td style="text-align: center;">典型场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>多项式特征</td>
<td>加入平方项、立方项、交叉项</td>
<td>把低阶线性模型扩展为可拟合非线性关系</td>
<td>线性回归、逻辑回归、线性分类器</td>
</tr>
<tr>
<td>基函数展开</td>
<td>高斯基、样条基、傅里叶基等</td>
<td>用一组预定义函数把局部或周期结构显式展开</td>
<td>回归、广义加性模型、核近似</td>
</tr>
<tr>
<td>核方法</td>
<td>通过核函数隐式映射到高维甚至无限维空间</td>
<td>在不显式展开特征的情况下提升可分性</td>
<td>SVM、核岭回归、核 PCA</td>
</tr>
<tr>
<td>One-hot 编码</td>
<td>把离散类别映射为高维稀疏向量</td>
<td>让类别变量进入线性模型并保持类别独立性</td>
<td>表格特征、推荐、广告、点击率预估</td>
</tr>
<tr>
<td>N-gram / 词袋</td>
<td>把文本映射为高维稀疏词项空间</td>
<td>显式展开局部共现与组合模式</td>
<td>传统文本分类、检索、朴素贝叶斯、线性 SVM</td>
</tr>
<tr>
<td>随机特征</td>
<td>用随机映射近似某些核空间</td>
<td>在显式高维表示与核方法之间做折中</td>
<td>Random Fourier Features、核近似</td>
</tr>
</tbody>
</table>
<p>其中，核方法是经典机器学习里最有代表性的升维思想。以 SVM 为例，若定义特征映射 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}=\phi(\boldsymbol{x})\)</span>，则线性分类器可写成 <span displaypfx="inline-" class="mathjax-container">\(f(\boldsymbol{x})=\boldsymbol{w}^\top \tilde{\boldsymbol{x}}+b\)</span>。核技巧（Kernel Trick）并不显式构造 <span displaypfx="inline-" class="mathjax-container">\(\tilde{\boldsymbol{x}}\)</span>，而是直接通过核函数</p>
<span displaypfx="" class="mathjax-container">\[K(\boldsymbol{x},\boldsymbol{x}')=\phi(\boldsymbol{x})^\top \phi(\boldsymbol{x}')\]</span>
<p>计算升维后特征空间中的内积。这样既保留了高维映射的表达力，又避免了显式展开到巨大维度的代价。也正因为如此，SVM 是很多人最先感受到“升维威力”的经典模型。</p>
<div class="blog_h3"><span class="graybg">应用实例</span></div>
<p>在圆形可分但线性不可分的数据中，原始二维平面上一条直线无法把内圈与外圈分开；但若加入半径相关的二次特征，例如 <span displaypfx="inline-" class="mathjax-container">\(x_1^2+x_2^2\)</span>，则问题可以转写为更高维空间中的线性分离。文本任务里，N-gram 稀疏特征同样是一种典型升维：原始句子是离散序列，经过词袋或 N-gram 展开后，就变成了数万维甚至更高维的稀疏向量，随后再交给逻辑回归、朴素贝叶斯或线性 SVM 处理。</p>
<p>表格任务中的类别变量处理也体现了同样思想。一个城市字段看起来只是一个离散取值，但 one-hot 编码后，它会被展开成一个高维稀疏向量，使模型能够为每个类别学习独立参数。推荐系统、广告点击率预估、工业风控中的大规模离散特征，长期都高度依赖这种升维方式。</p>
<div class="blog_h3"><span class="graybg">为什么经典机器学习更谨慎地升维</span></div>
<p>经典机器学习也会使用升维，但通常更谨慎。原因在于，维度一旦上去，过拟合风险、存储开销与计算开销都会迅速增加，这就是维度灾难（Curse of Dimensionality）的典型体现。于是经典方法常把“升维”与“控制复杂度”配套使用：一边通过特征展开提升表达能力，一边用正则化（Regularization）、特征选择（Feature Selection）或后续降维来抑制过拟合。</p>
<p>因此，在经典机器学习里，常见工作流并不是“只升维”或“只降维”，而是两者交替配合：先做有针对性的特征展开，让结构变得更容易表达；再通过正则化、筛选或压缩保留真正有效的部分。升维是在展开表达能力，降维是在压缩冗余信息，它们并不是对立操作，而是围绕表示空间做的两种互补控制。</p>
<div class="blog_h3"><span class="graybg">维度灾难</span></div>
<p>维度灾难（Curse of Dimensionality）指的是：当特征维度不断升高时，许多在低维空间中直观、有效的统计与几何规律会迅速恶化，导致数据需求、计算成本与建模难度同时上升。它不是某一个单独问题，而是一组高维效应的统称，包括样本空间体积指数级膨胀、样本变得极其稀疏、局部邻域难以稳定估计、距离与密度统计的判别力下降，以及模型更容易用复杂边界去记忆训练集。</p>
<p>其中一个最重要的现象确实是：<span style="background-color: #c0c0c0;">高维里距离往往会变得不再像低维那样有区分力</span>。直观地说，当维度很多时，样本之间的最近距离和最远距离可能越来越接近，导致“谁是真正近邻”这件事变得没那么清晰。依赖距离或局部邻域的方法，例如 KNN、聚类、核密度估计、局部异常检测等，往往会因此退化。这也是为什么高维数据上，距离度量、标准化、特征筛选和嵌入学习会变得格外重要。</p>
<p>但“高维更容易过拟合”并不只因为距离失去意义。更根本的原因是：当维度升高后，表示空间的自由度与可容纳的划分方式急剧增加，而训练样本相对于整个空间显得越来越稀疏。模型于是更容易找到一些只对训练集成立、却不能推广到新样本的偶然边界或偶然相关性。换句话说，<span style="background-color: #c0c0c0;">距离退化主要伤害的是邻域、相似度与密度估计；过拟合风险上升则更直接地来自空间稀疏化、参数自由度增加和有效样本覆盖不足</span>。</p>
<p>这也是为什么经典机器学习在做升维时往往必须同步引入约束：一方面用特征展开提升表达能力，另一方面通过正则化（Regularization）、特征选择（Feature Selection）、降维（Dimensionality Reduction）或更强的数据先验，限制模型不要把高维空间当成“背题空间”。因此，维度灾难并不是在说“高维一定不好”，而是在提醒：<span style="background-color: #c0c0c0;">维度每增加一层，模型就需要更多数据、更强归纳偏置和更谨慎的复杂度控制，才能让新增维度真正转化为有效表达能力</span>。</p>
<div class="blog_h3"><span class="graybg">和深度学习中的升维关系</span></div>
<p>深度学习中的很多操作，本质上也在做升维。Transformer 的前馈网络（Feed-Forward Network, FFN / MLP）常把 <span displaypfx="inline-" class="mathjax-container">\(d_{\text{model}}\)</span> 升到更大的 <span displaypfx="inline-" class="mathjax-container">\(d_{\text{ff}}\)</span>，再降回原维度；这与经典机器学习里“先展开、再用简单变换处理”的思想是一脉相承的。差别在于，经典方法里的升维往往是人工设计或固定形式的，而深度学习中的升维通常是可学习的线性投影与非线性组合。</p>
<p>因此，若从表示学习角度看，升维在机器学习中并不罕见。SVM 的核空间、逻辑回归的多项式特征、文本的 N-gram 稀疏向量、推荐系统的 one-hot 离散展开，以及 Transformer MLP 中的中间维度扩张，本质上都属于同一条主线：<span style="background-color: #c0c0c0;">把原本难以处理的关系，展开到一个更容易表达与分离的表示空间里</span>。</p>
<div class="blog_h2"><span class="graybg">降维</span></div>
<p>降维处理的核心问题是：高维数据往往包含冗余、相关性与噪声，既增加计算成本，也削弱可视化与建模稳定性。目标不是简单“删维度”，而是在压缩表示的同时尽量保留有用结构——这个“有用”可以是方差、类别可分性、局部邻域、全局流形，具体取决于所采用的方法。</p>
<div class="blog_h3"><span class="graybg">主成分分析（PCA）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>主成分分析（Principal Component Analysis, PCA）处理的是线性降维问题：在尽量保留数据主要变化信息的前提下，把高维样本映射到更低维空间。它常用于压缩维度、去相关、可视化与噪声抑制。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/pca.jpg"><img class="alignnone size-full wp-image-41347" src="https://blog.gmem.cc/wp-content/uploads/2026/03/pca.jpg" alt="pca" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>PCA 用方差（Variance）近似衡量信息量。若数据在某个方向上的投影变化很大，说明该方向承载了更多结构；于是 PCA 选择能最大化投影方差的一组正交方向作为新的表示基底。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定中心化后的数据矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{X}_c\in\mathbb{R}^{N\times d}\)</span>，协方差矩阵为：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{\Sigma}=\frac{1}{N}\boldsymbol{X}_c^\top \boldsymbol{X}_c\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{X}_c\)</span> 是已经减去均值后的数据矩阵， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{X}_c^\top \boldsymbol{X}_c\)</span> 汇总了各个特征之间如何共同变化；再除以 <span displaypfx="inline-" class="mathjax-container">\(N\)</span>，就得到平均意义下的协方差矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\Sigma}\)</span>。PCA 正是从这个矩阵里找“变化最大的方向”。</p>
<p>第一主成分对应的优化问题为：</p>
<span displaypfx="" class="mathjax-container">\[\max_{\boldsymbol{u}} \quad \boldsymbol{u}^\top \boldsymbol{\Sigma}\boldsymbol{u} \quad \text{s.t.} \quad \|\boldsymbol{u}\|_2=1\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{u}\)</span> 是候选投影方向， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{u}^\top \boldsymbol{\Sigma}\boldsymbol{u}\)</span> 表示数据投影到该方向后的方差，约束 <span displaypfx="inline-" class="mathjax-container">\(\|\boldsymbol{u}\|_2=1\)</span> 则防止通过把向量无限放大来虚增方差。于是这个优化问题真正寻找的是“单位长度下最能保留变化信息的方向”。</p>
<p>其解是协方差矩阵最大特征值对应的特征向量。取前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个主成分组成矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{U}_k\)</span> 后，样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 的低维表示为：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{z}=\boldsymbol{U}_k^\top(\boldsymbol{x}-\boldsymbol{\mu})\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}\)</span> 是原始数据均值， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}-\boldsymbol{\mu}\)</span> 先把样本中心化， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{U}_k\)</span> 由前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个主成分方向组成，最终 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{z}\)</span> 就是样本在这组主方向上的低维坐标。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>对数据做中心化，必要时再做标准化。</li>
<li>计算协方差矩阵或直接对数据矩阵做奇异值分解（SVD）。</li>
<li>取前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个主成分方向。</li>
<li>把数据投影到这些方向上得到低维表示。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在人脸图像压缩中，大量像素变化往往由少数全局因素驱动。PCA 可以用少量主成分保留大部分有效变化，从而显著降低特征维度。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：线性、稳定、可解释，常用作预处理和可视化。</li>
<li>局限：只能捕捉线性结构；对异常值敏感。</li>
<li>适用场景：线性降维、特征压缩、去相关与噪声过滤。</li>
</ul>
<div class="blog_h3"><span class="graybg">线性判别分析（LDA）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>线性判别分析（Linear Discriminant Analysis, LDA）用于监督降维与分类。与 PCA 只看输入分布不同，LDA 利用类别标签寻找一个投影空间，使同类样本尽量聚集、异类样本尽量分开。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/lda.jpg"><img class="alignnone size-full wp-image-41349" src="https://blog.gmem.cc/wp-content/uploads/2026/03/lda.jpg" alt="lda" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>LDA 同时考虑类内散度（Within-class Scatter）和类间散度（Between-class Scatter）。好的投影方向应当让类间距离大、类内波动小，因此它优化的核心是判别性。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>定义类内散度矩阵：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{S}_W=\sum_{k=1}^{C}\sum_{\boldsymbol{x}_i\in \mathcal{C}_k}(\boldsymbol{x}_i-\boldsymbol{\mu}_k)(\boldsymbol{x}_i-\boldsymbol{\mu}_k)^\top\]</span>
<p>类内散度矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_W\)</span> 统计的是“同一类内部有多分散”。 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{C}_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 类的样本集合， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}_k\)</span> 是该类均值。若类内样本围绕各自均值分布得很紧， <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_W\)</span> 就会较小。</p>
<p>类间散度矩阵：</p>
<span displaypfx="" class="mathjax-container">\[\boldsymbol{S}_B=\sum_{k=1}^{C}N_k(\boldsymbol{\mu}_k-\boldsymbol{\mu})(\boldsymbol{\mu}_k-\boldsymbol{\mu})^\top\]</span>
<p>类间散度矩阵 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_B\)</span> 统计的是“各类中心彼此有多分开”。其中 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\mu}\)</span> 是全局均值， <span displaypfx="inline-" class="mathjax-container">\(N_k\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 类样本数，因此样本多的大类会对类间结构贡献更大权重。</p>
<p>LDA 寻找投影向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 使 Fisher 判别准则最大：</p>
<span displaypfx="" class="mathjax-container">\[J(\boldsymbol{w})=\frac{\boldsymbol{w}^\top \boldsymbol{S}_B \boldsymbol{w}}{\boldsymbol{w}^\top \boldsymbol{S}_W \boldsymbol{w}}\]</span>
<p>Fisher 准则的分子衡量“投影后类间有多分开”，分母衡量“投影后类内有多混在一起”。因此 <span displaypfx="inline-" class="mathjax-container">\(J(\boldsymbol{w})\)</span> 越大，说明方向 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 越有助于把不同类别拉开、同时保持同类紧凑。</p>
<p>该问题可转化为广义特征值问题 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{S}_B\boldsymbol{w}=\lambda \boldsymbol{S}_W\boldsymbol{w}\)</span>。对 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 类问题，最多能得到 <span displaypfx="inline-" class="mathjax-container">\(C-1\)</span> 个有效判别方向。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>根据标签统计每一类的均值与全局均值。</li>
<li>构造类内散度矩阵和类间散度矩阵。</li>
<li>求解广义特征值问题，选择主要判别方向。</li>
<li>将样本投影到低维判别子空间中做分类或可视化。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在手写数字分类中，LDA 关注的是“哪些方向最有助于把不同数字分开”，因此在有标签监督时，它往往比只最大化总体方差的 PCA 更适合分类前降维。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：利用标签信息，投影方向更有判别性。</li>
<li>局限：线性假设较强；类内散度矩阵可能奇异。</li>
<li>适用场景：有标签监督的降维、分类前特征压缩、判别性可视化。</li>
</ul>
<div class="blog_h3"><span class="graybg">t-SNE</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>t-SNE（t-distributed Stochastic Neighbor Embedding）主要用于二维或三维可视化。它主要关注如何在低维图上尽量保留高维空间中的局部邻域关系。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>t-SNE 把“样本彼此接近”转写为概率相似度：在高维空间中，接近的样本应当有较大概率互为邻居；在低维嵌入中，也希望这种邻近关系继续成立。优化的目标是让两种邻域概率分布尽可能接近。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>在高维空间中，先定义邻域概率 <span displaypfx="inline-" class="mathjax-container">\(p_{ij}\)</span>；在低维空间中，对嵌入点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}_i\)</span> 定义：</p>
<span displaypfx="" class="mathjax-container">\[q_{ij}=\frac{(1+\|\boldsymbol{y}_i-\boldsymbol{y}_j\|_2^2)^{-1}}{\sum_{a\ne b}(1+\|\boldsymbol{y}_a-\boldsymbol{y}_b\|_2^2)^{-1}}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}_i\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{y}_j\)</span> 是低维嵌入坐标，分子把距离越近的点赋予越大的相似度，分母则把所有点对的相似度加总起来做归一化，因此 <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span> 可以被解释成低维空间里的邻域概率。</p>
<p>t-SNE 最小化高维邻域分布与低维邻域分布之间的 KL 散度：</p>
<span displaypfx="" class="mathjax-container">\[\text{KL}(P\|Q)=\sum_{i\ne j} p_{ij} \log \frac{p_{ij}}{q_{ij}}\]</span>
<p>KL 散度衡量的是“低维邻域分布 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 与高维邻域分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 相差多少”。若某对样本在高维里很近，即 <span displaypfx="inline-" class="mathjax-container">\(p_{ij}\)</span> 很大，但在低维里被拉得太开， <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span> 就会过小，从而产生较大惩罚。</p>
<p>低维中采用重尾 Student-t 分布，主要是为了缓解拥挤问题（Crowding Problem），使远处点更容易被拉开。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>根据高维距离计算样本间的邻域概率。</li>
<li>随机初始化低维坐标。</li>
<li>计算低维相似度 <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span>。</li>
<li>通过梯度下降最小化 KL 散度并更新低维坐标。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在词向量或图像嵌入可视化中，t-SNE 常把语义相近的样本压到局部团簇中，从而帮助研究者直观看到表示空间里是否出现了合理分群。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：二维可视化效果强，局部邻域保持能力好。</li>
<li>局限：全局距离与簇间相对位置不稳定；对超参数和随机初始化敏感。</li>
<li>适用场景：嵌入可视化、表示质量诊断、探索性数据分析。</li>
</ul>
<div class="blog_h3"><span class="graybg">UMAP</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>UMAP（Uniform Manifold Approximation and Projection）同样用于低维可视化与非线性降维。它面对的任务与 t-SNE 类似，但更强调在保留局部结构的同时，尽量维持一定的全局几何关系，并提升速度与可扩展性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/umap.jpg"><img class="alignnone size-full wp-image-41351" src="https://blog.gmem.cc/wp-content/uploads/2026/03/umap.jpg" alt="umap" width="1024" height="559" /></a></p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>UMAP 先在高维空间构建一个加权近邻图，把数据视为流形（Manifold）上的离散采样；随后在低维空间中寻找一张新图，使低维图与高维图的模糊连通结构尽量一致。它本质上是在匹配两张图的连通结构，而不只是在匹配两组欧氏距离。</p>
<p>这里的流形可以先从一个标准定义理解：设样本位于高维空间 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^D\)</span> 中，若它们实际上集中在某个低维集合 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{M}\subseteq\mathbb{R}^D\)</span> 附近，并且对 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{M}\)</span> 上任一点，都能在一个足够小的邻域内用 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维坐标平滑描述，其中 <span displaypfx="inline-" class="mathjax-container">\(d\ll D\)</span>，那么这个集合就可以看作一个 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 维流形。流形的关键性质不是“它弯不弯”，而是<span style="background-color: #c0c0c0;">它在局部看起来像低维欧氏空间，在全局上则可以嵌入到更高维空间并发生弯曲、卷曲或拉伸</span>。</p>
<p>在数据分析里，这个概念表示：虽然原始特征有很多维，但样本并没有真正填满整个高维空间，而是分布在某种低维结构附近。例如一组人脸图像在像素空间里维度极高，但由姿态、光照、表情等少数潜在因素变化时，样本往往只覆盖其中一个低维子区域。UMAP 的出发点正是：若数据主要几何结构由这个低维流形决定，那么局部近邻关系比“全局欧氏直线距离”更能反映真实结构。</p>
<p>“离散采样”则表示：真实流形 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{M}\)</span> 本身是连续对象，但我们手里只有有限个样本点 <span displaypfx="inline-" class="mathjax-container">\(\{x_i\}_{i=1}^N\)</span>。UMAP 通过近邻图近似这些点在流形上的局部连通关系，再在低维空间中寻找一组坐标 <span displaypfx="inline-" class="mathjax-container">\(\{y_i\}_{i=1}^N\)</span>，使这种局部连通关系尽量被保留下来。因此它不是直接恢复整张流形，而是在有限样本层面恢复流形的邻域结构。</p>
<p>与 t-SNE 相比，UMAP 更强调“样本来自某个低维流形，并且近邻图是在近似这个流形的局部连通结构”；t-SNE 的核心对象则更偏向“高维邻域概率分布与低维邻域概率分布的匹配”。两者都重视局部关系，但 UMAP 的表述更几何化，t-SNE 的表述更概率化。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>若高维图边权为 <span displaypfx="inline-" class="mathjax-container">\(p_{ij}\)</span>，低维图边权常写为：</p>
<span displaypfx="" class="mathjax-container">\[q_{ij}=\frac{1}{1+a\|\boldsymbol{y}_i-\boldsymbol{y}_j\|_2^{2b}}\]</span>
<p>UMAP 用这个函数把低维距离转换成连通强度。 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 控制曲线形状：距离很近时 <span displaypfx="inline-" class="mathjax-container">\(q_{ij}\)</span> 接近 1，距离变远时迅速衰减到接近 0，因此它可以把“近邻关系”转写成平滑的图边权。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(a,b\)</span> 决定低维距离与连接强度的映射形状。UMAP 的优化目标通常是高维图与低维图之间的交叉熵（Cross-Entropy）：既鼓励高维中相连的点在低维中也靠近，也鼓励高维中不相连的点在低维中适当分开。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>计算近邻图，并为边赋予模糊连通权重。</li>
<li>初始化低维坐标。</li>
<li>通过随机优化最小化高维图与低维图之间的交叉熵。</li>
<li>得到二维或三维嵌入用于可视化或下游分析。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在单细胞测序、文本嵌入或推荐向量分析中，UMAP 常被用来把高维表示映射到二维平面，从而观察群体结构、类别分布与异常点。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：通常比 t-SNE 更快，较好兼顾局部与部分全局结构。</li>
<li>局限：嵌入结果仍依赖超参数与随机种子；二维距离不能机械等同于原空间距离。</li>
<li>适用场景：大规模嵌入可视化、非线性降维、聚类前的低维表示。</li>
</ul>
<div class="blog_h2"><span class="graybg">异常检测</span></div>
<p>异常检测处理的核心问题是：正常样本通常大量存在，而异常样本稀少、形态多变、甚至在训练阶段根本拿不到完整标签。模型因此不再主要学习“类别之间如何区分”，而是学习“什么算正常”以及“偏离正常结构有多严重”。不同方法对“异常”的定义并不相同：有的依赖隔离难易度，有的依赖局部密度，有的学习正常区域边界。</p>
<div class="blog_h3"><span class="graybg">什么叫异常</span></div>
<p>在业务语境里，“异常”并不等于“数值特别大”或“离均值很远”。更准确地说，异常是<span style="background-color: #c0c0c0;">相对于当前业务规则、历史模式或同类群体而言，不应当出现、很少出现、或者一旦出现就值得额外关注的样本</span>。因此异常是一个“相对概念”，必须依赖参照系：相对于谁、在哪个时间段、在什么上下文里、以什么代价衡量。</p>
<p>例如，单笔消费 5000 元在全国范围内不一定异常，但若这位用户平时只在本地便利店做几十元交易，而这次交易突然发生在异地、高风险设备、深夜时段，并伴随支付习惯突变，那么它在风控上就可能是异常。类似地，服务器 CPU 使用率 85% 在大促期间可能是正常负载，在凌晨低峰却可能意味着任务堆积；工厂传感器温度轻微升高若同时伴随振动模式变化，也可能预示故障正在形成。</p>
<p>这说明业务上的异常通常至少包含三类含义。第一类是<span style="background-color: #c0c0c0;">统计稀有</span>：样本落在低概率区域。第二类是<span style="background-color: #c0c0c0;">行为失配</span>：它与该对象自己的历史模式不一致。第三类是<span style="background-color: #c0c0c0;">群体失配</span>：它在全局上不一定极端，但相对于同类群体显著不同。异常检测算法的差异，本质上就在于它们分别更擅长刻画哪一种“失配”。</p>
<p>因此，做异常检测时首先要回答的不是“用哪种模型”，而是“业务到底把什么视为异常”。若异常意味着“明显稀少且容易与大部队分开”，隔离式方法更合适；若异常意味着“在本地邻域里显得稀疏”，应优先考虑密度比较；若正常样本边界清楚、异常样本类型杂乱，则更适合只学习正常区域。算法不是在定义业务，而是在实现业务已经确定的异常标准。</p>
<div class="blog_h3"><span class="graybg">孤立森林（Isolation Forest）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>孤立森林（Isolation Forest）用于无监督异常检测：在没有可靠异常标签时，仅根据数据分布本身识别“容易被孤立”的异常样本。它尤其适合高维表格数据与大规模检测场景。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>孤立森林利用一个非常直接的直觉：异常点通常更稀少、更孤立，因此在随机切分下更容易被提早隔离。路径越短，越可能是异常；路径越长，越像处于正常群体内部。若把正常样本理解为“扎堆生活在高密度区域里的大群体”，那么它们往往需要经过很多次随机切割才会被单独分离出来；而那些落在边缘、稀疏区域、或与主体分布明显脱节的样本，只需少量切分就会被单独留在某个叶节点里。</p>
<p>这种思路和距离或密度方法非常不同。K 近邻（K-Nearest Neighbors, KNN）或局部异常因子（Local Outlier Factor, LOF）会显式比较“离别人有多远”或“局部密度有多低”；孤立森林则直接把异常检测改写成一个更具操作性的判据：<span style="background-color: #c0c0c0;">一个样本在随机树里平均多快会被单独隔开</span>。因此它并不先估计复杂的概率分布，也不依赖全局距离结构，而是用“隔离难易度”来近似刻画异常性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.png"><img class="alignnone size-full wp-image-41363" src="https://blog.gmem.cc/wp-content/uploads/2026/03/random-forest.png" alt="random-forest" width="1600" height="1308" /></a></p>
<div class="blog_h4"><span class="graybg">图示解读</span></div>
<p>图中的示意数据由两个主要正常簇和若干离散分布的异常点构成。背景等高线展示的是孤立森林学到的“异常分数地形”：颜色越深，表示该区域样本平均路径长度越长，更接近模型眼中的正常高密度区域；颜色越浅，表示样本更容易在随机切分中被提早隔离，因此更接近潜在异常区域。</p>
<p>图中圆形点对应被模型判为正常的样本，它们主要聚集在两个深色中心附近；叉号对应被模型判为异常的样本，它们更多分布在边缘、簇间空白带或浅色区域。这种可视化非常适合帮助理解孤立森林的工作方式：它并不是在画一条严格的几何边界，而是在表达“哪里更容易被随机树迅速切出来”。因此，等高线的深浅更接近一种“隔离难度地图”，而不是传统分类器意义上的硬分类边界。</p>
<div class="blog_h4"><span class="graybg">隔离树是什么</span></div>
<p>隔离树（Isolation Tree, iTree）可以理解成一棵专门为了“把样本逐步切开”而构造的随机二叉树。它与决策树（Decision Tree）在外形上相似，但目标完全不同：决策树是在找最有区分力的切分规则，隔离树则故意随机选择一个特征，再在该特征当前取值范围内随机选择一个切分点，把样本递归分到左右子节点。</p>
<p>设当前节点包含样本集合 <span displaypfx="inline-" class="mathjax-container">\(S\)</span>，随机选到的特征为第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 维，其切分阈值为 <span displaypfx="inline-" class="mathjax-container">\(\tau\)</span>，则一次切分可写成</p>
<span displaypfx="" class="mathjax-container">\[S_{\mathrm{left}}=\{\boldsymbol{x}\in S\mid x_j&lt;\tau\},\qquad S_{\mathrm{right}}=\{\boldsymbol{x}\in S\mid x_j\ge \tau\}\]</span>
<p>递归继续进行，直到某个节点只剩下 1 个样本，或所有样本在当前节点上已经无法再被有效区分。对一个本来就远离主体、所在区域又很稀疏的样本而言，随机切分往往只需要很少几步就能把它单独留在某个叶节点中；而对处在高密度正常群体内部的样本，通常要经过更多次切分才能被单独隔离。隔离树因此并不显式估计概率密度，而是把“异常”转写为“被随机切分提早单独分离”的难易度。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>对样本 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span>，记它在一棵隔离树中的路径长度为 <span displaypfx="inline-" class="mathjax-container">\(h(\boldsymbol{x})\)</span>。在多棵树上取平均路径长度 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[h(\boldsymbol{x})]\)</span> 后，异常分数定义为：</p>
<span displaypfx="" class="mathjax-container">\[s(\boldsymbol{x},n)=2^{-\mathbb{E}[h(\boldsymbol{x})]/c(n)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(h(\boldsymbol{x})\)</span> 是样本在一棵树里被隔离所需的路径长度， <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[h(\boldsymbol{x})]\)</span> 是在整片森林中的平均值， <span displaypfx="inline-" class="mathjax-container">\(c(n)\)</span> 是针对样本规模 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 的归一化常数。路径越短，指数里的值越小，异常分数 <span displaypfx="inline-" class="mathjax-container">\(s(\boldsymbol{x},n)\)</span> 就越接近 1。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(c(n)\)</span> 是平均路径长度的归一化常数，用来把不同样本规模下的路径长度放到可比较的尺度上。它常写为 <span displaypfx="inline-" class="mathjax-container">\(c(n)=2H_{n-1}-2(n-1)/n\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(H_{n-1}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(n-1\)</span> 个调和数（Harmonic Number）。若某点明显比普通样本更早被切分隔离，则其平均路径长度更小，异常分数更接近 1。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>从训练集中随机抽取多个子样本。</li>
<li>为每个子样本构建随机隔离树。</li>
<li>对待测点计算其在所有树中的平均路径长度。</li>
<li>将平均路径长度映射为异常分数。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在交易风控中，若某条交易在金额、时间、地点、设备等多个维度上都明显偏离正常模式，它往往能在随机切分下被较早隔离出来，因此会获得更高异常分数。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：无需显式估计密度，也不依赖两两距离矩阵；因此在大规模数据上通常比基于邻域或密度的方法更高效。</li>
<li>优点：对高维表格数据较友好，对随机噪声通常也有较强鲁棒性，因为最终判断来自多棵随机树上的平均隔离行为，而不是某一次局部切分。</li>
<li>局限：对特征编码和特征尺度的业务含义仍然敏感；若异常与正常高度混叠、或者异常本身并不更容易被切开，隔离优势会下降。</li>
<li>适用场景：风控、日志异常、设备故障、指标监控等无监督异常检测，尤其适合作为高维表格场景中的强基线。</li>
</ul>
<div class="blog_h3"><span class="graybg">局部异常因子（LOF）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>局部异常因子（Local Outlier Factor, LOF）主要解决“全局上不远，但在局部邻域中显著稀疏”的异常检测问题。当数据不同区域密度差异很大时，只看全局距离通常不够稳定。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>LOF 比较的是“这个点相对于其邻居是否更稀疏”。若一个点周围的局部密度显著低于邻居自己的局部密度，则它更像局部异常。它识别的不是“全局上最远的点”，而是<span style="background-color: #c0c0c0;">相对于自己所在局部环境显得不协调的点</span>。因此，当不同区域本来就有不同密度时，LOF 往往比只看全局距离的方法更稳。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/lof.png"><img class="alignnone size-full wp-image-41371" src="https://blog.gmem.cc/wp-content/uploads/2026/03/lof.png" alt="lof" width="1000" height="800" /></a></p>
<div class="blog_h4"><span class="graybg">图示解读</span></div>
<p>图中的实心圆点表示样本点，围绕样本点的空心圆圈大小表示该点的 LOF 分数大小。圆圈越小，说明该点的局部密度与其邻居相近，更像正常簇中的内部样本；圆圈越大，说明该点所在位置相对于邻居显得更稀疏，因此更可能是局部异常点。</p>
<p>这张图直观展示了 LOF 的核心判断方式：它并不先问“这个点离全局中心有多远”，而是先问“这个点和自己周围那一圈邻居相比，是不是显得过于稀疏”。因此，大圆圈对应的未必是全局最远的点，而更可能是那些<span style="background-color: #c0c0c0;">周围邻居仍然较密、但它自己明显脱离了局部密度水平</span>的点。</p>
<p>若把这张示意图看作两个高密度簇与少量随机噪声点的组合，则邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 决定了算法观察“局部环境”的尺度。 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 较小时，模型更敏感于非常局部的扰动； <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 较大时，密度比较更平滑，但也可能削弱对细粒度异常的敏感性。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>给定邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，设 <span displaypfx="inline-" class="mathjax-container">\(N_k(\boldsymbol{p})\)</span> 表示点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{p}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个最近邻集合。LOF 的计算可以拆成三层：先算可达距离（Reachability Distance），再算局部可达密度（Local Reachability Density, LRD），最后比较邻居密度与自身密度，得到局部异常因子（Local Outlier Factor, LOF）。</p>
<p>先定义可达距离（Reachability Distance）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{rd}_k(\boldsymbol{p},\boldsymbol{o})=\max\big(\text{k-distance}(\boldsymbol{o}),d(\boldsymbol{p},\boldsymbol{o})\big)\]</span>
<p>可达距离会把两个量取最大值：邻居点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{o}\)</span> 自己的第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 邻距离，以及点对之间的真实距离。这样做可以防止极近的点对把局部密度估得过于夸张，使密度估计更稳。</p>
<p>再定义局部可达密度（Local Reachability Density, LRD）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{lrd}_k(\boldsymbol{p})=\left(\frac{1}{|N_k(\boldsymbol{p})|}\sum_{\boldsymbol{o}\in N_k(\boldsymbol{p})} \mathrm{rd}_k(\boldsymbol{p},\boldsymbol{o})\right)^{-1}\]</span>
<p>局部可达密度 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{lrd}_k(\boldsymbol{p})\)</span> 本质上是“平均可达距离”的倒数：平均距离越小，周围越拥挤，密度越大；平均距离越大，周围越稀疏，密度越小。</p>
<p>最终的 LOF 分数为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{LOF}_k(\boldsymbol{p})=\frac{1}{|N_k(\boldsymbol{p})|}\sum_{\boldsymbol{o}\in N_k(\boldsymbol{p})} \frac{\mathrm{lrd}_k(\boldsymbol{o})}{\mathrm{lrd}_k(\boldsymbol{p})}\]</span>
<p>LOF 分数比较的是“邻居的局部密度”和“自己本身的局部密度”。若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{lrd}_k(\boldsymbol{p})\)</span> 明显小于邻居的密度，分数就会大于 1，说明该点相对周围环境显得更孤立。更具体地说，分子是邻居局部密度的平均水平，分母是点 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{p}\)</span> 自己的局部密度，因此这个比值本质上是在问：<span style="background-color: #c0c0c0;">你周围的人都很挤，而你自己是不是站得太空</span>。</p>
<p>结果通常可以这样解读：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{LOF}_k(\boldsymbol{p})\approx 1\)</span>：该点的局部密度与邻居相近，通常属于正常样本。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{LOF}_k(\boldsymbol{p})&lt;1\)</span>：该点甚至比邻居更密集，往往处于簇的核心区域。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathrm{LOF}_k(\boldsymbol{p})&gt;1\)</span>：该点局部密度低于邻居，越大越像异常点。</li>
</ul>
<p>因此，LOF 回答的是“这个点在自己的局部邻域里是不是显得过于稀疏”，而不是“这个点离全局中心远不远”。这也是它能识别局部异常、却对距离度量与邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 较敏感的根本原因。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>为每个样本找到 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个近邻。</li>
<li>计算可达距离与局部可达密度。</li>
<li>比较样本与邻居的局部密度，得到 LOF 分数。</li>
<li>按分数排序或设置阈值输出异常点。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在消费行为数据中，某一线城市用户的高消费在全国范围内未必异常，但在“同年龄、同区域、同收入”的邻域里可能明显偏离。LOF 正是通过这种局部密度比较识别这类异常。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：适合不同密度区域共存的数据，能识别“全局上不极端、但局部上明显失配”的异常。</li>
<li>优点：解释性较强，因为 LOF 分数直接来自“邻居密度 / 自身密度”的局部比较，便于回答异常是相对于谁显得异常。</li>
<li>局限：对距离度量、标准化和邻居数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 很敏感； <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 太小会放大噪声，太大又可能抹平真正的局部异常。</li>
<li>局限：大规模近邻搜索成本较高，因此在超大数据上常需要索引加速或近似近邻方法配合。</li>
<li>适用场景：局部离群检测、消费异常、群体内部行为异常、同类用户或设备群体中的行为失配识别。</li>
</ul>
<div class="blog_h3"><span class="graybg">单类支持向量机（One-Class SVM）</span></div>
<div class="blog_h4"><span class="graybg">背景和问题定义</span></div>
<p>单类支持向量机（One-Class Support Vector Machine, One-Class SVM）用于“只有正常样本、缺少可靠异常样本”的异常检测任务。它的目标是学习一个描述正常样本区域的边界。</p>
<div class="blog_h4"><span class="graybg">核心思想</span></div>
<p>One-Class SVM 通过核映射把样本送到高维特征空间，再寻找一个把大多数正常样本与原点分开的超平面。等价地看，它学习的是一个“包住正常样本”的高维边界，边界外的点更可能是异常。</p>
<div class="blog_h4"><span class="graybg">算法公式和详细解释</span></div>
<p>一种标准形式为：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\boldsymbol{w},\rho,\boldsymbol{\xi}} \frac{1}{2}\|\boldsymbol{w}\|_2^2+\frac{1}{\nu N}\sum_{i=1}^{N} \xi_i-\rho\]</span>
<p>One-Class SVM 的目标由三部分组成： <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\|\boldsymbol{w}\|_2^2\)</span> 控制边界不要太复杂， <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{\nu N}\sum_i \xi_i\)</span> 惩罚落在边界外或靠得太近的样本， <span displaypfx="inline-" class="mathjax-container">\(-\rho\)</span> 则鼓励把正常区域尽量向外推开。 <span displaypfx="inline-" class="mathjax-container">\(\nu\)</span> 越大，对违约样本的容忍度越高。</p>
<span displaypfx="" class="mathjax-container">\[\text{s.t.} \quad \boldsymbol{w}^\top \phi(\boldsymbol{x}_i) \ge \rho-\xi_i,\qquad \xi_i \ge 0\]</span>
<p>这些约束表示：样本映射到特征空间后，其投影值至少要达到阈值 <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span>；若做不到，就用非负松弛变量 <span displaypfx="inline-" class="mathjax-container">\(\xi_i\)</span> 记录违约程度。于是模型允许少量样本越界，但必须为此付出代价。</p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}\)</span> 是超平面法向量， <span displaypfx="inline-" class="mathjax-container">\(\rho\)</span> 是阈值， <span displaypfx="inline-" class="mathjax-container">\(\xi_i\)</span> 是松弛变量， <span displaypfx="inline-" class="mathjax-container">\(\nu\in(0,1]\)</span> 控制允许落在边界外的比例与支持向量比例。判别函数为：</p>
<span displaypfx="" class="mathjax-container">\[f(\boldsymbol{x})=\text{sign}\left(\boldsymbol{w}^\top \phi(\boldsymbol{x})-\rho\right)\]</span>
<p>判别函数先计算样本在特征空间里相对边界的位置：若 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{w}^\top \phi(\boldsymbol{x})-\rho\)</span> 为正，说明样本位于学习到的正常区域一侧；若为负，则更可能落在边界之外，被视为异常。</p>
<p>若使用核函数 <span displaypfx="inline-" class="mathjax-container">\(K(\boldsymbol{x},\boldsymbol{x}')\)</span>，则该边界可以是非线性的，因此能表达复杂的正常区域。</p>
<div class="blog_h4"><span class="graybg">训练或推断流程</span></div>
<ol>
<li>只用正常样本训练模型，选择核函数与超参数 <span displaypfx="inline-" class="mathjax-container">\(\nu\)</span>。</li>
<li>在特征空间中学习分离超平面。</li>
<li>对新样本计算判别函数值。</li>
<li>若分数低于边界阈值，则判为异常。</li>
</ol>
<div class="blog_h4"><span class="graybg">应用实例</span></div>
<p>在设备健康监控中，异常故障类型往往变化很大，难以完整收集，但正常运行数据很多。One-Class SVM 可以只基于正常样本学习边界，一旦新样本落出该区域，就触发异常告警。</p>
<div class="blog_h4"><span class="graybg">优缺点与适用场景</span></div>
<ul>
<li>优点：只依赖正常样本；核方法可表达复杂边界。</li>
<li>局限：对特征缩放和核参数敏感；大规模训练较重。</li>
<li>适用场景：设备监控、入侵检测、质量控制等“正常样本丰富、异常样本稀缺”的任务。</li>
</ul>
<div class="blog_h1"><span class="graybg">神经网络</span></div>
<div class="blog_h2"><span class="graybg">前馈神经网络</span></div>
<div class="blog_h3"><span class="graybg">感知机</span></div>
<p>感知机（Perceptron）是最早的神经元模型之一，也是现代神经网络最基本的计算原型。无论是 MLP、CNN、RNN，还是 Transformer，本质上都由大量“线性变换 + 非线性变换”的单元堆叠而成；从这个意义上说，理解感知机，就是理解大型模型最小的功能部件。</p>
<p>最原始的感知机先做线性组合，再经过一个阈值函数给出二分类输出：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=\mathrm{sign}(\mathbf{w}^\top \mathbf{x}+b)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 是输入特征， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 是权重， <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是偏置。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top \mathbf{x}+b\)</span> 的含义是“沿着权重指定的方向对输入做加权打分”，而 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{sign}(\cdot)\)</span> 则把连续分数变成离散决策：高于阈值判为正类，低于阈值判为负类。</p>
<p>需要特别区分的是：最早的感知机确实直接做分类，但现代神经网络里的大多数“感知机式单元”并不直接输出最终类别。隐藏层单元更常见的形式是 <span displaypfx="inline-" class="mathjax-container">\(h=\phi(\mathbf{w}^\top \mathbf{x}+b)\)</span>，它们输出的是中间表示（Intermediate Representation），职责是检测局部模式、重组特征并为后续层提供更有用的表示；只有最后的任务头（Task Head）才把这些中间表示转成分类、回归或生成输出。</p>
<p>这里的中间表示（Intermediate Representation）本质上就是一组<span style="background-color: #c0c0c0;">可被后续层继续计算的数值特征</span>。它既可以是向量（Vector），也可以是矩阵（Matrix），更一般地说，它通常是张量（Tensor）。向量和矩阵都只是张量的特殊情形：若只看单个样本的 MLP 隐层输出，最常见的是向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{h}\in\mathbb{R}^{d}\)</span>；若看 Transformer 对整段序列的隐藏状态，常写成矩阵 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span>；若再把 batch 维也带上，则会变成三维张量 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\in\mathbb{R}^{B\times L\times d}\)</span>。在卷积网络（Convolutional Neural Network, CNN）里，中间表示则常是特征图张量（Feature Map Tensor） <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\in\mathbb{R}^{C\times H\times W}\)</span>，或带 batch 的 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^{B\times C\times H\times W}\)</span>。</p>
<p>因此，“中间表示”不是某种神秘对象，它就是网络在某一层对输入所形成的内部编码。它不像最终标签那样直接面向人类语义，而是把输入重写成更适合下一层处理的坐标系。例如，文本模型中的某一层隐藏状态可能同时编码词义、上下文关系、句法位置和任务相关线索；图像模型中的某一层特征图则可能突出边缘、纹理、局部部件或更高层形状。后续层与任务头读到的，正是这些内部编码。</p>
<p>感知机的重要性不止在于历史地位，更在于今天大型模型中的知识，本质上仍然是通过这类权重结构逐层编码进去的。训练过程并不会把知识像数据库那样逐条写成显式记录，而是不断调整参数，使某些输入模式被放大、某些输入模式被抑制。于是，模型在数据中反复见到的统计规律——词与词的共现、图像局部纹理、特征之间的组合关系——都会被压缩进参数矩阵的数值结构中。</p>
<p>更准确地说，大模型中的知识通常不是“某一个感知机单独存储一条事实”，而是以<span style="background-color: #c0c0c0;">分布式表示（Distributed Representation）</span>的形式分散在大量参数里。单个单元更像一个局部特征探测器（Feature Detector）：它只对某种模式敏感；许多单元级联后，网络才能把低层简单模式组合成高层抽象概念。模型规模越大、层数越深、参数越多，可被编码的模式组合也越丰富，这正是大模型具备强表达能力与“知识容量”的原因之一。</p>
<p>感知机能学会线性可分任务，但无法处理 XOR 这类线性不可分问题，这正是多层网络出现的动机：当一个超平面不够时，就需要通过多层组合把输入空间逐步重写成更容易分开的表示。</p>
<div class="blog_h3"><span class="graybg">多层感知机（MLP）</span></div>
<p>多层感知机（Multi-Layer Perceptron, MLP）处理的核心问题是：当输入与输出之间的关系不是一个超平面就能表达时，如何通过多层可学习变换，把原始特征逐步改写成更容易完成任务的表示。它由多层线性变换与逐元素非线性激活交替组成，是最基本也最通用的前馈网络结构。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp.png"><img class="alignnone size-full wp-image-41415" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp.png" alt="mlp" width="1024" height="1024" /></a></p>
<p>单层可写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{h}=\phi(W\mathbf{x}+\mathbf{b})\]</span>
<p>多层堆叠后，可写为：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{h}^{(1)}=\phi(W^{(1)}\mathbf{x}+\mathbf{b}^{(1)}),\quad \mathbf{h}^{(2)}=\phi(W^{(2)}\mathbf{h}^{(1)}+\mathbf{b}^{(2)}),\quad \hat{\mathbf{y}}=W^{(3)}\mathbf{h}^{(2)}+\mathbf{b}^{(3)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(W^{(l)}\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}^{(l)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层参数， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{h}^{(l)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层隐藏表示。所谓“逐层重写表示”，就是每一层都在回答一个更具体的问题：哪些原始模式值得保留、哪些组合值得放大、哪些方向更利于后续任务。于是，早期层往往捕捉较局部、较简单的模式，后期层则把这些模式组合成更抽象的语义结构。</p>
<p>MLP 比单层感知机强，关键不在“层数更多”本身，而在于层与层之间插入了非线性激活函数 <span displaypfx="inline-" class="mathjax-container">\(\phi\)</span>。如果没有非线性，多层线性变换满足</p>
<span displaypfx="" class="mathjax-container">\[W^{(2)}(W^{(1)}\mathbf{x}+\mathbf{b}^{(1)})+\mathbf{b}^{(2)}=\tilde W\mathbf{x}+\tilde{\mathbf{b}}\]</span>
<p>最终仍然等价于一层线性变换，表达能力不会因为堆叠而提升。只有加入非线性后，网络才能把输入空间切分、折叠、拉伸并重新组合，形成复杂的分段线性或平滑非线性决策边界。</p>
<p>从几何角度看，单层模型像“用一个超平面切一次”；而多层 MLP 则是在表示空间中反复做坐标变换和非线性折叠，把原本难分的数据逐步变成线性头也能分开的形状。文中的激活函数对比图展示的正是这个过程：不同激活函数会把同一个三层网络变成完全不同的几何变换器。</p>
<p>在现代大模型中，MLP 的作用远不只是“附属模块”。在 Transformer 里，注意力层负责在 token 之间路由信息，而 MLP / Feed-Forward Network（FFN）则负责在每个位置上做通道维度的非线性特征变换，把路由来的信息重新编码进更强的表示。因此，很多语义模式、组合规则与任务相关知识，最终都会沉淀到这些大规模参数化的 MLP 权重中。</p>
<p>更进一步说，在 Transformer 的常见解释框架里，MLP / FFN 往往被看作<span style="background-color: #c0c0c0;">事实性知识的重要载体之一</span>。一个常见直觉是把 FFN 看成参数化的“键值存储器（Key-Value Memory）”：第一层线性变换更像在检测当前输入是否匹配某种模式或概念，第二层线性变换则把与该模式相关的语义方向重新写回残差流（Residual Stream）。这里的残差流，可以理解为 Transformer 里那条贯穿各层的主表示通道：每一层注意力与 MLP 的输出，都会通过残差相加的方式写回这条主通道，再交给后续层继续处理。因此，诸如“实体—属性”“术语—定义”“模式—响应”这类较稳定的关联，常常更容易在 MLP 权重里留下痕迹。</p>
<p>但这并不意味着“一个神经元就存一条事实”。更准确的描述是：<span style="background-color: #c0c0c0;">知识通常以分布式方式存在于许多层、许多通道和许多参数方向里</span>。单个神经元有时会对某种关系或概念特别敏感，因而出现所谓“知识神经元（Knowledge Neurons）”现象；但更稳定的事实表示，通常仍然依赖一组共同激活的单元和跨层传递的表示。可以把两类子层的分工概括为：注意力更擅长“去哪里找、和谁建立联系”，MLP 更擅长“把匹配到的模式变成可供后续层使用的语义内容”。因此，说 MLP 是知识的主要载体之一是合理的；说知识只存在于 MLP、完全不在注意力里，则过于简单。</p>
<div class="blog_h2"><span class="graybg">激活函数</span></div>
<p>激活函数（Activation Function）的作用是给线性层引入非线性。如果没有激活函数，多层线性层叠起来仍然等价于一层线性变换，深度就失去了意义。</p>
<p>先给出一个实用的选型总表。它不代替后文的机制分析，但可以先回答工程上最常见的问题：某个激活函数通常应该放在输出层、隐藏层，还是特定结构里。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">激活函数</td>
<td style="text-align: center;">输出范围</td>
<td style="text-align: center;">典型适用场景</td>
<td style="text-align: center;">主要原因</td>
</tr>
</thead>
<tbody>
<tr>
<td>Sigmoid</td>
<td><span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span></td>
<td>二分类输出层；LSTM / GRU 门控</td>
<td>天然给出概率或开关强度，但隐藏层易饱和</td>
</tr>
<tr>
<td>Tanh</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span></td>
<td>较浅网络；需要零中心有界激活的结构</td>
<td>比 sigmoid 更利于优化，但深层仍会饱和</td>
</tr>
<tr>
<td>ReLU</td>
<td><span displaypfx="inline-" class="mathjax-container">\([0,+\infty)\)</span></td>
<td>深层前馈网络、CNN 的默认隐藏层</td>
<td>不饱和、计算便宜、分段线性、优化稳定</td>
</tr>
<tr>
<td>Leaky ReLU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\infty,+\infty)\)</span></td>
<td>担心 Dying ReLU 的深层隐藏层</td>
<td>保留 ReLU 优点，同时避免负半轴完全断梯度</td>
</tr>
<tr>
<td>ELU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\alpha,+\infty)\)</span></td>
<td>希望激活更平滑、且均值更接近 0 的隐藏层</td>
<td>负区间平滑并可取负值，但计算开销高于 ReLU</td>
</tr>
<tr>
<td>GELU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\infty,+\infty)\)</span></td>
<td>Transformer、BERT 类模型的隐藏层</td>
<td>选择性强且过渡平滑，兼顾表达能力与优化平滑性</td>
</tr>
<tr>
<td>Swish / SiLU</td>
<td><span displaypfx="inline-" class="mathjax-container">\((-\infty,+\infty)\)</span></td>
<td>现代卷积网络；部分大模型隐藏层</td>
<td>软门控、平滑、比硬截断更柔和</td>
</tr>
<tr>
<td>Softmax</td>
<td>各分量在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 且总和为 1</td>
<td>多分类输出层；语言模型词表分布输出</td>
<td>把 logits 归一化为概率分布，不用于普通隐藏层</td>
</tr>
</tbody>
</table>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp-activation-summary.png"><img class="alignnone size-full wp-image-41289" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mlp-activation-summary.png" alt="mlp-activation-summary" width="1920" height="957" /></a></p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/activation-functions-grid.png"><img class="alignnone size-full wp-image-41461" src="https://blog.gmem.cc/wp-content/uploads/2026/03/activation-functions-grid.png" alt="activation-functions-grid" width="1920" height="1079" /></a></p>
<div class="blog_h3"><span class="graybg">Sigmoid</span></div>
<p>Sigmoid 把实数压到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\sigma(x)=\frac{1}{1+e^{-x}}\]</span>
<p>Sigmoid 的优势在于输出天然落在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>，因此非常适合表示概率，尤其常用于二分类输出层或门控结构中。但它在隐藏层里的主要问题是饱和（saturation）：当输入绝对值较大时，函数会迅速贴近 0 或 1，此时导数接近 0，梯度在反向传播时会被不断压缩，深层网络因此容易出现梯度消失（Vanishing Gradient）。从优化角度看，这意味着前面层参数即使有误，也很难收到足够强的更新信号。</p>
<p>此外，sigmoid 的输出始终为正，不以 0 为中心，这会使后续层接收到带偏移的激活分布，通常不利于优化动态的稳定性。因此，sigmoid 今天更多保留在“需要概率解释”的输出层，或在 LSTM / GRU 等门控结构中充当开关函数，而不再是深层前馈隐藏层的默认选择。</p>
<div class="blog_h3"><span class="graybg">Tanh</span></div>
<p>Tanh 的输出范围是 <span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}\]</span>
<p>Tanh 可以看作“零中心版 sigmoid”：它同样会在大幅度输入时饱和，但输出分布位于 <span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span> 且以 0 为中心，这通常比 sigmoid 更利于优化，因为后续层接收到的激活不再整体偏向正侧。对于需要表达“正负方向”差异的隐藏表示，tanh 往往也比 sigmoid 更自然。</p>
<p>不过，tanh 并没有解决饱和带来的根本问题：当 <span displaypfx="inline-" class="mathjax-container">\(|x|\)</span> 很大时，导数仍接近 0，深层网络中的梯度传播依然会变弱。因此，在较深的前馈网络里，tanh 通常不如 ReLU 家族稳定；它更多出现在较浅网络、早期神经网络设计，或某些希望激活有界且零中心的结构中。</p>
<div class="blog_h3"><span class="graybg">ReLU</span></div>
<p>ReLU（Rectified Linear Unit）定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ReLU}(x)=\max(0,x)\]</span>
<p>ReLU 看起来几乎“过于简单”，但它之所以长期有效，关键不在公式花哨，而在于它同时满足了深层优化最需要的几条性质。第一，ReLU 在正半轴不饱和（non-saturating）：当 <span displaypfx="inline-" class="mathjax-container">\(x&gt;0\)</span> 时导数恒为 1，梯度穿过这一单元时不会像 sigmoid / tanh 那样被持续压小，因此更有利于深层网络中的梯度传播。第二，ReLU 保留了非线性，但正半轴仍是线性的，这使整个网络变成<span style="background-color: #c0c0c0;">分段线性（piecewise linear）</span>系统：表达能力足够强，同时局部优化形状又比高度弯曲的饱和函数更“规整”。第三，负半轴直接截断会带来自然的稀疏激活（sparse activation）：不是所有单元都会在每个样本上同时活跃，这通常有助于提升表示分解能力，并降低无效共适应（co-adaptation）。这里的共适应，指多个神经元在训练中形成了过强的相互依赖：某个单元之所以有效，不是因为它单独学到了稳定、可迁移的模式，而是因为它总是和另外几个特定单元“成套工作”。一旦输入分布变化，或其中某些单元没有按训练时那样响应，这种脆弱的协同关系就容易失效，从而削弱泛化能力。</p>
<p>从工程角度看，ReLU 的优势还包括计算代价极低，只需一次比较运算；这在大规模训练中会被成千上万层和数十亿次前向/反向传播放大。更深层的原因是：深度网络真正需要的不是“平滑得很漂亮”的激活函数，而是一个既能打破线性、又不会在大范围内把梯度压扁、还能让优化器容易工作的非线性。ReLU 恰好在这三点之间取得了非常实用的平衡，这就是为什么一个形式上极其朴素的函数，反而成为现代深度学习最成功的默认选择之一。它的代价也很明确：负区间梯度为 0，单元可能长期失活，这就是后面 Leaky ReLU、ELU、GELU 等变体继续改进的出发点。</p>
<div class="blog_h3"><span class="graybg">Leaky ReLU</span></div>
<p>Leaky ReLU 给负半轴保留一个很小的斜率：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{LeakyReLU}(x)=\max(\alpha x,x),\quad \alpha\ll 1\]</span>
<p>这样做是为了缓解“死亡 ReLU（Dying ReLU）”问题：若某神经元长期落在负区间，普通 ReLU 的梯度可能一直为 0，而 Leaky ReLU 仍保留一点更新信号。</p>
<p>从机制上看，Leaky ReLU 的核心改动很小，但很有针对性：它保留了 ReLU 在正半轴的不饱和与分段线性优点，同时避免把负半轴完全切断。这样即使某个单元暂时落入负区间，参数仍有机会通过非零梯度被重新拉回活跃状态。因此，Leaky ReLU 可以看作对 ReLU 的保守修正：表达风格几乎不变，但训练风险更低。</p>
<div class="blog_h3"><span class="graybg">ELU</span></div>
<p>ELU（Exponential Linear Unit）在负半轴使用指数平滑：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ELU}(x)=\begin{cases}x,&amp;x&gt;0\\ \alpha(e^x-1),&amp;x\le 0\end{cases}\]</span>
<p>它的目标是兼顾 ReLU 的优化优势和负区间的平滑性，使激活均值更接近 0。</p>
<p>与 Leaky ReLU 相比，ELU 在负半轴不再是简单直线，而是用指数曲线平滑衰减到负饱和值。这带来两个效果：一是避免了 ReLU 在 0 点附近过于生硬的折线结构；二是允许激活出现稳定的负值，从而减轻隐藏表示整体偏正的问题。代价是计算比 ReLU 更重，且负区间在极小值处同样会逐渐饱和，因此它通常被视为“更平滑、更零中心”的 ReLU 变体，而不是彻底不同的一类激活。</p>
<div class="blog_h3"><span class="graybg">GELU</span></div>
<p>GELU（Gaussian Error Linear Unit）可理解为“按输入大小平滑地决定保留多少信号”。它不像 ReLU 那样硬截断，而是对小正值和小负值做连续、概率化的保留，因此在 0 附近更平滑。</p>
<p>这种设计的价值在于：它仍然保留了 ReLU 家族的选择性——不是所有信号都被同等对待——但又避免了硬截断带来的尖锐折点和完全失活区。于是，GELU 往往能在“表达选择性”和“优化平滑性”之间取得更好的折中，这也是它在 Transformer、BERT 及其后续大量变体中被广泛采用的重要原因。</p>
<div class="blog_h3"><span class="graybg">Swish / SiLU</span></div>
<p>Swish / SiLU 定义为</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{SiLU}(x)=x\,\sigma(x)\]</span>
<p>它是平滑、非单调的激活函数，在某些深层网络里表现优于 ReLU。其结构可以直接读成“输入值 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 乘上一个 sigmoid 门控 <span displaypfx="inline-" class="mathjax-container">\(\sigma(x)\)</span>”：当 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 很大时， <span displaypfx="inline-" class="mathjax-container">\(\sigma(x)\approx 1\)</span>，信号几乎原样通过；当 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 很小时， <span displaypfx="inline-" class="mathjax-container">\(\sigma(x)\approx 0\)</span>，信号被显著压低；在 0 附近则是连续、平滑的软过渡。</p>
<p>这种形式的价值在于：它既不像 ReLU 那样做硬截断，也不像 sigmoid 那样把输出彻底压进固定区间，而是让网络学到一种“按输入强度自适应通过多少”的软门控机制。结果是，SiLU / Swish 往往能在保持优化平滑性的同时，保留较强的表达灵活性，因此在一些现代卷积网络与大模型变体中表现良好。它可以看作介于 ReLU 家族与门控激活之间的一种折中设计。</p>
<div class="blog_h3"><span class="graybg">Softmax</span></div>
<p>Softmax 把一组实数分数（scores）映射为概率分布（Probability Distribution）。在分类与语言模型里，这组分数通常称为 logit（Logits）：它们是 softmax 之前的未归一化输出。</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{softmax}(z)_i=\frac{e^{z_i}}{\sum_{j=1}^{V} e^{z_j}}\]</span>
<p>logits 的两个关键性质：</p>
<ul>
<li>logits 不需要是概率，可以是任意实数；softmax 才把它变成 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span> 且和为 1 的分布。</li>
<li>softmax 对整体平移不敏感：对任意常数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{softmax}(z)=\mathrm{softmax}(z+c\mathbf{1})\)</span>。因此实现里常用 <span displaypfx="inline-" class="mathjax-container">\(z\leftarrow z-\max_i z_i\)</span> 做数值稳定（Numerical Stability）。</li>
</ul>
<p>这说明 softmax 真正关心的是各个 logit 之间的相对差值，而不是某个 logit 的绝对数值。若把所有分数同时加 10，模型对“哪一类更占优”的判断不会改变，因为指数项会在分子和分母里同时乘上 <span displaypfx="inline-" class="mathjax-container">\(e^{10}\)</span>，最终完全约掉。改变 softmax 输出的，是某个类别相对其他类别高了多少，而不是整体抬高或压低所有分数。</p>
<p>也正因为如此，logit 更适合理解为“未归一化偏好分数（unnormalized preference scores）”，而不是“概率雏形”。例如两类 logits 从 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\((101,102)\)</span>，softmax 输出完全相同；但若从 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\((1,5)\)</span>，第二类相对第一类的优势被显著拉大，概率才会明显变化。</p>
<p>在语言模型（Language Model）中，给定最后一层隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d_{\text{model}}}\)</span>，线性输出头产生词表大小 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 的 logits： <span displaypfx="inline-" class="mathjax-container">\(z=hW_{\text{vocab}}+b\)</span>，再经 softmax 得到下一个 token 的分布。若采用权重共享（Weight Tying），则输入嵌入表 <span displaypfx="inline-" class="mathjax-container">\(E\in\mathbb{R}^{V\;\times d_{\text{model}}}\)</span> 与输出头满足 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab}}=E^\top\)</span>，于是可直接写成 <span displaypfx="inline-" class="mathjax-container">\(z=hE^\top+b\)</span>。</p>
<div class="blog_h4"><span class="graybg">Softmax和分类任务</span></div>
<p>在多分类任务里，这几个概念实际上是一条连续的计算链：任务头先输出 logits <span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}^{C}\)</span>，softmax 把它们变成条件概率 <span displaypfx="inline-" class="mathjax-container">\(p(y=i|x)\)</span>，再取真实类别 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 的负对数概率作为单样本损失，也就是负对数似然（Negative Log-Likelihood, NLL）。</p>
<span displaypfx="" class="mathjax-container">\[p(y=i|x)=\mathrm{softmax}(z)_i=\frac{e^{z_i}}{\sum_{j=1}^{C}e^{z_j}},\qquad \ell_{\mathrm{NLL}}(z,c)=-\log p(y=c|x)\]</span>
<p>把两步合起来，NLL 可以直接写成 logits 的函数：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{NLL}}(z,c)=-\log\frac{e^{z_c}}{\sum_{j=1}^{C}e^{z_j}}=-z_c+\log\sum_{j=1}^{C}e^{z_j}\]</span>
<p>这个式子把训练目标拆成了两部分：第一项 <span displaypfx="inline-" class="mathjax-container">\(-z_c\)</span> 要求真实类别的 logit 足够大；第二项 <span displaypfx="inline-" class="mathjax-container">\(\log\sum_j e^{z_j}\)</span> 是归一化项（log-sum-exp），它把所有类别的竞争都算进去。因此训练并不是单独把正确类别分数抬高，而是要让它相对其他类别更占优势。</p>
<p>softmax 的平移不变性（Translation Invariance）在这里也能直接看见。对任意常数 <span displaypfx="inline-" class="mathjax-container">\(a\)</span>，若把所有 logits 同时改为 <span displaypfx="inline-" class="mathjax-container">\(z+a\mathbf{1}\)</span>，则 softmax 概率不变，NLL 也不变：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{softmax}(z+a\mathbf{1})_i=\mathrm{softmax}(z)_i,\qquad \ell_{\mathrm{NLL}}(z+a\mathbf{1},c)=\ell_{\mathrm{NLL}}(z,c)\]</span>
<p>因此 logits 的绝对零点没有意义，真正有意义的是类别之间的相对差值。工程实现里常把 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 先整体减去 <span displaypfx="inline-" class="mathjax-container">\(\max_i z_i\)</span>，再计算 softmax 或 log-sum-exp。这样不会改变概率与损失，但能显著降低指数溢出的风险。</p>
<div class="blog_h2"><span class="graybg">损失函数</span></div>
<p>术语区分：假设函数（Hypothesis Function）/模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 定义“模型在做什么映射”；样本损失（Loss Function）定义在单样本上；代价函数/成本函数（Cost Function）是把样本损失在全数据集上做平均或求和后的经验风险；目标函数（Objective Function）是优化器真正要优化的函数，最常见写法是 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)=L(\theta)+\lambda\Omega(\theta)\)</span>。</p>
<div class="blog_h3"><span class="graybg">回归损失</span></div>
<div class="blog_h4"><span class="graybg">MSE</span></div>
<p>均方误差（Mean Squared Error, MSE）定义为</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{MSE}}(y,\hat y)=(\hat y-y)^2\]</span>
<p>平方的作用是让大误差被放大处罚。若一个样本错 10，另一个样本错 1，那么前者在 MSE 里不是“重 10 倍”，而是“重 100 倍”。因此 MSE 很适合你明确希望重罚大错的场景，也对应前面讲过的高斯噪声假设。</p>
<div class="blog_h4"><span class="graybg">MAE</span></div>
<p>平均绝对误差（Mean Absolute Error, MAE）对应单样本形式</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{MAE}}(y,\hat y)=|\hat y-y|\]</span>
<p>它直接度量偏差大小，对离群点更鲁棒，因为不会像平方那样把大误差急剧放大。若做房价预测，数据中存在一批价格远高于主体分布的豪宅样本时，MAE 往往比 MSE 更稳；这里的“主体分布”指的是样本中占大多数的普通住宅价格区间，而豪宅样本相对它明显偏高。这样一来，哪怕豪宅样本数量不多，它们也会在 MSE 下因为误差被平方而获得过大的影响力。</p>
<div class="blog_h4"><span class="graybg">Huber Loss</span></div>
<p>Huber Loss 结合了 MSE 与 MAE：误差小时像平方误差，误差大时像绝对误差。设阈值 <span displaypfx="inline-" class="mathjax-container">\(\delta\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\ell_\delta(r)=\begin{cases}\frac{1}{2}r^2,&amp;|r|\le \delta\\ \delta(|r|-\frac{1}{2}\delta),&amp;|r|&gt;\delta\end{cases},\quad r=\hat y-y\]</span>
<p>这条式子的直觉是：小误差区间内保持平滑、便于优化；大误差区间内降低对离群点的过度敏感。它像“正常误差严肃处理，极端异常别让它一票否决整个模型”。</p>
<div class="blog_h3"><span class="graybg">分类损失</span></div>
<div class="blog_h4"><span class="graybg">交叉熵损失（Binary）</span></div>
<p>二分类交叉熵（Binary Cross-Entropy, BCE）用来训练输出概率 <span displaypfx="inline-" class="mathjax-container">\(p\in(0,1)\)</span> 的二分类器。这里 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 通常表示模型预测“正类”的概率， <span displaypfx="inline-" class="mathjax-container">\(y\in\{0,1\}\)</span> 是真实标签： <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 表示正类， <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span> 表示负类。</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{BCE}}(y,p)=-\Big(y\log p+(1-y)\log(1-p)\Big)\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(\ell_{\mathrm{BCE}}(y,p)\)</span> 表示“单个样本在真实标签为 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>、模型预测正类概率为 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 时的 BCE 损失值”。也就是说，这不是新的概率，而是一个标量惩罚：预测越符合真实标签，它越小；预测越违背真实标签，它越大。</p>
<p>这条公式会根据 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的取值自动选择应当惩罚哪一项。当 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 时，式中的 <span displaypfx="inline-" class="mathjax-container">\((1-y)=0\)</span>，因此第二项消失，损失化简为 <span displaypfx="inline-" class="mathjax-container">\(-\log p\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span> 时，第一项中的 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span>，因此第一项消失，损失化简为 <span displaypfx="inline-" class="mathjax-container">\(-\log(1-p)\)</span>。于是它惩罚的本质就是：让真实类别对应的概率尽可能高。</p>
<p>数值例子（自然对数）：若 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(p=0.9\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\ell\approx 0.105\)</span>；若 <span displaypfx="inline-" class="mathjax-container">\(p=0.1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\ell\approx 2.303\)</span>。正确但不自信会被罚，错误且自信会被重罚。</p>
<div class="blog_h4"><span class="graybg">交叉熵损失（Categorical）</span></div>
<p>多分类交叉熵（Categorical Cross-Entropy, CE）与 softmax 通常配套使用。这里 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实分布（Ground-Truth Distribution）在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类上的概率质量， <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 是模型预测分布（Predicted Distribution）在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类上的概率。若类别总数为 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>，则单样本损失写成：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{CE}}(y,p)=-\sum_{i=1}^{C} y_i\log p_i\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(\ell_{\mathrm{CE}}(y,p)\)</span> 表示“当真实标签分布为 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>、模型预测分布为 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 时，这个样本对应的交叉熵损失值”。其本质是：<span style="background-color: #c0c0c0;">用真实标签分布 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 作为权重，对预测分布的负对数概率 <span displaypfx="inline-" class="mathjax-container">\(-\log p_i\)</span> 做加权平均</span>。</p>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是 one-hot 分布时，只有真实类别 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 那一维的权重为 1，其余维度权重都为 0，因此求和会自动塌缩成单项：</p>
<span displaypfx="" class="mathjax-container">\[\ell=-\log p_c\]</span>
<p>这正是分类任务里最常见的形式。它与最大似然估计（Maximum Likelihood Estimation, MLE）完全一致：最小化交叉熵等价于最大化真实类别的对数似然。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 不再是 one-hot，而是一个在多个类别上分配了非零概率质量的分布，那么交叉熵就不再只读取单个类别 <span displaypfx="inline-" class="mathjax-container">\(p_c\)</span>，而是会对整条标签分布做加权。常见来源有两类。</p>
<p>第一类是软标签（Soft Label）。它指真实监督信号本身就是一个概率分布，而不是“只有一个绝对正确类别”的硬标签（Hard Label）。例如一张图像可能被标注为“70% 像猫、30% 像狐狸”，或一个样本本身就带有多标注者投票汇总后的类别分布。在这种情况下， <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 直接表示第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类的目标概率，交叉熵自然要对所有类别一起计算。</p>
<p>第二类是教师分布蒸馏（Knowledge Distillation from Teacher Distribution）。知识蒸馏（Knowledge Distillation）的做法是：不用人工标签单独监督学生模型（Student Model），而是让学生去拟合教师模型（Teacher Model）给出的类别分布。若教师在某个样本上输出 <span displaypfx="inline-" class="mathjax-container">\(q\)</span>，学生输出 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，则训练目标常包含 <span displaypfx="inline-" class="mathjax-container">\(-\sum_i q_i\log p_i\)</span> 这样的交叉熵或等价的 KL 散度项。它传递的不只是“哪一类是对的”，还传递“其余类别分别有多像”，因此常被称为暗知识（Dark Knowledge）。</p>
<p>这两种情况的共同点是：标签本身已经不是单点答案，而是一条分布。于是交叉熵的计算对象就不再只是“真实类别那一项”，而是整个目标分布与预测分布之间的匹配程度。Label Smoothing 也会把 one-hot 改成软分布，但它承担的是训练目标修正的角色，因此放在后文“分类任务正则化”里更清晰。</p>
<p>从信息论角度看，交叉熵的标准定义是两个分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 之间的</p>
<span displaypfx="" class="mathjax-container">\[H(P,Q)=-\sum_x P(x)\log Q(x)\]</span>
<p>分类里的 <span displaypfx="inline-" class="mathjax-container">\(\ell_{\mathrm{CE}}(y,p)\)</span> 与这个定义并不矛盾，它只是把信息论中的 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 分别具体化成了“单个样本对应的真实标签分布 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>”与“模型在这个样本上的预测分布 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>”。若标签是 one-hot，那么 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 退化成一个只在真实类别处取值为 1 的离散分布，于是信息论里的交叉熵自然退化成 <span displaypfx="inline-" class="mathjax-container">\(-\log p_c\)</span>。</p>
<p>进一步地，交叉熵与 KL 散度（Kullback–Leibler Divergence）的关系可以直接写成：</p>
<span displaypfx="" class="mathjax-container">\[H(P,Q)=H(P)+D_{\mathrm{KL}}(P\|Q)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(H(P)=-\sum_x P(x)\log P(x)\)</span> 是真实分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 自身的熵（Entropy），只由数据分布决定； <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(P\|Q)=\sum_x P(x)\log\frac{P(x)}{Q(x)}\)</span> 是 KL 散度，用来衡量预测分布 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 相对真实分布 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 的偏离程度。于是，在训练数据固定时， <span displaypfx="inline-" class="mathjax-container">\(H(P)\)</span> 是常数，最小化交叉熵 <span displaypfx="inline-" class="mathjax-container">\(H(P,Q)\)</span> 就等价于最小化 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(P\|Q)\)</span>。</p>
<p>因此，“最小化交叉熵就是最小化 KL 散度”这句话在监督学习里通常成立，但更准确的表述是：<span style="background-color: #c0c0c0;">在真实分布固定不变时，最小化交叉熵与最小化预测分布对真实分布的 KL 偏离是等价的</span>。KL 散度确实可以理解为“与真实分布的差异或偏离”，但它不是对称距离（Symmetric Distance）：一般有 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(P\|Q) \neq D_{\mathrm{KL}}(Q\|P)\)</span>，也不满足严格距离函数的三角不等式，因此更准确的名称是分布失配（distribution mismatch）或相对熵（relative entropy）。</p>
<p>若进一步把 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 写成 softmax 作用在 logits <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 上，则多分类交叉熵可直接写成：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{CE}}(z,c)=-\log\mathrm{softmax}(z)_c=-z_c+\log\sum_{j=1}^{C}e^{z_j}\]</span>
<p>这条式子把任务头和损失函数直接接起来：线性头先输出 logits，softmax 把它们归一化为概率，交叉熵再读取真实类别的负对数概率。由于 softmax 具有平移不变性，给所有 logits 同时加上同一个常数不会改变这个损失，因此实现中通常直接从 logits 计算交叉熵（log-sum-exp 形式），并先减去 <span displaypfx="inline-" class="mathjax-container">\(\max_j z_j\)</span> 做数值稳定，而不是显式先算 softmax 再取 log。</p>
<div class="blog_h4"><span class="graybg">Focal Loss</span></div>
<p>Focal Loss 常用于类别极不平衡的分类任务，尤其是目标检测（Object Detection）这类“负样本远多于正样本”的场景。它不是简单换掉交叉熵，而是在交叉熵前再乘一个与样本难度相关的调制因子，使已经分得很对的容易样本贡献变小，把梯度预算更多留给困难样本：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{focal}}(p_t)=-(1-p_t)^\gamma\log p_t\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(p_t\)</span> 表示“真实类别对应的预测概率”：若真实标签 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(p_t=p\)</span>；若 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(p_t=1-p\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(-\log p_t\)</span> 就是普通交叉熵，而前面的 <span displaypfx="inline-" class="mathjax-container">\((1-p_t)^\gamma\)</span> 是额外加上的难度调制项。参数 <span displaypfx="inline-" class="mathjax-container">\(\gamma\ge 0\)</span> 控制聚焦强度： <span displaypfx="inline-" class="mathjax-container">\(\gamma=0\)</span> 时，Focal Loss 退化回普通交叉熵； <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 越大，对容易样本的压低越明显。</p>
<p>这个机制的关键在于样本难度如何反映到权重上。若某个样本已经分得很对，例如真实类别概率 <span displaypfx="inline-" class="mathjax-container">\(p_t=0.99\)</span>，则调制因子 <span displaypfx="inline-" class="mathjax-container">\((1-p_t)^\gamma\)</span> 会非常小；以 <span displaypfx="inline-" class="mathjax-container">\(\gamma=2\)</span> 为例，权重大约只有 <span displaypfx="inline-" class="mathjax-container">\((0.01)^2=10^{-4}\)</span>，这意味着它对总损失和梯度的影响被大幅削弱。相反，若某个样本很难，例如 <span displaypfx="inline-" class="mathjax-container">\(p_t=0.2\)</span>，则权重约为 <span displaypfx="inline-" class="mathjax-container">\((0.8)^2=0.64\)</span>，其损失会被较大程度保留。于是训练过程不再被海量“早就分对的简单样本”主导，而会持续关注误分样本、边界样本和少数类样本。</p>
<p>目标检测是最典型的应用例子。以单阶段检测器（One-stage Detector）为例，一张图像上往往有成千上万个候选框（Anchors），但真正包含目标的正样本只占极少数；绝大多数候选框都是背景。若直接使用普通交叉熵，训练会被这些“背景且容易判断”的负样本淹没：它们单个损失虽小，但数量太多，累积后仍然主导梯度。Focal Loss 的作用正是把这批容易背景样本的权重压下去，让模型把更多注意力放在少数正样本、遮挡目标、边界模糊目标，以及那些看起来像目标但其实是背景的困难负样本上。这样做通常会显著改善长尾检测与前景-背景极不平衡时的训练效果。</p>
<div class="blog_h4"><span class="graybg">Hinge Loss</span></div>
<p>Hinge Loss 是 SVM 常用的分类损失：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{hinge}}(y,f)=\max(0,1-yf),\quad y\in\{-1,+1\}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 是模型分数而不是概率。当 <span displaypfx="inline-" class="mathjax-container">\(yf\ge 1\)</span> 时，说明不仅分类正确，而且留出了足够间隔，损失为 0；当 <span displaypfx="inline-" class="mathjax-container">\(yf&lt;1\)</span> 时，就要受罚。它强调的不只是“分对”，而是“分对且留有安全距离”。</p>
<div class="blog_h3"><span class="graybg">度量学习损失</span></div>
<p>度量学习（Metric Learning）不直接预测类别，而是学习一个表示空间，让“应该相似的样本靠近，不该相似的样本拉远”。这类损失在检索、人脸识别、推荐召回和 embedding 学习中非常常见。</p>
<div class="blog_h4"><span class="graybg">Contrastive Loss</span></div>
<p>Contrastive Loss 处理样本对（pair）。若一对样本应相似，则拉近它们；若应不同，则至少推开到某个间隔 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 之外：</p>
<span displaypfx="" class="mathjax-container">\[\ell=y\,d^2+(1-y)\max(0,m-d)^2\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(d\)</span> 是两者在嵌入空间中的距离， <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span> 表示正对， <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span> 表示负对。它像“朋友要坐得近，陌生人至少别挤在一起”。</p>
<div class="blog_h4"><span class="graybg">Triplet Loss</span></div>
<p>Triplet Loss 使用三元组：锚点（Anchor）、正样本（Positive）、负样本（Negative）。目标是让锚点离正样本比离负样本更近至少一个 margin：</p>
<span displaypfx="" class="mathjax-container">\[\ell=\max\big(0,\ d(a,p)-d(a,n)+m\big)\]</span>
<p>这条式子表达的是一种相对排序约束，而不是绝对相似度分数。它非常适合“谁比谁更像”的任务，例如人脸验证：同一个人的两张照片应比不同人的照片更近。</p>
<div class="blog_h4"><span class="graybg">InfoNCE</span></div>
<p>InfoNCE 是现代对比学习最常见的损失之一。对一个锚点来说，它把正样本放进一堆候选里，要求模型把正样本打分最高：</p>
<span displaypfx="" class="mathjax-container">\[-\log \frac{\exp(\mathrm{sim}(z_i,z_i^+)/\tau)}{\sum_j \exp(\mathrm{sim}(z_i,z_j)/\tau)}\]</span>
<p>分子是正确配对，分母是所有候选。这个结构和 softmax 分类非常像，只不过类别不再是固定标签，而是“在一堆候选里，谁才是真正匹配的那个”。在大语言模型 embedding、图像表征学习和多模态对齐里，它几乎是标准配置。</p>
<div class="blog_h2"><span class="graybg">任务头（Task Head）</span></div>
<p>任务头（Task Head）是把主干网络（Backbone）产出的隐藏表示（Hidden Representation）映射到具体任务输出空间的模块。主干负责抽取通用特征，任务头负责把特征“读出来”并对齐到目标形式（类别、数值、序列标签、跨度、关系等）。在工程上，绝大多数“用 Transformer 做下游任务”都可以写成：<span style="background-color: #c0c0c0;">Transformer backbone + task head + task loss</span>。</p>
<div class="blog_h3"><span class="graybg">中间表示、logits 与任务头的关系</span></div>
<p>主干网络输出的隐藏表示（Hidden Representation）是任务头的输入，任务头则是把这种内部表示读成具体任务输出的最后一层或最后几层变换。若把主干输出记为 <span displaypfx="inline-" class="mathjax-container">\(h\)</span>、<span displaypfx="inline-" class="mathjax-container">\(H\)</span> 或更一般的张量 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{H}\)</span>，则任务头通常先做一次线性读出（Linear Readout）得到分数，再视任务类型决定是否接 sigmoid、softmax、CRF 解码或直接保留实数输出。</p>
<p>logits 就是在这个读出阶段最常见的中间产物。它们是<span style="background-color: #c0c0c0;">任务头输出、但尚未归一化或尚未解码的原始分数</span>。例如，多分类头常先产生 <span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}^{C}\)</span>，这里 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 是类别数， <span displaypfx="inline-" class="mathjax-container">\(z_c\)</span> 表示模型对第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 类的偏好分数；softmax 之后这些分数才变成概率。对 token 分类任务，task head 产生的不是单个向量，而是一整张 logits 矩阵 <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times C}\)</span>；对语言模型，输出则是词表 logits <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times V}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 是词表大小。</p>
<p>因此，关系可以概括为：<span style="background-color: #c0c0c0;">输入先被 backbone 编码成中间表示，中间表示再被 task head 读成 logits 或其他任务分数，最后再由概率映射、解码器或损失函数把这些分数变成最终预测</span>。logits 不是所有中间表示的统称，而是“距离最终任务输出只差一步”的那类任务分数；它们通常由任务头生成，而不是由主干网络中间每一层都显式生成。</p>
<div class="blog_h3"><span class="graybg">任务头输出对照表</span></div>
<p>不同任务头的差异，最核心地体现在“直接输出什么张量、这些张量后面还要经过什么处理”这两个问题上。下面这张表把常见任务头的输入形状、直接输出和后续处理并列起来，便于从工程实现角度快速对照。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">任务类型</td>
<td style="text-align: center;">任务头常见输入</td>
<td style="text-align: center;">任务头直接输出</td>
<td style="text-align: center;">后续处理</td>
</tr>
</thead>
<tbody>
<tr>
<td>二分类</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>标量 logit <span displaypfx="inline-" class="mathjax-container">\(z\in\mathbb{R}\)</span>，或二维 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{2}\)</span></td>
<td>sigmoid 或 softmax，得到类别概率</td>
</tr>
<tr>
<td>多分类</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>类别 logits 向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{C}\)</span></td>
<td>softmax 后得到 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 类概率</td>
</tr>
<tr>
<td>多标签分类</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>每个标签一个 logit，组成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{C}\)</span></td>
<td>对每一维独立做 sigmoid，而不是在类别间做 softmax</td>
</tr>
<tr>
<td>回归</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>实数或向量 <span displaypfx="inline-" class="mathjax-container">\(\hat{\mathbf{y}}\in\mathbb{R}^{m}\)</span></td>
<td>通常不做概率归一化，直接配合回归损失</td>
</tr>
<tr>
<td>Token 分类 / NER</td>
<td>序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span></td>
<td>token-level logits 矩阵 <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times C}\)</span></td>
<td>逐 token softmax，或接 CRF 做全局解码</td>
</tr>
<tr>
<td>语言模型 / 文本生成</td>
<td>序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span></td>
<td>词表 logits <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\times V}\)</span></td>
<td>对每个位置在词表维做 softmax，得到 next-token 分布</td>
</tr>
<tr>
<td>跨度抽取（Span Extraction）</td>
<td>序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span></td>
<td>起点 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\in\mathbb{R}^{L}\)</span> 与终点 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\in\mathbb{R}^{L}\)</span>，或 span 分数矩阵</td>
<td>在起止位置上做 softmax 或联合评分，输出片段边界</td>
</tr>
<tr>
<td>依存句法 / 关系抽取</td>
<td>成对表示 <span displaypfx="inline-" class="mathjax-container">\(h_i,h_j\)</span> 或序列表示 <span displaypfx="inline-" class="mathjax-container">\(H\)</span></td>
<td>边分数矩阵 <span displaypfx="inline-" class="mathjax-container">\(S\in\mathbb{R}^{L\times L}\)</span>，或关系分数张量 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{S}\in\mathbb{R}^{L\times L\times C}\)</span></td>
<td>argmax、biaffine 解码或图结构约束解码</td>
</tr>
<tr>
<td>度量学习 / 检索</td>
<td>单样本表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span></td>
<td>embedding 向量 <span displaypfx="inline-" class="mathjax-container">\(e\in\mathbb{R}^{d'}\)</span></td>
<td>不直接输出 logits；后续用相似度函数或对比损失比较</td>
</tr>
</tbody>
</table>
<p>这张表的关键在于区分“任务头直接输出什么”和“用户最终看到什么”。很多任务头直接输出的并不是概率，也不是标签，而是 logits、边分数、起止位置分数或 embedding。概率、标签、生成 token、依存边、异常分数等最终结果，通常还需要经过归一化、解码、阈值化或搜索过程才能得到。</p>
<div class="blog_h3"><span class="graybg">分类头</span></div>
<p>分类头（Classification Head）的核心职责，是把主干网络输出的表示 <span displaypfx="inline-" class="mathjax-container">\(h\in\mathbb{R}^{d}\)</span> 变成类别分数（Class Scores）或 logits。最常见的做法是一层线性映射：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{z}=W\mathbf{h}+\mathbf{b}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{h}\in\mathbb{R}^{d}\)</span> 是单个样本的隐藏表示， <span displaypfx="inline-" class="mathjax-container">\(W\)</span> 是任务头权重矩阵， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 是偏置向量， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 是未归一化类别分数。分类任务的关键区别不在于“有没有线性层”，而在于：<span style="background-color: #c0c0c0;">输出空间是否互斥、每个样本允许几个标签成立、以及这些分数之后接什么归一化与损失</span>。</p>
<p>直觉上，这个线性头就是一个“可学习的读出（Readout）”：在高维表示空间里用超平面（Hyperplane）切分区域，或用线性映射把表示投影到目标坐标系。这里的“读出”指的是：主干网络先把输入编码成内部表示，而任务头再把这种内部表示转换成模型真正需要输出的量，例如类别 logits、词表 logits、回归值、span 分数或关系分数。换言之，读出不是“再提特征”，而是<span style="background-color: #c0c0c0;">把已经形成的表示翻译成任务空间中的可判定分数</span>。</p>
<div class="blog_h4"><span class="graybg">二分类</span></div>
<p>二分类（Binary Classification）要求每个样本只在两个互斥类别中选一个，例如“垃圾 / 非垃圾”“欺诈 / 正常”“阳性 / 阴性”。最常见的写法是输出一个标量 logit：</p>
<span displaypfx="" class="mathjax-container">\[z=\mathbf{w}^\top \mathbf{h}+b\]</span>
<p>然后通过 sigmoid 得到正类概率：</p>
<span displaypfx="" class="mathjax-container">\[p=P(y=1\mid x)=\sigma(z)=\frac{1}{1+e^{-z}}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是模型对正类的原始偏好分数， <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是正类概率，负类概率则是 <span displaypfx="inline-" class="mathjax-container">\(1-p\)</span>。训练时通常配合二分类交叉熵（Binary Cross-Entropy, BCE）。工程上也可以输出二维 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\in\mathbb{R}^{2}\)</span>，再接 softmax；但若任务确实只有两个互斥类别，单 logit + sigmoid 更常见，也更经济。</p>
<div class="blog_h4"><span class="graybg">多分类</span></div>
<p>多分类（Multi-class Classification）要求每个样本在 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 个互斥类别中恰好选一个类别，例如“猫 / 狗 / 鸟”“体育 / 财经 / 科技 / 娱乐”。此时任务头不会只输出一个标量，而是输出长度为 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 的 logits 向量：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{z}=W\mathbf{h}+\mathbf{b},\qquad W\in\mathbb{R}^{C\times d},\ \mathbf{z}\in\mathbb{R}^{C}\]</span>
<p>其中第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 维 <span displaypfx="inline-" class="mathjax-container">\(z_c\)</span> 表示模型对第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 个类别的原始偏好分数。由于这些类别互斥，后续通常接 softmax，把整组分数归一化成概率分布：</p>
<span displaypfx="" class="mathjax-container">\[p(y=c\mid x)=\frac{e^{z_c}}{\sum_{j=1}^{C}e^{z_j}}\]</span>
<p>这条式子的含义很直接：分子 <span displaypfx="inline-" class="mathjax-container">\(e^{z_c}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 类的相对强度，分母 <span displaypfx="inline-" class="mathjax-container">\(\sum_{j=1}^{C}e^{z_j}\)</span> 把所有类别一起归一化，因此最终得到的 <span displaypfx="inline-" class="mathjax-container">\(p(y=c\mid x)\)</span> 落在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 之间，且所有类别概率和为 1。也正因为总和必须为 1，多分类头天然表达的是“类间竞争”：某一类概率上升，其他类的总概率就必须下降。</p>
<p>训练时通常直接把 logits <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 输入多分类交叉熵（Cross-Entropy）损失，而不是手动先算 softmax 再取对数。这样做的原因是数值稳定：损失函数内部会把 softmax 与对数合并成 log-sum-exp 形式，避免指数溢出。推理时若只关心类别标签，直接取 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_c z_c\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_c p(y=c\mid x)\)</span> 即可；若需要概率、阈值或校准，再显式使用 softmax。</p>
<div class="blog_h4"><span class="graybg">多标签分类</span></div>
<p>多标签分类（Multi-label Classification）与多分类名字相近，但任务结构完全不同。它不是“从多个类里选一个”，而是<span style="background-color: #c0c0c0;">同一个样本可以同时拥有多个标签</span>。例如一篇文章可以同时属于“AI、NLP、Transformer”，一张图片可以同时打上“室内、人物、宠物”三个标签。</p>
<p>这种任务里，标签之间不再互斥，因此不能使用 softmax。若仍用 softmax，所有标签概率会被强制归一化为和 1，相当于错误地假设“只能有一个标签成立”。多标签头通常输出 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 个 logits：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{z}=W\mathbf{h}+\mathbf{b},\qquad \mathbf{z}\in\mathbb{R}^{C}\]</span>
<p>但后续不是做一次整体 softmax，而是对每一维独立做 sigmoid：</p>
<span displaypfx="" class="mathjax-container">\[p_i=\sigma(z_i),\qquad i=1,\dots,C\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(p_i\)</span> 表示“第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个标签是否成立”的独立概率。训练时通常使用逐维二分类交叉熵，即把每个标签都当作一个独立的二分类问题，再在标签维上求和或取平均：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{multi\mbox{-}label}}=-\sum_{i=1}^{C}\Big(y_i\log p_i+(1-y_i)\log(1-p_i)\Big)\]</span>
<p>因此，多标签头的关键不是“输出维度也叫 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>”，而是<span style="background-color: #c0c0c0;">这 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 维之间不竞争，每一维都在独立回答一个 yes/no 问题</span>。推理时也不是取单个 argmax，而是对每一维做阈值判断，例如输出所有满足 <span displaypfx="inline-" class="mathjax-container">\(p_i&gt;0.5\)</span> 的标签，或按业务分别设定不同标签阈值。</p>
<div class="blog_h3"><span class="graybg">回归头</span></div>
<p>回归头（Regression Head）不负责在离散类别之间做判别，而是直接输出连续数值（Continuous Value）或连续向量。最常见的形式仍然是一层线性映射：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\mathbf{y}}=W\mathbf{h}+\mathbf{b},\qquad \hat{\mathbf{y}}\in\mathbb{R}^{m}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 是回归目标维度。若 <span displaypfx="inline-" class="mathjax-container">\(m=1\)</span>，就是标量回归，例如房价预测、评分预测、温度预测；若 <span displaypfx="inline-" class="mathjax-container">\(m&gt;1\)</span>，则是多维回归，例如边界框坐标回归、姿态参数回归或多目标数值预测。</p>
<p>回归头通常不接 softmax，也不强制输出落在 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span>。原因很简单：回归任务关心的是数值大小本身，而不是类别概率竞争。训练时常配合均方误差（MSE）、平均绝对误差（MAE）或 Huber Loss。只有当目标值本身有明确范围约束时，才会额外接 sigmoid、tanh 或其他变换，把输出压到指定区间。</p>
<div class="blog_h3"><span class="graybg">语言模型头（LM Head）</span></div>
<p>语言模型头（Language Modeling Head, LM Head）是把隐藏表示映射回词表空间的输出头。只要任务目标是“在若干位置上对词表中的 token 做预测”，就会出现这一类头；因此它不只存在于 Decoder-only 大模型，也存在于 Encoder-only 的掩码语言模型（Masked Language Model, MLM）以及 Encoder-Decoder 的生成端。它读取主干网络在每个位置输出的隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\;\times d_{\text{model}}}\)</span>，并把每个位置的表示投影到整张词表空间，得到词表 logits：</p>
<span displaypfx="" class="mathjax-container">\[Z = HW_{\text{vocab}} + \mathbf{1}b^\top,\quad W_{\text{vocab}}\in\mathbb{R}^{d_{\text{model}}\;\times {V}},\ Z\in\mathbb{R}^{L\;\times {V}}\]</span>
<p>这条式子先描述整体矩阵，再自然落到单个位置。这里 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 是序列长度，表示当前一共有多少个位置； <span displaypfx="inline-" class="mathjax-container">\(d_{\text{model}}\)</span> 是隐藏维度； <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 是词表大小（Vocabulary Size）。因此，隐藏状态矩阵 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\;\times d_{\text{model}}}\)</span> 的每一行对应一个位置的表示，输出权重矩阵 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab}}\in\mathbb{R}^{d_{\text{model}}\;\times {V}}\)</span> 的每一列对应“词表中某个 token 作为候选答案时的读出方向”，最终得到的 <span displaypfx="inline-" class="mathjax-container">\(Z\in\mathbb{R}^{L\;\times {V}}\)</span> 就是一个“位置 <span displaypfx="inline-" class="mathjax-container">\(\times\)</span> 词表”的打分表：行表示位置，列表示候选 token。</p>
<p>把这张打分表聚焦到第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 行，就得到 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\in\mathbb{R}^{V}\)</span>，也就是“第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置对整张词表所有 token 的一整行 logits”。这里 <span displaypfx="inline-" class="mathjax-container">\(H_t\in\mathbb{R}^{d_{\text{model}}}\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏状态，冒号 <span displaypfx="inline-" class="mathjax-container">\(:\)</span> 表示该行的全部列；若写成 <span displaypfx="inline-" class="mathjax-container">\(Z_{:,i}\)</span>，则表示第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 列，也就是“所有位置对第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个 token 的分数”。继续缩小到单个元素 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,i}\)</span>，它表示“在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置，把词表中第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个 token 作为下一个输出时的原始分数”。</p>
<p>把矩阵形式按单个位置、单个候选 token 展开后，打分可写成：</p>
<span displaypfx="" class="mathjax-container">\[Z_{t,i}=H_t\cdot W_{\text{vocab},:,i}+b_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab},:,i}\)</span> 表示输出权重矩阵的第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 列，也就是与词表第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个 token 对应的参数向量； <span displaypfx="inline-" class="mathjax-container">\(b_i\)</span> 是该 token 的偏置项。这个公式的读法是：拿第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏表示 <span displaypfx="inline-" class="mathjax-container">\(H_t\)</span>，与“token <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 的读出向量”做一次点积，再加偏置，就得到该 token 在该位置的 logit。</p>
<p>LM Head 与分类头的根本区别在于输出空间。普通分类头通常只需输出 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 个类别分数；LM Head 则要在每个位置输出 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 个分数，而 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 往往达到几万甚至几十万。因此，LM Head 本质上是“逐位置的大规模多分类器”：每一步都在问“下一个 token 应该是词表中的哪一个”。</p>
<p>同一个 LM Head 公式，在不同 Transformer 架构里的使用方式并不相同。对 Encoder-only 模型，LM Head 通常服务于掩码语言建模：模型先用双向注意力得到各位置隐藏状态，再只在被遮蔽的位置上读取 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\)</span> 来预测原 token；这一过程通常是一次性编码，不涉及自回归生成，也没有 KV Cache 逐步增长的问题。对 Decoder-only 模型，LM Head 用于 next-token 预测：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏状态对应预测 <span displaypfx="inline-" class="mathjax-container">\(x_{t+1}\)</span>，推理时会逐步生成，因此会配合因果注意力（Causal Self-Attention）和 KV Cache。对 Encoder-Decoder 模型，LM Head 位于解码器一侧：编码器先产出源序列表示，解码器再在因果自注意力与交叉注意力（Cross-Attention）的共同作用下生成目标侧隐藏状态，最后由 LM Head 映射到词表。</p>
<p>训练时，自回归语言模型（Autoregressive Language Model）通常采用 next-token 目标：第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置的隐藏状态 <span displaypfx="inline-" class="mathjax-container">\(H_t\)</span> 用来预测真实的下一个 token <span displaypfx="inline-" class="mathjax-container">\(x_{t+1}\)</span>。对应损失可写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{LM}}=-\sum_{t=1}^{L-1}\log \frac{\exp(Z_{t,x_{t+1}})}{\sum_{i=1}^{V}\exp(Z_{t,i})}\]</span>
<p>这个损失公式也可以逐项拆开理解。左边的 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}_{\mathrm{LM}}\)</span> 是整段序列的语言模型损失；求和下标 <span displaypfx="inline-" class="mathjax-container">\(t=1,\dots,L-1\)</span> 表示：前 <span displaypfx="inline-" class="mathjax-container">\(L-1\)</span> 个位置都要各自预测一次下一个 token。分子里的 <span displaypfx="inline-" class="mathjax-container">\(\exp(Z_{t,x_{t+1}})\)</span> 表示“真实下一个 token 在第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置对应的指数化分数”；这里 <span displaypfx="inline-" class="mathjax-container">\(x_{t+1}\)</span> 是真实的下一个 token id，所以 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,x_{t+1}}\)</span> 表示在位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 对这个真实 token 的 logit。分母 <span displaypfx="inline-" class="mathjax-container">\(\sum_{i=1}^{V}\exp(Z_{t,i})\)</span> 则把整张词表所有候选 token 的分数全部加起来做归一化。因此整个分式就是“在位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 预测真实下一个 token 的概率”，外面的负对数再把它变成训练损失。</p>
<p>推理时则不是直接输出标签，而是先对 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\)</span> 做 softmax 得到下一个 token 的条件分布。这里的 <span displaypfx="inline-" class="mathjax-container">\(Z_{t,:}\)</span> 不是一个标量，而是长度为 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 的向量，包含位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 对整张词表每个 token 的分数。对这整行做 softmax 后，得到的是：</p>
<span displaypfx="" class="mathjax-container">\[p(x_{t+1}=i\mid x_{\le t})=\frac{e^{Z_{t,i}}}{\sum_{j=1}^{V}e^{Z_{t,j}}},\qquad i=1,\dots,V\]</span>
<p>这条式子表示：在已经看到前缀 <span displaypfx="inline-" class="mathjax-container">\(x_{\le t}\)</span> 的条件下，下一个 token 取词表中第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个词的概率是多少。随后再配合贪心搜索（Greedy Decoding）、束搜索（Beam Search）、温度采样（Temperature Sampling）、top-k / top-p 等策略，从这组概率中选出真正生成的 token。也就是说，LM Head 负责把隐藏表示变成“词表级候选分数”，真正的文本生成还要再经过一层解码（Decoding）策略。</p>
<p>很多 LLM 还会使用权重共享（Weight Tying）：把输入嵌入表 <span displaypfx="inline-" class="mathjax-container">\(E\in\mathbb{R}^{V\;\times d_{\text{model}}}\)</span> 与输出头绑定，使 <span displaypfx="inline-" class="mathjax-container">\(W_{\text{vocab}}=E^\top\)</span>。这样一来，输入端“一个 token 的向量表示”与输出端“一个 token 作为候选答案时的原型向量”共用同一套参数空间。它通常既能减少参数量，也让输入和输出语义空间保持更强一致性。</p>
<p>从工程角度看，LM Head 往往比分类头更贵，因为它直接与词表大小 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 成正比：词表越大，最后一层的矩阵乘法、softmax 和采样都越重。因此，大模型的推理优化常会专门围绕 LM Head 展开，例如 fused softmax、采样优化、logits processor、词表裁剪或 speculative decoding 等。本质上，这些优化都在解决同一个问题：<span style="background-color: #c0c0c0;">如何更高效地从词表 logits 走到最终生成 token</span>。</p>
<div class="blog_h3"><span class="graybg">序列标注头（Token Classification）</span></div>
<p>序列标注（Sequence Labeling）/Token 分类（Token Classification）要求对每个 token 预测一个标签（例如 NER）。设主干输出 <span displaypfx="inline-" class="mathjax-container">\(H\in\mathbb{R}^{L\times d}\)</span>（长度 <span displaypfx="inline-" class="mathjax-container">\(L\)</span>，隐藏维 <span displaypfx="inline-" class="mathjax-container">\(d\)</span>），则逐 token 线性头为：</p>
<span displaypfx="" class="mathjax-container">\[Z=HW^\top+\mathbf{1}b^\top,\quad W\in\mathbb{R}^{C\times d},\ Z\in\mathbb{R}^{L\times C}\]</span>
<p>对第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个 token，用 softmax 得到 <span displaypfx="inline-" class="mathjax-container">\(p(y_t|x)\)</span> 并用逐 token 交叉熵训练。该做法把每个位置的标签看作条件独立，能工作，但会忽略标签之间的结构约束（例如 BIO 体系中不允许 <pre class="crayon-plain-tag">I-ORG</pre> 紧跟 <pre class="crayon-plain-tag">O</pre>）。</p>
<p>例：对 “Apple Inc. is in California” 的 NER（Named Entity Recognition），合理标签序列可能是 <pre class="crayon-plain-tag">B-ORG I-ORG O O B-LOC</pre>。若逐 token softmax 独立预测，模型可能输出不合法的组合；这类“结构错误”通常需要结构化任务头（Structured Head）来显式建模。</p>
<div class="blog_h4"><span class="graybg">条件随机场（CRF）</span></div>
<p>线性链条件随机场（Linear-chain Conditional Random Field, CRF）在 token 分类头上增加一个转移矩阵（Transition Matrix）<span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{C\times C}\)</span>，对整段标签序列做归一化建模。令发射分数（Emission Score）为 <span displaypfx="inline-" class="mathjax-container">\(s_t(y)=Z_{t,y}\)</span>，则序列 <span displaypfx="inline-" class="mathjax-container">\(y_{1:L}\)</span> 的总分为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{score}(x,y)=\sum_{t=1}^{L}\Big(A_{y_{t-1},y_t}+s_t(y_t)\Big)\]</span>
<p>并定义条件概率：</p>
<span displaypfx="" class="mathjax-container">\[p(y|x)=\frac{\exp(\mathrm{score}(x,y))}{\sum_{y'}\exp(\mathrm{score}(x,y'))}\]</span>
<p>训练最小化负对数似然（Negative Log-Likelihood）；解码用 Viterbi 算法（动态规划）求 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_y \mathrm{score}(x,y)\)</span>。CRF 的收益是把“标签合法性/连贯性”学进转移项，从而显著减少结构错误。</p>
<div class="blog_h3"><span class="graybg">双仿射头（Biaffine）</span></div>
<p>双仿射（Biaffine）任务头常用于“成对打分”（Pairwise Scoring），例如依存句法（Dependency Parsing）里的“当前词应依附到哪个词”，或关系抽取（Relation Extraction）里的“两个实体之间是否存在某种关系”。它的名字可以按层级直接理解：对一个变量， <span displaypfx="inline-" class="mathjax-container">\(Wx\)</span> 是线性， <span displaypfx="inline-" class="mathjax-container">\(Wx+b\)</span> 是仿射；对两个变量， <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span> 是双线性（Bilinear）；在这个双线性项之外再加上线性项和偏置项，就得到双仿射（Biaffine）。因此，双仿射的本质是：<span style="background-color: #c0c0c0;">既建模两个表示之间的交互，又保留各自单独的角色偏好</span>。</p>
<span displaypfx="" class="mathjax-container">\[s(i,j)=h_i^\top U h_j + w^\top [h_i;h_j] + b\]</span>
<p>这条式子可以逐项拆开读。 <span displaypfx="inline-" class="mathjax-container">\(h_i,h_j\in\mathbb{R}^{d}\)</span> 是两个待配对对象的表示向量；在依存句法里，它们可分别表示“当前词”和“候选 head”；在关系抽取里，它们可分别表示“实体 1”和“实体 2”。 <span displaypfx="inline-" class="mathjax-container">\(U\in\mathbb{R}^{d\times d}\)</span> 是双线性参数矩阵，因此 <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span> 建模的是二者之间的交互强度：不是只看 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 自己，也不是只看 <span displaypfx="inline-" class="mathjax-container">\(h_j\)</span> 自己，而是看“这两个向量放在一起是否匹配”。把它展开后就是 <span displaypfx="inline-" class="mathjax-container">\(\sum_{p=1}^{d}\sum_{q=1}^{d}(h_i)_p\,U_{pq}\,(h_j)_q\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(U_{pq}\)</span> 可以理解为“第 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 个 dependent 特征与第 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 个 head 特征同时出现时，该给多少分”。</p>
<p>若任务需要一次输出多类关系分数，工程实现通常会为每个类别各放一张矩阵 <span displaypfx="inline-" class="mathjax-container">\(U^{(k)}\)</span>，或直接使用三阶参数张量。这样不同关系类型就能拥有不同的交互模式，而不必共用同一套 <span displaypfx="inline-" class="mathjax-container">\(U\)</span>。</p>
<p><span displaypfx="inline-" class="mathjax-container">\([h_i;h_j]\in\mathbb{R}^{2d}\)</span> 表示把两个向量直接拼接； <span displaypfx="inline-" class="mathjax-container">\(w^\top [h_i;h_j]\)</span> 是普通仿射项，可以进一步拆成“只依赖 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 的线性项 + 只依赖 <span displaypfx="inline-" class="mathjax-container">\(h_j\)</span> 的线性项”。它的作用是补充单边信息：即使暂时不看两者之间的乘性交互，某些 token 或实体本身也可能更像 head、更像 dependent，或更像某类关系的一端。最后的 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是全局偏置，给整类配对提供一个基线分数。</p>
<p>具象地看， <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span> 像在问“这两个人之间是否搭得上”， <span displaypfx="inline-" class="mathjax-container">\(w^\top [h_i;h_j]\)</span> 像在问“这两个人各自单独看，是否本来就带某种角色倾向”。把两部分合起来，模型既能利用配对关系，也不会丢掉单个对象自身的类型线索。这正是双仿射通常比纯双线性更稳、更有表达力的原因。</p>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(s(i,j)\)</span> 对所有 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 组合都算出来，就能形成一个 <span displaypfx="inline-" class="mathjax-container">\(L\times L\)</span> 的分数矩阵；对每个位置 <span displaypfx="inline-" class="mathjax-container">\(i\)</span>，再在所有候选 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 上做 <span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 或 softmax 分类，就能得到“它最可能依附到谁”或“它与谁最可能存在某种关系”。这种头的关键点在于：任务监督信号作用在“对”的层面，而不是单点分类。</p>
<div class="blog_h3"><span class="graybg">解析式 NLP（Analytical NLP）任务选型</span></div>
<p>解析式 NLP（Analytical NLP）指一类“结构化输出”的语言任务：输出不是一段自由文本，而是标签序列、树或图。这类任务的工程难点通常不在 backbone，而在<span style="background-color: #c0c0c0;">输出结构约束、标注成本、以及错误后果</span>（例如信息抽取用于合规/风控）。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">任务</td>
<td style="text-align: center;">输出结构</td>
<td style="text-align: center;">常见任务头</td>
<td style="text-align: center;">数据/标注成本</td>
<td style="text-align: center;">蒸馏难度</td>
</tr>
</thead>
<tbody>
<tr>
<td>NER</td>
<td>BIO 标签序列</td>
<td>Token 分类；CRF（可选）</td>
<td>中（需要一致的标注规范）</td>
<td>低~中（教师模型能稳定给出 token-level 或 span-level 标签）</td>
</tr>
<tr>
<td>DEP（Dependency Parsing）</td>
<td>树（每 token 一个 head）</td>
<td>Biaffine / 图解析器（Parser）</td>
<td>高（标注复杂；语言差异大）</td>
<td>中~高（结构约束更强；蒸馏需覆盖长尾句式）</td>
</tr>
<tr>
<td>SDP（Semantic Dependency Parsing）</td>
<td>有向图（多 head / 多边）</td>
<td>图结构头（Graph Head）</td>
<td>很高（语义标注成本高）</td>
<td>高（输出空间更大；错误更难用局部规则修正）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">多任务与多头</span></div>
<p>同一个 backbone 可以挂多个任务头（Multi-head / Multi-task）：共享表示学习带来数据效率，但不同任务的梯度可能冲突，需要权衡损失权重（Loss Weighting）与采样策略。工程上也常见“同任务多头”：例如同时输出 token 标签与 span 边界，或同时输出检索 embedding 与分类 logits，用不同头对齐不同指标。</p>
<div class="blog_h2"><span class="graybg">反向传播</span></div>
<p>反向传播（Backpropagation）描述的是训练阶段中“如何把损失函数对输出的误差信号传回网络内部，并进一步求出各层参数梯度”的过程。理解这一章时，可以把它拆成三件事：前向传播先把值算出来，反向传播再把梯度传回去，而链式法则与 Jacobian 则给出这件事的数学形式。</p>
<div class="blog_h3"><span class="graybg">前向传播</span></div>
<p>前向传播（Forward Propagation）是训练与推理中首先发生的计算过程：给定输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 和当前参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，按照网络定义从前到后依次计算各层输出、最终预测 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span>，以及训练时对应的损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span>。若把网络写成函数复合</p>
<span displaypfx="" class="mathjax-container">\[h_1=f_0(x),\quad h_2=f_1(h_1),\quad \cdots,\quad h_L=f_{L-1}(h_{L-1}),\quad \hat y=g(h_L),\quad L=\ell(\hat y,y)\]</span>
<p>那么前向传播做的就是按 <span displaypfx="inline-" class="mathjax-container">\(x\to h_1\to h_2\to \cdots \to h_L\to \hat y \to L\)</span> 的顺序把这些值算出来。它回答的问题是：<span style="background-color: #c0c0c0;">在当前参数下，这个样本会被模型算成什么结果，以及这个结果与真实标签相差多大</span>。</p>
<p>前向传播的产物不仅是最终预测，还包括中间激活值（Activation）。这些中间量一方面构成模型当前这次推理的内部表示，另一方面也是后续反向传播计算梯度时必须依赖的节点。因此，训练过程总是先有前向传播，再有反向传播：前者负责算值，后者负责算这些值对参数的敏感度。</p>
<div class="blog_h3"><span class="graybg">反向传播</span></div>
<p>反向传播（Backpropagation）是深度学习里用于<span style="background-color: #c0c0c0;">高效计算梯度（Gradient）</span>的算法。训练神经网络的目标，是最小化损失函数（Loss Function）<span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span>；而要用梯度下降（Gradient Descent）或 Adam 之类的优化器更新参数，就必须先知道每个参数对损失的影响，也就是 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial \theta}\)</span>。</p>
<p>这个需求在深层网络里会立刻变得庞大。一个模型往往包含从数万到数十亿个参数；如果对每个参数都单独做一次数值扰动，逐个估计“它变一点时损失会怎么变”，计算代价会高得无法训练。反向传播出现的直接原因，就是要把这个原本几乎不可做的问题，变成一次前向传播（Forward Pass）加一次反向传播（Backward Pass）即可完成的梯度计算流程。</p>
<p>它的来源并不神秘：神经网络本质上是许多简单函数的复合。线性层、激活函数、归一化、注意力、损失函数，都会把前一层输出当作后一层输入。只要这些局部变换可导（Differentiable）或几乎处处可导，就可以对整条复合链应用链式法则（Chain Rule）。反向传播就是把链式法则改写成一种适合计算图（Computational Graph）执行的程序：<span style="background-color: #c0c0c0;">前向时保存必要的中间结果，反向时从最终损失出发，把局部导数一层层乘回去，并把梯度分发给每个中间变量和参数</span>。</p>
<p>因此，反向传播不是另一条独立于微积分的“神经网络专用规则”，而是链式法则在复合函数上的工程化实现。它做的事情可以概括为两步：第一步，前向传播先算出各层激活值与最终损失；第二步，从 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial L}=1\)</span> 出发，把损失敏感度沿着依赖边反向传回去，依次得到 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_l}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial W_l}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial b_l}\)</span> 等量。到这一步为止，反向传播完成的是<span style="background-color: #c0c0c0;">梯度计算</span>；参数真正如何更新，则属于优化器（Optimizer）的职责。</p>
<p>最简单的梯度下降（Gradient Descent）更新写成</p>
<span displaypfx="" class="mathjax-container">\[\theta \leftarrow \theta - \eta \nabla_{\theta} L\]</span>
<p>这里更适合写梯度符号 <span displaypfx="inline-" class="mathjax-container">\(\nabla_{\theta}L\)</span>，因为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 往往是整个参数向量或参数集合，而不是单个标量。若只讨论某一个标量参数，才可写成 <span displaypfx="inline-" class="mathjax-container">\(\theta \leftarrow \theta - \eta \frac{\partial L}{\partial \theta}\)</span>。从计算方式看，反向传播对应自动微分（Automatic Differentiation）中的反向模式（Reverse Mode AD）。它特别适合“输入参数很多、输出损失很少”的场景；而神经网络训练恰好就是这种结构：参数维度极大，但最终通常只关心一个标量损失。因此，反向传播成为现代深度学习训练的标准机制。</p>
<div class="blog_h4"><span class="graybg">一个最小手算例子</span></div>
<p>先看一个只多加一层激活函数（Activation）的最小网络。设输入 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>，参数为权重 <span displaypfx="inline-" class="mathjax-container">\(w=1\)</span> 与偏置 <span displaypfx="inline-" class="mathjax-container">\(b=-1\)</span>，先做仿射变换（Affine Transform）</p>
<span displaypfx="" class="mathjax-container">\[z=wx+b=1\cdot 2-1=1\]</span>
<p>再经过 ReLU 激活函数</p>
<span displaypfx="" class="mathjax-container">\[a=\mathrm{ReLU}(z)=\max(0,z)=1\]</span>
<p>把激活值当作最终预测，即 <span displaypfx="inline-" class="mathjax-container">\(\hat y=a\)</span>。若真实标签 <span displaypfx="inline-" class="mathjax-container">\(y=0\)</span>，损失取平方损失（Squared Loss）</p>
<span displaypfx="" class="mathjax-container">\[L=\frac12(\hat y-y)^2=\frac12(1-0)^2=0.5\]</span>
<p>前向传播链条现在是</p>
<span displaypfx="" class="mathjax-container">\[x\ \longrightarrow\ z\ \longrightarrow\ a=\hat y\ \longrightarrow\ L\]</span>
<p>反向传播先从损失对输出的导数开始：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial \hat y}=\hat y-y=1\]</span>
<p>由于这里 <span displaypfx="inline-" class="mathjax-container">\(\hat y=a\)</span>，所以</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial a}=1\]</span>
<p>再经过激活函数这一层。因为当前 <span displaypfx="inline-" class="mathjax-container">\(z=1&gt;0\)</span>，ReLU 的导数为</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial a}{\partial z}=\mathrm{ReLU}'(z)=1\]</span>
<p>于是梯度传回仿射层输出：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial z}=\frac{\partial L}{\partial a}\cdot\frac{\partial a}{\partial z}=1\cdot 1=1\]</span>
<p>最后再看仿射层对参数的局部导数：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial z}{\partial w}=x=2,\qquad \frac{\partial z}{\partial b}=1\]</span>
<p>因此</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w}=\frac{\partial L}{\partial z}\cdot\frac{\partial z}{\partial w}=1\cdot 2=2\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial b}=\frac{\partial L}{\partial z}\cdot\frac{\partial z}{\partial b}=1\cdot 1=1\]</span>
<p>若学习率 <span displaypfx="inline-" class="mathjax-container">\(\eta=0.1\)</span>，梯度下降更新后</p>
<span displaypfx="" class="mathjax-container">\[w\leftarrow 1-0.1\cdot 2=0.8,\qquad b\leftarrow -1-0.1\cdot 1=-1.1\]</span>
<p>这个版本比纯线性例子多了一站 <span displaypfx="inline-" class="mathjax-container">\(z\to a\)</span>，因此链式法则也多乘了一个局部导数 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial a}{\partial z}\)</span>。这正是反向传播的一般形式：<span style="background-color: #c0c0c0;">误差信号每穿过一层，就乘上这一层自己的局部导数</span>。若这里的 <span displaypfx="inline-" class="mathjax-container">\(z&lt;0\)</span>，则 ReLU 导数为 0，上游梯度也会在这一层被截断。</p>
<div class="blog_h3"><span class="graybg">链式法则与雅可比连乘</span></div>
<p>反向传播（Backpropagation）本质是链式法则（Chain Rule）在计算图（Computational Graph）上的系统化应用：把最终损失对中间变量的导数沿依赖关系向后传。</p>
<p>固定一个训练样本 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 后，损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 最终当然可以看成参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的函数；训练时真正要更新的也确实是权重（Weight）。但在计算图里， <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 并不是一步直接从 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 跳到损失，而是先经过各层线性变换、激活值（Activation）和输出再到损失。因此，反向传播会先求 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_l}\)</span> 这类“损失对中间激活值的敏感度”，再由这些中间梯度继续求出 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial \theta_l}\)</span>。</p>
<p>Jacobian 一节已经给出局部线性化：若 <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}=f_l(h_l)\)</span>，则在当前点附近有 <span displaypfx="inline-" class="mathjax-container">\(dh_{l+1}\approx J_l\,dh_l\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(J_l=\frac{\partial h_{l+1}}{\partial h_l}\)</span>。这条式子描述的是<span style="background-color: #c0c0c0;">输入扰动如何向前传到输出扰动</span>；反向传播关心的是相反方向的问题：输出端的损失敏感度如何传回输入端。</p>
<p>先看一层。由于损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 是标量，若采用与前文 Jacobian 一致的分子布局（Numerator Layout），把 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_l}\)</span> 记成 <span displaypfx="inline-" class="mathjax-container">\(1\;\times d_l\)</span> 的行向量，则向量链式法则写成：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_l}=\frac{\partial L}{\partial h_{l+1}}\frac{\partial h_{l+1}}{\partial h_l}=\frac{\partial L}{\partial h_{l+1}}J_l\]</span>
<p>这条式子最好按符号逐个读。先看 <span displaypfx="inline-" class="mathjax-container">\(h_l\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}\)</span>：它们分别表示第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层的输入表示和该层输出表示。若 <span displaypfx="inline-" class="mathjax-container">\(h_l\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(d_l\)</span> 个分量， <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(d_{l+1}\)</span> 个分量，那么</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_{l+1}}=\begin{bmatrix}\frac{\partial L}{\partial h_{l+1,1}} &amp; \frac{\partial L}{\partial h_{l+1,2}} &amp; \cdots &amp; \frac{\partial L}{\partial h_{l+1,d_{l+1}}}\end{bmatrix}\in\mathbb{R}^{1\;\times d_{l+1}}\]</span>
<p>它表示：损失 <span displaypfx="inline-" class="mathjax-container">\(L\)</span> 对第 <span displaypfx="inline-" class="mathjax-container">\(l+1\)</span> 层每个输出分量分别有多敏感。这里把它写成行向量，是因为前文采用的是分子布局（Numerator Layout）。</p>
<p>再看 Jacobian：</p>
<span displaypfx="" class="mathjax-container">\[J_l=\frac{\partial h_{l+1}}{\partial h_l}\in\mathbb{R}^{d_{l+1}\;\times d_l}\]</span>
<p>其中第 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 个元素是</p>
<span displaypfx="" class="mathjax-container">\[(J_l)_{ij}=\frac{\partial h_{l+1,i}}{\partial h_{l,j}}\]</span>
<p>意思是：第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个输入分量变化一点，会让第 <span displaypfx="inline-" class="mathjax-container">\(l+1\)</span> 层第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个输出分量变化多少。因此，Jacobian 记录的是“输入各分量 <span displaypfx="inline-" class="mathjax-container">\(\to\)</span> 输出各分量”的局部影响表。</p>
<p>现在看乘法</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_l}=\frac{\partial L}{\partial h_{l+1}}J_l\]</span>
<p>左边最终应该是一个关于 <span displaypfx="inline-" class="mathjax-container">\(h_l\)</span> 各分量的梯度，所以它必须有 <span displaypfx="inline-" class="mathjax-container">\(d_l\)</span> 个分量。维度上， <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_{l+1}}\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(1\;\times d_{l+1}\)</span>， <span displaypfx="inline-" class="mathjax-container">\(J_l\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(d_{l+1}\;\times d_l\)</span>，两者相乘后确实得到 <span displaypfx="inline-" class="mathjax-container">\(1\;\times d_l\)</span>。</p>
<p>如果把第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个分量单独展开，这个乘法其实就是</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_{l,j}}=\sum_{i=1}^{d_{l+1}}\frac{\partial L}{\partial h_{l+1,i}}\frac{\partial h_{l+1,i}}{\partial h_{l,j}}\]</span>
<p>这时含义就完全显出来了：第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个输入分量，会通过第 <span displaypfx="inline-" class="mathjax-container">\(l+1\)</span> 层的所有输出分量共同影响损失；因此要把“损失对每个输出分量的敏感度”乘上“该输出分量对当前输入分量的局部导数”，再对所有输出分量求和。矩阵乘法只是把这组求和一次性写成了紧凑形式。</p>
<p>把这条一层公式沿网络递推展开，对深度网络 <span displaypfx="inline-" class="mathjax-container">\(h_{l+1}=f_l(h_l)\)</span> 可得：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_0}=\frac{\partial L}{\partial h_L}J_{L-1}J_{L-2}\cdots J_0,\qquad J_l=\frac{\partial h_{l+1}}{\partial h_l}\]</span>
<p>这条公式的读法非常具体：前向传播按 <span displaypfx="inline-" class="mathjax-container">\(h_0\to h_1\to\cdots\to h_L\)</span> 计算；反向传播则从最终梯度 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_L}\)</span> 出发，依次乘上最后一层到第一层的 Jacobian，把敏感度一层层传回去。矩阵乘法满足结合律（Associativity），因此可以逐层累积；但不满足交换律，乘法顺序不能改，因为每个 Jacobian 都对应不同层的局部坐标变换。</p>
<p>一个最小可算例可以把这件事完全写开。设输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是标量，第一层有两个参数 <span displaypfx="inline-" class="mathjax-container">\(w_{11},w_{12}\)</span>，输出二维隐藏表示：</p>
<span displaypfx="" class="mathjax-container">\[h_1=\begin{bmatrix}h_{1,1}\\h_{1,2}\end{bmatrix}=\begin{bmatrix}w_{11}x\\w_{12}x\end{bmatrix}\]</span>
<p>第二层有两个参数 <span displaypfx="inline-" class="mathjax-container">\(w_{21},w_{22}\)</span>，把二维隐藏表示读成一个标量预测：</p>
<span displaypfx="" class="mathjax-container">\[\hat y=w_{21}h_{1,1}+w_{22}h_{1,2}\]</span>
<p>损失取最简单的平方损失（Squared Loss）：</p>
<span displaypfx="" class="mathjax-container">\[L=\frac12(\hat y-y)^2\]</span>
<p>这里总参数一共四个，但反向传播不会直接去“猜” <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial w_{11}}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial w_{12}}\)</span> 等结果，而是先沿着计算图写出中间量：</p>
<span displaypfx="" class="mathjax-container">\[x\ \longrightarrow\ h_1\ \longrightarrow\ \hat y\ \longrightarrow\ L\]</span>
<p>先看第二层的局部 Jacobian。因为 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span> 是标量、 <span displaypfx="inline-" class="mathjax-container">\(h_1\in\mathbb{R}^2\)</span>，所以</p>
<span displaypfx="" class="mathjax-container">\[J_1=\frac{\partial \hat y}{\partial h_1}=\begin{bmatrix}w_{21}&amp;w_{22}\end{bmatrix}\]</span>
<p>再看第一层的局部 Jacobian。因为 <span displaypfx="inline-" class="mathjax-container">\(h_1\in\mathbb{R}^2\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是标量，所以</p>
<span displaypfx="" class="mathjax-container">\[J_0=\frac{\partial h_1}{\partial x}=\begin{bmatrix}w_{11}\\w_{12}\end{bmatrix}\]</span>
<p>损失对最终输出的导数最容易先算：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial \hat y}=\hat y-y\]</span>
<p>于是，损失对隐藏表示的梯度由链式法则得到：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_1}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial h_1}=(\hat y-y)\begin{bmatrix}w_{21}&amp;w_{22}\end{bmatrix}\]</span>
<p>这一步已经把“损失对输出的敏感度”传回到了中间激活值 <span displaypfx="inline-" class="mathjax-container">\(h_1\)</span>。继续往回乘第一层 Jacobian，就得到损失对输入的梯度：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial x}=\frac{\partial L}{\partial \hat y}J_1J_0\]</span>
<span displaypfx="" class="mathjax-container">\[=(\hat y-y)\begin{bmatrix}w_{21}&amp;w_{22}\end{bmatrix}\begin{bmatrix}w_{11}\\w_{12}\end{bmatrix}\]</span>
<span displaypfx="" class="mathjax-container">\[=(\hat y-y)(w_{21}w_{11}+w_{22}w_{12})\]</span>
<p>对参数的梯度也是同样的思路。第二层参数直接作用在 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span> 上，因此</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{21}}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial w_{21}}=(\hat y-y)h_{1,1}\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{22}}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial w_{22}}=(\hat y-y)h_{1,2}\]</span>
<p>第一层参数不直接连到损失，而是先影响 <span displaypfx="inline-" class="mathjax-container">\(h_1\)</span>，再影响 <span displaypfx="inline-" class="mathjax-container">\(\hat y\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(L\)</span>，所以必须经过中间激活值这一站：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{11}}=\frac{\partial L}{\partial h_{1,1}}\frac{\partial h_{1,1}}{\partial w_{11}}=[(\hat y-y)w_{21}]\,x\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{12}}=\frac{\partial L}{\partial h_{1,2}}\frac{\partial h_{1,2}}{\partial w_{12}}=[(\hat y-y)w_{22}]\,x\]</span>
<p>若取一组具体数值 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(y=1\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{11}=1\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{12}=-1\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{21}=0.5\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(w_{22}=2\)</span>，则前向传播先得到</p>
<span displaypfx="" class="mathjax-container">\[h_1=\begin{bmatrix}2\\-2\end{bmatrix},\qquad \hat y=0.5\cdot 2+2\cdot(-2)=-3,\qquad \frac{\partial L}{\partial \hat y}=\hat y-y=-4\]</span>
<p>于是反向传播依次得到</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial h_1}=-4\begin{bmatrix}0.5&amp;2\end{bmatrix}=\begin{bmatrix}-2&amp;-8\end{bmatrix}\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial x}=\begin{bmatrix}-2&amp;-8\end{bmatrix}\begin{bmatrix}1\\-1\end{bmatrix}=6\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{21}}=-4\cdot 2=-8,\qquad \frac{\partial L}{\partial w_{22}}=-4\cdot(-2)=8\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial w_{11}}=(-2)\cdot 2=-4,\qquad \frac{\partial L}{\partial w_{12}}=(-8)\cdot 2=-16\]</span>
<p>这个最小例子把反向传播的结构完整展示了出来：损失函数最终是参数的函数，但梯度并不是“绕开中间层直接对权重求”。它必须先通过 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial \hat y}\)</span>、 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial L}{\partial h_1}\)</span> 这样的中间敏感度逐步回传。激活值因此不是“额外引入的变量”，而是计算图中必经的节点。</p>
<p>若改用深度学习里更常见的列向量梯度记号 <span displaypfx="inline-" class="mathjax-container">\(\nabla_{h_l}L\in\mathbb{R}^{d_l}\)</span>，同一件事会写成</p>
<span displaypfx="" class="mathjax-container">\[\nabla_{h_l}L=J_l^\top \nabla_{h_{l+1}}L\]</span>
<p>这与上面的公式没有本质区别，只是把“左乘 Jacobian 的行向量记号”改写成了“右侧乘 Jacobian 转置的列向量记号”。工程实现里常见的 Vector-Jacobian Product，本质上就是这一步。</p>
<p>对参数的梯度也是同一个模式。若第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层含参数 <span displaypfx="inline-" class="mathjax-container">\(\theta_l\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial L}{\partial \theta_l}=\frac{\partial L}{\partial h_{l+1}}\frac{\partial h_{l+1}}{\partial \theta_l}\]</span>
<p>因此 backward 的核心不是“把前向再算一遍”，而是复用每一层的局部 Jacobian，把同一个上游梯度分别传给输入变量和参数。</p>
<div class="blog_h3"><span class="graybg">梯度消失与爆炸</span></div>
<p>梯度消失（Vanishing Gradients）与梯度爆炸（Exploding Gradients）通常来自连乘的数值尺度：若这些雅可比的有效增益（Effective Gain）的奇异值（Singular Values）长期小于 1，则梯度指数级衰减；长期大于 1 则指数级放大。</p>
<p>在 RNN 的 BPTT 中，这种现象尤其明显：同一递归矩阵在时间轴上重复相乘。粗略地看，若 <span displaypfx="inline-" class="mathjax-container">\(W_{hh}\)</span> 的谱半径（Spectral Radius）<span displaypfx="inline-" class="mathjax-container">\(\rho(W_{hh})\)</span> 明显大于 1，梯度更易爆炸；明显小于 1 更易消失。但这不是“只要 <span displaypfx="inline-" class="mathjax-container">\(\lambda&gt;1\)</span> 就一定爆炸”的二选一结论，因为实际还受到激活函数导数、归一化、残差/门控结构、以及数据分布的共同影响。</p>
<p>“梯度随训练慢慢变小是否正常？”在接近最优点时，梯度范数下降是预期现象；需要警惕的是训练早期就出现系统性消失（例如大量饱和激活导致导数接近 0）或出现不稳定爆炸（loss/梯度频繁变成 NaN/Inf）。</p>
<p>常见应对：</p>
<ul>
<li>梯度裁剪（Gradient Clipping）：抑制爆炸。</li>
<li>合理初始化（Xavier/He）、归一化（BatchNorm/LayerNorm）、残差连接（Residual）。</li>
<li>门控结构（Gated Units）：LSTM/GRU 通过门控缓解长程梯度问题。</li>
</ul>
<div class="blog_h2"><span class="graybg">权重初始化</span></div>
<p>权重初始化（Weight Initialization）的目标不是“随机给一个小数”，而是控制信号在层间传播的数值尺度。设第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层的线性部分为 <span displaypfx="inline-" class="mathjax-container">\(z^{(l)}=W^{(l)}h^{(l-1)}+b^{(l)}\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(z^{(l)}\)</span> 的二阶原点矩（Second Raw Moment）在层间持续放大，前向激活与反向梯度都会倾向爆炸；若持续衰减，则会出现梯度消失。</p>
<p>对单个神经元，写成 <span displaypfx="inline-" class="mathjax-container">\(z=\sum_{i=1}^{n}w_i x_i+b\)</span>。初始化分析里通常进一步假设 <span displaypfx="inline-" class="mathjax-container">\(w_i\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 相互独立，且权重满足 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[w_i]=0\)</span>。这里的 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[w_i]=0\)</span> 不是在声称“训练后的权重天然应当均值为 0”，而是在分析初始化时，把权重看作从一个以 0 为中心的随机分布中抽样得到；这样做的目的，是避免网络在一开始就对某一方向产生系统性偏置，并让前向输出的均值计算更干净。基于这一初始化假设，在线性部分有</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[z]=0,\quad \mathbb{E}[z^2]=n\,\mathrm{Var}(w)\,\mathbb{E}[x^2]\]</span>
<p>当输入也近似零均值时， <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[x^2]\approx \mathrm{Var}(x)\)</span>，于是常写成 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(z)\approx n\,\mathrm{Var}(w)\,\mathrm{Var}(x)\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 就是 fan_in。因此初始化的核心约束可以概括为：让 <span displaypfx="inline-" class="mathjax-container">\(n\,\mathrm{Var}(w)\)</span> 保持在 1 附近。</p>
<p>更严格地说，深度初始化分析常跟踪的是 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[h^2]\)</span>，而不是所有层都精确使用方差；因为经过 ReLU 之后，激活不再零均值，此时二阶原点矩与方差不再完全相同。但在线性层与零均值近似下，两者是一致的，因此“保持方差稳定”仍是准确的工程表述。</p>
<p>两种朴素做法都会失败：若所有权重都初始化为 0，则各神经元保持完全对称，反向传播得到相同更新，网络退化为“许多拷贝的同一个单元”；若直接令 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)=1\)</span>，则线性输出的尺度会随层数按 fan_in 连乘，深层网络极易数值失稳。偏置（Bias）通常初始化为 0 或很小的常数；真正决定尺度的是权重矩阵的方差结构。</p>
<p>这里的 fan_in 与 fan_out 可以直接按“这层连接了多少路信号”来理解。对某个线性层中的一个输出神经元，fan_in 是流入它的输入通道数；输入通道越多，很多随机小贡献叠加后，输出就越容易变大。对某个输入神经元，fan_out 是它连接到下一层多少个输出通道；fan_out 越大，这个输入方向上的梯度在反向传播时就会被分发到更多支路。因此，fan_in 主要约束前向激活的放大量级，fan_out 主要约束反向梯度的放大量级。</p>
<p>具象地看，一个神经元像一个汇流节点：fan_in 决定有多少根水管把信号同时灌进来，fan_out 决定这股信号会被分流到多少个下游节点。初始化如果不考虑这两个连接数，网络就会在“汇流过猛”与“分流过弱”之间失衡。Xavier 正是在前向与反向之间做折中，He 则进一步把激活函数本身带来的能量损失也纳入补偿。</p>
<div class="blog_h3"><span class="graybg">Xavier 初始化</span></div>
<p>Xavier 初始化（Xavier Initialization）也称 Glorot 初始化（Glorot Initialization），针对近似线性的激活工作区间设计，例如 tanh 与 sigmoid 在原点附近的局部线性区域。它的目标是同时控制前向激活与反向梯度的尺度，使它们在穿过每一层时不发生系统性放大或衰减。</p>
<p>若暂时忽略激活函数对二阶原点矩的额外缩放，则前向保持尺度不变要求 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)\approx 1/\mathrm{fan\_in}\)</span>；反向对应地要求 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)\approx 1/\mathrm{fan\_out}\)</span>。Glorot 给出的折中形式因此写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(w)=\frac{2}{\mathrm{fan\_in}+\mathrm{fan\_out}}\]</span>
<p>常见实现包括正态版 <span displaypfx="inline-" class="mathjax-container">\(w\sim\mathcal{N}\!\left(0,\frac{2}{\mathrm{fan\_in}+\mathrm{fan\_out}}\right)\)</span>，以及均匀版 <span displaypfx="inline-" class="mathjax-container">\(w\sim U\!\left[-\sqrt{\frac{6}{\mathrm{fan\_in}+\mathrm{fan\_out}}},\sqrt{\frac{6}{\mathrm{fan\_in}+\mathrm{fan\_out}}}\right]\)</span>。</p>
<p>从直觉上看，Xavier 初始化像是在给每一层设置一个“不过度放大、也不过度压缩”的中性增益（neutral gain）。可以把一层线性变换想成一组并联的混音器：输入信号从上一层流入，经过许多权重通道混合后送到下一层。Xavier 的目标就是让这组混音器在初始状态下近似保持“总音量”不变，使信号既不会层层变得越来越吵，也不会层层变得越来越弱。</p>
<p>Xavier 的局限在于：它默认激活函数不会系统性丢失太多能量。对 ReLU 这类半波整流（Half-wave Rectification）激活，这个假设不再成立；即使线性层前后的尺度匹配，经过激活后信号的二阶原点矩仍会明显下降。</p>
<div class="blog_h3"><span class="graybg">He 初始化</span></div>
<p>He 初始化（He Initialization）也称 Kaiming 初始化（Kaiming Initialization），专门处理 ReLU 家族的整流效应。若线性输出 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 近似关于 0 对称，经过 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{ReLU}(z)=\max(0,z)\)</span> 后，大约一半的质量被截断为 0，并且</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[\mathrm{ReLU}(z)^2]=\frac{1}{2}\mathbb{E}[z^2]\]</span>
<p>因此，若仍使用 Xavier 量级，信号的二阶原点矩会随层数持续衰减。He 初始化通过把权重方差提高到</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(w)=\frac{2}{\mathrm{fan\_in}}\]</span>
<p>来补偿这一半波损失。对应实现可写为正态版 <span displaypfx="inline-" class="mathjax-container">\(w\sim\mathcal{N}\!\left(0,\frac{2}{\mathrm{fan\_in}}\right)\)</span>，或均匀版 <span displaypfx="inline-" class="mathjax-container">\(w\sim U\!\left[-\sqrt{\frac{6}{\mathrm{fan\_in}}},\sqrt{\frac{6}{\mathrm{fan\_in}}}\right]\)</span>。</p>
<p>具象地看，ReLU 像一道只允许正值通过的闸门：一批近似对称分布的信号经过后，负半边会被直接截成 0，等于天然损失了一部分能量。He 初始化做的事情就是在闸门前把信号预先放大一些，使它通过这道“半波闸门”之后，整体尺度仍能维持在稳定区间。Xavier 假定通道基本不漏能量，He 则明确把 ReLU 的漏损补偿计入初始化方差。</p>
<p>对 Leaky ReLU/PReLU，补偿因子可推广为 <span displaypfx="inline-" class="mathjax-container">\(\frac{2}{(1+a^2)\,\mathrm{fan\_in}}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 是负半轴斜率。实践上，ReLU 及其变体默认优先使用 He 初始化；tanh/sigmoid 更适合 Xavier。归一化层与残差连接可以进一步放宽初始化的容错区间，但不能替代合理的初始尺度控制。</p>
<div class="blog_h3"><span class="graybg">大语言模型里的初始化</span></div>
<p>Transformer / 大语言模型（Large Language Model, LLM）里的初始化通常不按“整模统一套 Xavier”或“整模统一套 He”来理解，而更像一套围绕<span style="background-color: #c0c0c0;">残差流（Residual Stream）稳定性、归一化层和深度扩展</span>设计的小方差初始化配方。原因在于：LLM 的主干不是简单的“线性层 + 单一激活函数”堆叠，而是由自注意力、残差连接、LayerNorm / RMSNorm、MLP / SwiGLU 等子结构共同组成；纯粹以某个激活函数为中心推导的 Xavier / He，只能覆盖其中一部分局部直觉。</p>
<p>工程上最常见的做法是：嵌入层与线性层权重用零均值的小方差正态分布初始化，偏置置零或省略；随后依靠 LayerNorm / RMSNorm 与残差结构维持训练初期的数值稳定。BERT、GPT-2 一类经典 Transformer 常见做法是使用标准差约为 <span displaypfx="inline-" class="mathjax-container">\(0.02\)</span> 的正态或截断正态初始化；很多更现代的 Decoder-only LLM 仍延续“小方差高斯初始化”这一主线，只是在具体投影层上再叠加按隐藏维度、层数或残差分支做缩放的配方。</p>
<p>从直觉上看，LLM 初始化更像是在给一条很深的多车道主干道设置初始车流密度。若注意力里的 <span displaypfx="inline-" class="mathjax-container">\(Q/K/V/O\)</span> 投影、FFN 的升维/降维投影以及其他会把结果写回残差流的线性层初始尺度过大，残差分支会把信号越叠越猛，训练初期容易震荡；若尺度过小，几十层上百层残差块叠起来后，真正进入有效学习区的信号又会太弱。因此，现代 LLM 初始化的重点往往不是“严格选 Xavier 还是 He”，而是<span style="background-color: #c0c0c0;">让残差支路在深层网络中既能传递信息，又不会在训练一开始就把数值尺度推离稳定区</span>。</p>
<p>具体到模块分工，也可以这样理解：MLP 内部若使用 ReLU 家族激活，He 的补偿思想仍然成立；若使用 GELU、SiLU、SwiGLU 这类更平滑或带门控的激活，则实现里往往直接采用统一的小方差正态初始化，再由归一化、残差和训练配方共同保证稳定性。换言之，LLM 并没有抛弃 Xavier / He 背后的“方差守恒”原则，而是把这套原则嵌入到了更完整的 Transformer 初始化策略里。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">方法</td>
<td style="text-align: center;">核心方差</td>
<td style="text-align: center;">适用激活</td>
<td style="text-align: center;">主要问题 / 逻辑</td>
</tr>
</thead>
<tbody>
<tr>
<td>零初始化</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)=0\)</span></td>
<td>无</td>
<td>破坏对称性；所有神经元学到相同特征</td>
</tr>
<tr>
<td>标准正态随机</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(w)=1\)</span></td>
<td>无</td>
<td>尺度随深度快速放大；易导致爆炸</td>
</tr>
<tr>
<td>Xavier / Glorot</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{2}{\mathrm{fan\_in}+\mathrm{fan\_out}}\)</span></td>
<td>Tanh、Sigmoid</td>
<td>兼顾前向与反向尺度；假设激活近似线性</td>
</tr>
<tr>
<td>He / Kaiming</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{2}{\mathrm{fan\_in}}\)</span></td>
<td>ReLU、Leaky ReLU</td>
<td>补偿 ReLU 的半波截断；保持二阶原点矩稳定</td>
</tr>
<tr>
<td>LLM 常用小方差高斯初始化</td>
<td>常取固定小标准差，或再叠加按宽度 / 深度缩放</td>
<td>Transformer 模块整体</td>
<td>服务残差流、归一化层与深层稳定训练；不是单纯按某一种激活推导</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">正则化</span></div>
<p>正则化（Regularization）在经验风险（Empirical Risk）上增加复杂度惩罚，缓解过拟合（Overfitting）。假设函数/模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span> 决定预测；样本损失 <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span> 度量单样本误差；代价函数/成本函数 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 汇总训练集误差；目标函数 <span displaypfx="inline-" class="mathjax-container">\(J(\theta)\)</span> 则是 <span displaypfx="inline-" class="mathjax-container">\(L(\theta)\)</span> 加上正则化项： <span displaypfx="inline-" class="mathjax-container">\(J(\theta)=L(\theta)+\lambda\Omega(\theta)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=\frac{1}{m}\sum_{i=1}^{m}\ell\!\left(f_{\theta}(x^{(i)}),y^{(i)}\right)+\lambda\Omega(\theta)\]</span>
<div class="blog_h3"><span class="graybg">L1 正则化（Lasso）</span></div>
<p>L1 正则化（L1 Regularization）使用 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)=\|\theta\|_1\)</span>。它倾向产生稀疏解（Sparsity），即一部分权重被直接压到 0，因此可同时做参数学习和特征选择（Feature Selection）。</p>
<p>从优化角度看，L1 的梯度是次梯度（Subgradient）：对单个参数 <span displaypfx="inline-" class="mathjax-container">\(\theta_k\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(\theta_k\ne 0\)</span> 时有 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial}{\partial \theta_k}\|\theta\|_1=\mathrm{sign}(\theta_k)\)</span>；在 0 点是一段区间 <span displaypfx="inline-" class="mathjax-container">\([-1,1]\)</span>。这使得优化过程更容易把小权重“推过 0”，形成精确稀疏。</p>
<p>在很多实现中，会用近端算子（Proximal Operator）给出更清晰的“压到 0”机制：对标量 <span displaypfx="inline-" class="mathjax-container">\(w\)</span>，软阈值化（Soft-Thresholding）是</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{soft}(w,\alpha)=\mathrm{sign}(w)\max(|w|-\alpha,0)\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(|w|\le \alpha\)</span> 时直接变为 0。</p>
<div class="blog_h3"><span class="graybg">L2 正则化（Ridge / Weight Decay）</span></div>
<p>L2 正则化（L2 Regularization）使用 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)=\|\theta\|_2^2\)</span>。它倾向均匀缩小参数，但通常不会把参数精确压到 0。直观上，L1 在 0 点不可导，更容易触发“阈值化”解；L2 是光滑二次惩罚，更新更连续。</p>
<p>梯度层面，L2 会在原梯度上叠加一个“拉回原点”的项：若目标为 <span displaypfx="inline-" class="mathjax-container">\(\ell(\theta)+\lambda\|\theta\|_2^2\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta=\nabla \ell(\theta)+2\lambda\theta\)</span>。工程上常把这个效果称为权重衰减（Weight Decay）：每一步都把权重按比例缩小。</p>
<p>需要区分一个常见细节：在自适应优化器（如 Adam）里，“把 <span displaypfx="inline-" class="mathjax-container">\(\lambda\|\theta\|_2^2\)</span> 加到损失里”与“直接做 weight decay”在数值上并不完全等价；因此实践中常用解耦权重衰减（Decoupled Weight Decay，典型实现是 AdamW）来获得更可控的正则化行为。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/regularization-l1-l2-geometry.png"><img class="alignnone size-full" src="https://blog.gmem.cc/wp-content/uploads/2026/03/regularization-l1-l2-geometry.png" alt="regularization-l1-l2-geometry" width="1920" height="930" /></a></p>
<p>把带惩罚的目标改写成约束形式后，几何差异会非常直观： <span displaypfx="inline-" class="mathjax-container">\(\min_\theta L(\theta)\ \mathrm{s.t.}\ \|\theta\|_1\le t\)</span> 的可行域在二维里是菱形， <span displaypfx="inline-" class="mathjax-container">\(\min_\theta L(\theta)\ \mathrm{s.t.}\ \|\theta\|_2\le t\)</span> 的可行域则是圆盘。损失等高线从外向内收缩时，第一次接触 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 边界，往往更容易落在角点；角点恰好对应某些坐标精确等于 0。 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 边界处处光滑，接触点通常只是把所有坐标一起缩小，而不是把其中一部分直接压成 0。</p>
<p>从一维更新机制看，这个差异也对应两种不同的收缩方式。 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 更像连续比例收缩：权重越大，拉回原点的力越强，但通常不会在有限步内变成精确 0。 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 则对应软阈值化（Soft-Thresholding）：一旦 <span displaypfx="inline-" class="mathjax-container">\(|w|\)</span> 小于阈值，就会被直接压成 0。因此， <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 不只是“把权重变小”，而是会主动把一部分坐标从模型中删掉，这就是稀疏性（Sparsity）的来源。</p>
<div class="blog_h3"><span class="graybg">Dropout</span></div>
<p>Dropout 通过随机屏蔽部分神经元输出，减少共适应（Co-adaptation），等价于在训练时对网络做一种随机子网络集成（Ensemble）。这里的共适应，是指若干神经元彼此形成了固定搭配：模型过度依赖它们同时出现、按特定组合共同完成判断，而不是让每个单元都学到相对独立、稳健的特征。Dropout 随机拿掉其中一部分单元后，网络不能再把能力押注在某一组固定配合上，只能把有用模式分散到更稳健的表示里。令隐藏向量为 <span displaypfx="inline-" class="mathjax-container">\(h\)</span>，mask <span displaypfx="inline-" class="mathjax-container">\(m_i\sim \mathrm{Bernoulli}(p)\)</span>，常见的 inverted dropout 写法为：</p>
<span displaypfx="" class="mathjax-container">\[\tilde h = \frac{m\odot h}{p}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是保留概率（Keep Probability）。这样推理时可直接使用原网络（不再采样 mask），避免额外缩放。</p>
<div class="blog_h3"><span class="graybg">Batch Normalization</span></div>
<p>Batch Normalization（BatchNorm）在训练时用 mini-batch 的均值/方差做归一化，并学习缩放/平移参数。对特征维上的某个分量 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，典型形式是：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{BN}(x)=\gamma\cdot \frac{x-\mu_{\text{batch}}}{\sqrt{\sigma_{\text{batch}}^2+\epsilon}}+\beta\]</span>
<p>推理阶段通常使用训练过程累积的运行均值（Running Mean）与运行方差（Running Variance），因此训练/推理行为不同。BatchNorm 在 CNN 中极常见，因为卷积特征图在同一通道上的空间位置具有较强同质性，跨样本统计量较容易稳定；但在 Transformer 中，主流做法并不是沿 batch 维做归一化，而是使用与 batch 统计无关的 LayerNorm 或 RMSNorm。</p>
<p>原因在于 Transformer 的基本计算单元是 token 表示。序列长度常常可变，batch size 在训练与推理中也经常变化，尤其大模型训练会受到显存限制而使用较小、波动甚至分布式切分后的 micro-batch。若使用 BatchNorm，某个 token 的归一化结果会显式依赖同一 batch 中其他样本与其他位置的统计量，这会引入跨样本耦合，使训练和推理的数值语义不一致，也不利于自回归解码阶段逐 token 稳定生成。</p>
<div class="blog_h3"><span class="graybg">Layer Normalization</span></div>
<p>Layer Normalization（LayerNorm）对每个样本（或每个 token）的特征维做归一化，不依赖 batch 统计量，因此更适合变长序列与自回归推理。对向量 <span displaypfx="inline-" class="mathjax-container">\(x\in\mathbb{R}^{d}\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{LN}(x)=\gamma\odot \frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}+\beta,\quad \mu=\frac{1}{d}\sum_{i=1}^{d}x_i,\ \sigma^2=\frac{1}{d}\sum_{i=1}^{d}(x_i-\mu)^2\]</span>
<p>这里的均值 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 与方差 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 都只在当前样本、当前 token 的特征维内部计算，因此每个 token 都能独立完成归一化。这个性质与 Transformer 的残差流（Residual Stream）非常匹配：无论 batch 如何变化、序列如何裁剪、推理时是否一次只输入一个 token，归一化规则都保持一致。</p>
<p>当前主流的 Transformer 归一化实践可以概括为两类。Encoder-only 与很多 Vision Transformer（ViT）架构通常沿用 LayerNorm；Decoder-only 大语言模型（Large Language Model, LLM）则大量采用 Pre-Norm 残差块，并进一步把 LayerNorm 简化为 RMSNorm。前者保留去均值与方差缩放，后者只保留尺度归一化，计算更轻，也更适合超大规模训练。</p>
<div class="blog_h3"><span class="graybg">Early Stopping</span></div>
<p>Early Stopping 用验证集指标监控训练过程，在泛化性能不再提升时提前停止，从而避免在训练集上继续拟合噪声。工程上常用“耐心（Patience）”：若验证集指标连续 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 次评估都未改善，则停止训练并回滚到最佳 checkpoint。它属于训练流程级的隐式正则化（Implicit Regularization）。</p>
<div class="blog_h3"><span class="graybg">分类任务正则化</span></div>
<p>分类任务正则化（Regularization for Classification Tasks）直接作用在分类训练的监督方式、样本混合方式或概率分布形状上。它的主要目标包括：缓解过度自信、减轻类别不平衡造成的梯度偏置、降低标签噪声影响，以及让决策边界在训练样本之间保持更平滑的过渡。</p>
<div class="blog_h4"><span class="graybg">Label Smoothing</span></div>
<p>Label Smoothing（标签平滑）把 one-hot 标签从“真实类为 1、其余类为 0”改写成更平滑的目标分布。对 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 分类问题，常见写法是：</p>
<span displaypfx="" class="mathjax-container">\[y_i^{\mathrm{LS}}=(1-\varepsilon)\mathbf{1}[i=c]+\frac{\varepsilon}{C}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 是真实类别， <span displaypfx="inline-" class="mathjax-container">\(\varepsilon\in(0,1)\)</span> 是平滑系数。真实类别仍占最大权重，但其他类别也会分到一小部分概率质量。训练因此不再持续奖励模型把正确类别概率推到 1、把其余类别压到 0，输出分布通常会更平滑，概率校准（Calibration）也更稳定。</p>
<p>在固定阈值或直接取 <span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 的分类系统里，Label Smoothing 带来的收益往往更多体现在 loss 曲线与概率可信度上，而不是直接转化为同等幅度的 F1 提升。若模型输出概率还会进入阈值调优、排序、融合、拒识或风险控制，这种校准改进的价值会更明显。</p>
<div class="blog_h4"><span class="graybg">类别重加权与重采样</span></div>
<p>类别重加权（Class Reweighting）与重采样（Resampling）主要处理类别不平衡（Class Imbalance）。其核心思想是让少数类样本在训练中获得更大的有效权重，避免优化过程被大量易分类的多数类样本主导。加权交叉熵的常见形式为：</p>
<span displaypfx="" class="mathjax-container">\[\ell_{\mathrm{wCE}}(y,p)=-\sum_{i=1}^{C}\alpha_i y_i\log p_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 类的损失权重。若某类样本极少，可以给它更大的 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>，使模型在误分该类时承担更高代价。重采样则直接改变 mini-batch 的类别组成，例如过采样少数类、欠采样多数类，或用类别均衡采样器（Class-balanced Sampler）保证 batch 内标签分布更均衡。</p>
<p>这类方法的直接效果，是让决策边界不再默认偏向多数类。代价则是：过强的重加权会放大少数类中的噪声标签，过强的过采样会提高过拟合风险。因此它通常要与验证集上的 Precision / Recall / F1 一起调节，而不只盯着训练损失。</p>
<div class="blog_h4"><span class="graybg">Mixup 与 CutMix</span></div>
<p>Mixup 与 CutMix 通过构造“介于两个训练样本之间”的新样本，显式约束分类器在样本间插值区域上的行为，从而平滑决策边界。Mixup 的典型形式是：</p>
<span displaypfx="" class="mathjax-container">\[\tilde x=\lambda x_i+(1-\lambda)x_j,\qquad \tilde y=\lambda y_i+(1-\lambda)y_j\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\lambda\in[0,1]\)</span> 通常从 Beta 分布采样。输入和标签都被线性混合，于是模型被要求在 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(x_j\)</span> 之间给出相应的插值预测。CutMix 则不是对整张输入做加权平均，而是把一张图像中的局部区域替换为另一张图像的对应区域，同时按被替换面积比例混合标签。</p>
<p>这类方法对图像分类尤其有效，因为它们直接惩罚“决策边界贴着训练样本走”的过拟合行为。换一个视角看，Mixup / CutMix 并不是简单的数据增强，而是在告诉模型：输入空间里两点之间的过渡区域也应当保持语义上的平滑可解释。</p>
<div class="blog_h4"><span class="graybg">置信度惩罚与熵正则</span></div>
<p>置信度惩罚（Confidence Penalty）和熵正则（Entropy Regularization）直接约束输出分布不要过早塌缩到极端尖锐形状。一个常见做法是在交叉熵之外，再加入预测分布熵的奖励项：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}=\mathcal{L}_{\mathrm{CE}}-\beta H(p),\qquad H(p)=-\sum_{i=1}^{C}p_i\log p_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\beta&gt;0\)</span> 控制正则强度。由于熵越大表示分布越平滑，这一项会抑制模型过快形成极端概率。它和 Label Smoothing 的方向相近，但切入点不同：Label Smoothing 改的是监督目标分布，置信度惩罚改的是模型预测分布本身。</p>
<p>两者都常用于需要更好概率校准的分类系统。相比之下，Focal Loss 更强调“把梯度预算留给困难样本”，因此它在类别不平衡或难例挖掘场景更常见；Label Smoothing 与熵正则则更偏向控制过度自信与改善概率形状。</p>
<div class="blog_h3"><span class="graybg">回归任务正则化</span></div>
<p>回归任务正则化（Regularization for Regression Tasks）除了常见的参数惩罚外，还经常直接约束预测函数 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 的形状。回归目标是连续值，因此“曲线是否足够平滑、是否满足单调关系、是否具有合理曲率”往往和任务正确性本身直接相关。</p>
<div class="blog_h4"><span class="graybg">平滑性正则化与样条惩罚</span></div>
<p>平滑性正则化（Smoothness Regularization）要求回归函数不要在输入空间里出现不必要的高频震荡。最典型的形式是惩罚导数，尤其是二阶导数：</p>
<span displaypfx="" class="mathjax-container">\[J(f)=\sum_{i=1}^{N}(y_i-f(x_i))^2+\lambda\int (f''(x))^2\,dx\]</span>
<p>第二项就是经典的样条平滑惩罚（Smoothing Spline Penalty）。它惩罚曲率过大，相当于抑制函数频繁弯折。 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越大，拟合曲线越平滑； <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 越小，模型越愿意追随样本中的局部波动。很多非参数回归（Nonparametric Regression）与时间序列平滑，本质上都在做这种“数据拟合 + 曲率惩罚”的权衡。</p>
<div class="blog_h4"><span class="graybg">Total Variation 与 Fused Lasso</span></div>
<p>Total Variation（TV）正则与 Fused Lasso 适合分段平滑（Piecewise Smooth）的回归目标。它们不强求函数处处光滑，而是允许少数突变点存在，同时惩罚过多的相邻跳变。离散形式的 TV 惩罚常写成：</p>
<span displaypfx="" class="mathjax-container">\[\Omega_{\mathrm{TV}}(f)=\sum_{t=2}^{T}|f_t-f_{t-1}|\]</span>
<p>若同时对参数本身加 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 惩罚，再对相邻参数差分加 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 惩罚，就得到 Fused Lasso：</p>
<span displaypfx="" class="mathjax-container">\[J(\beta)=L(\beta)+\lambda_1\sum_j |\beta_j|+\lambda_2\sum_{j=2}^{d}|\beta_j-\beta_{j-1}|\]</span>
<p>这类方法非常适合信号去噪、时序回归、基因拷贝数分段估计，以及任何“整体大致平稳、局部允许少数结构突变”的问题。与样条惩罚相比，它更偏好形成平坦区段，而不是全局光滑弯曲曲线。</p>
<div class="blog_h4"><span class="graybg">单调性约束</span></div>
<p>很多回归任务天然带有单调先验：广告出价上升，曝光概率通常不应系统性下降；贷款风险特征上升，违约风险不应系统性降低；药物剂量增加，效应在一定区间内通常应单调增强。单调性约束（Monotonicity Constraint）把这种领域知识直接写进模型：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial f(x)}{\partial x_k}\ge 0\quad \text{或}\quad \frac{\partial f(x)}{\partial x_k}\le 0\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x_k\)</span> 是某个具有明确方向含义的特征。在线性模型里，这等价于约束对应权重非负或非正；在树模型和神经网络里，则可以通过结构限制、投影步骤或软惩罚项来实现。单调约束的价值不只在提高泛化，还在于提升可解释性与业务一致性。</p>
<div class="blog_h4"><span class="graybg">凸性与曲率约束</span></div>
<p>当回归函数预期具有凸性（Convexity）、凹性（Concavity）或有限曲率时，可以继续对二阶导数施加方向性约束。例如一维凸函数满足：</p>
<span displaypfx="" class="mathjax-container">\[f''(x)\ge 0\]</span>
<p>这类约束在成本函数建模、供需曲线估计、剂量反应建模和某些经济学回归问题里非常常见。即使不要求严格凸性，也常通过曲率上界控制函数不要弯折过猛，例如惩罚 Hessian 范数或二阶差分幅度。它们的作用与平滑惩罚相近，但强调的是“弯曲方向和弯曲强度应满足领域结构”，而不只是单纯地压低高频波动。</p>
<div class="blog_h4"><span class="graybg">Lipschitz 与梯度约束</span></div>
<p>Lipschitz 约束（Lipschitz Constraint）控制的是输入微小变化会把输出放大多少。若存在常数 <span displaypfx="inline-" class="mathjax-container">\(K\)</span>，使得</p>
<span displaypfx="" class="mathjax-container">\[|f(x)-f(x')|\le K\|x-x'\|\]</span>
<p>则函数变化速度受到统一上界控制。对可导函数，一个常见做法是直接惩罚输入梯度范数：</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=L(\theta)+\lambda\,\mathbb{E}_{x}\|\nabla_x f_\theta(x)\|_2^2\]</span>
<p>这种正则化常用于需要鲁棒输出的回归系统，例如物理量估计、坐标回归和噪声敏感的传感器建模。它抑制模型对局部输入扰动过度敏感，也能缓解高维回归中出现的不稳定尖峰。</p>
<div class="blog_h4"><span class="graybg">概率回归的分布约束</span></div>
<p>概率回归（Probabilistic Regression）不仅预测均值，还会预测方差、分位数或整个条件分布。此时正则化对象不再只有均值函数，还包括分布参数之间的结构关系。例如异方差回归（Heteroscedastic Regression）中，方差参数必须保持正值；分位数回归（Quantile Regression）中，不同分位点曲线应尽量避免交叉（Quantile Crossing）。</p>
<p>若模型同时预测多个分位点 <span displaypfx="inline-" class="mathjax-container">\(\hat q_{\tau_1}(x),\hat q_{\tau_2}(x)\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\tau_1&lt;\tau_2\)</span>，则理想上应满足：</p>
<span displaypfx="" class="mathjax-container">\[\hat q_{\tau_1}(x)\le \hat q_{\tau_2}(x)\]</span>
<p>工程上常通过排序约束、投影修正或惩罚项来维持这种分布一致性。对高斯 NLL、混合密度网络（Mixture Density Network）或生存分析模型，也会对尺度参数、危险率函数或累积分布形状加入额外约束，使预测分布既拟合数据，又保持统计上可解释、数值上稳定。</p>
<div class="blog_h1"><span class="graybg">深度学习</span></div>
<p>深度学习（Deep Learning）是以多层神经网络为核心、通过大规模数据和梯度优化自动学习表示的建模范式。若上一章讨论的是神经网络的基本部件，例如线性层、激活函数、损失函数、反向传播、初始化与正则化；这一章讨论的则是这些部件在更深层、更大规模、更强归纳偏置下，如何组合成现代模型家族，并在视觉、语音、语言、生成和图结构任务上形成方法论分水岭。</p>
<p>“深度”并不只是层数更多。更关键的变化是：模型开始把原始输入逐层改写成越来越抽象的中间表示，从边缘、纹理、局部模式，逐步组合到部件、对象、语义关系与任务决策。于是，模型能力的来源不再只是最后那一层分类器，而是整条表示变换链本身。</p>
<p>下文的卷积神经网络（CNN）、循环神经网络（RNN）、Transformer、生成模型（Generative Model）、图神经网络（GNN）和 ONNX，分别对应深度学习里几条非常重要的主线：空间局部归纳偏置、时序递推建模、自注意力驱动的通用序列建模、生成式分布学习、关系数据表示学习，以及模型部署交换格式。它们共同构成了现代深度学习的主要版图。其中 Transformer 因为后续内容体量很大，会在第 3 篇中单独展开；这里先给出它在技术演进中的位置。</p>
<div class="blog_h2"><span class="graybg">表示学习与端到端学习</span></div>
<p>在传统机器学习流程里，特征工程和预测器常常是分开的：先由人手设计特征，再把这些特征交给线性模型、树模型或核方法完成分类与回归。深度学习把这两步合并进同一个可微计算图。前面的层负责把原始输入转写为更有判别力、更有结构感的表示，后面的层负责完成具体任务，所有参数围绕同一个目标函数联合优化。这就是端到端学习（End-to-End Learning）的核心含义。</p>
<p>表示学习（Representation Learning）之所以重要，是因为感知任务真正困难的部分，往往不在“最后怎么分一下类”，而在“怎样把原始信号变成对任务友好的坐标系”。图像像素、语音波形、文本序列和图结构都高度高维、局部相关且语义分散。深度网络的价值，在于它能用层级结构自动提取适合当前任务的中间表示，而不再完全依赖人工定义纹理统计量、边界特征、语言规则或图特征模板。</p>
<p>这也解释了为什么深度学习会改变整个方法栈。过去很多系统的主要工作量放在“特征怎么造”；深度学习之后，工作重心逐渐转向“架构如何设计、数据如何构造、训练如何稳定、预训练如何迁移、部署如何落地”。从研究到工业实践，核心竞争力开始沿着表示学习能力重新分布。</p>
<div class="blog_h2"><span class="graybg">为什么深度学习成为里程碑</span></div>
<p>多层神经网络并不是 2010 年代才出现的新概念。真正的转折点在于，一系列条件在同一时期同时成熟，使深网络第一次具备了大规模可训练、可迁移、可复用的现实基础。深度学习成为里程碑，靠的不是单一公式突破，而是数据、算力、优化、架构和软件工程几条线一起闭环。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">关键条件</td>
<td style="text-align: center;">作用</td>
<td style="text-align: center;">为什么重要</td>
</tr>
</thead>
<tbody>
<tr>
<td>大规模数据</td>
<td>提供足够多样的监督信号与统计规律</td>
<td>深网络参数量大，若没有足够样本，表达能力会迅速转化为过拟合风险</td>
</tr>
<tr>
<td>GPU / 并行算力</td>
<td>把大规模矩阵乘法、卷积和反向传播变成可承受的训练过程</td>
<td>很多深层模型在理论上可定义，但在工程上长期“训不动”</td>
</tr>
<tr>
<td>优化技术成熟</td>
<td>ReLU、Xavier / He 初始化、BatchNorm、残差连接、门控结构等共同提升可训练性</td>
<td>它们解决的是梯度消失、数值失稳和深层退化等根本瓶颈</td>
</tr>
<tr>
<td>强归纳偏置的架构</td>
<td>CNN 利用空间局部性，RNN 利用时序递推，GNN 利用图邻接关系</td>
<td>深度并不自动等于有效，架构必须贴合数据结构</td>
</tr>
<tr>
<td>预训练与迁移学习</td>
<td>先在大数据上学通用表示，再迁移到下游任务</td>
<td>这使深度学习从“每个任务从零训练”转向“共享表征资产”</td>
</tr>
<tr>
<td>框架与工程生态</td>
<td>自动求导、分布式训练、模型导出和部署工具日益成熟</td>
<td>研究原型和生产系统之间的距离被显著缩短</td>
</tr>
</tbody>
</table>
<p>从方法史角度看，深度学习真正改变的是“模型从何处获得有用表示”。经典机器学习更多依赖人工抽取特征，再用较浅模型做判别；深度学习则把表示构造本身纳入训练。这个变化一旦与数据和算力结合，就会呈现出非常强的规模效应。</p>
<div class="blog_h2"><span class="graybg">关键技术演进</span></div>
<div class="blog_h3"><span class="graybg">AlexNet 与视觉模型复兴</span></div>
<p>AlexNet 是现代深度学习史上的标志性节点。它的意义不只是 ImageNet 分类精度显著提升，而是证明了一个足够深、足够宽、用 GPU 训练、配合 ReLU 和数据增强的卷积网络，能够系统性压过人工特征加浅层分类器的旧路线。视觉领域由此从“设计特征”转向“训练特征”。</p>
<p>这次转折的后果极其深远。分类只是起点，检测、分割、检索、视频理解和视觉问答很快都转向以深层 backbone 为中心的范式。很多后续工作不再从零设计整套视觉特征，而是围绕预训练卷积网络做迁移、微调和多任务扩展。</p>
<div class="blog_h3"><span class="graybg">Seq2Seq 与 Attention</span></div>
<p>在序列任务上，深度学习的关键进展并不只来自更强的 RNN / LSTM / GRU，还来自编码器-解码器（Encoder-Decoder）和注意力（Attention）机制。Seq2Seq 让模型能够把输入序列映射为输出序列，最早大规模改变了机器翻译、语音识别和摘要生成等任务；注意力则突破了“所有信息都必须挤进一个固定长度向量”的瓶颈，使解码器可以在生成每一步时，动态读取输入序列中最相关的部分。</p>
<p>这条技术线的重要性在于，它把序列建模从“单纯递推记忆”推进到“按需访问上下文”。Transformer 正是在这条线上进一步把注意力从辅助机制推到主干架构，因此 Seq2Seq 与 Attention 构成了通往下一篇 Transformer 主线的直接前史。</p>
<div class="blog_h3"><span class="graybg">Transformer</span></div>
<p>Transformer 可以看作深度学习在序列建模上的又一次架构级跃迁。它不再把循环或卷积作为主干，而是以自注意力（Self-Attention）为核心，让每个位置都能直接与其他位置建立依赖关系。这样做的结果是：长距离关系更容易建模，训练更适合并行化，模型也更容易沿着数据规模、参数规模和上下文长度继续扩展。</p>
<p>Transformer 最初在机器翻译中取得突破，随后迅速扩展到语言建模、视觉、语音、多模态和生成任务，最终成为大模型时代最核心的基础架构之一。从深度学习的全局版图看，它不是脱离前文另起炉灶的新物种，而是建立在表示学习、端到端训练、注意力机制、残差连接和大规模预训练这些深度学习主线之上的综合结果。后续第 3 篇会专门展开它的结构与演进。</p>
<div class="blog_h3"><span class="graybg">GAN 与生成式建模跃迁</span></div>
<p>深度学习早期最强的成果主要集中在识别任务，而 GAN 把研究重点大幅推进到“高质量生成”本身。它展示了深网络不仅能判断图像属于什么类别，还能学习数据分布并生成逼真的新样本。这使图像合成、风格迁移、超分辨率、图像到图像翻译等方向迅速发展，也改变了人们对“模型能力边界”的直觉。</p>
<p>GAN 之后，生成式建模继续沿着 VAE、流模型、扩散模型等路线演进。它们关注的问题已经不只是判别准确率，而是样本质量、潜空间结构、可控生成和条件生成。生成模型因此成为深度学习内部一条独立而强势的方法线，后文会单独展开。</p>
<div class="blog_h3"><span class="graybg">预训练与迁移学习</span></div>
<p>深度学习的另一个方法学跃迁，是从“每个任务都从随机初始化开始学”转向“先学通用表示，再迁移到具体任务”。在视觉里，这条线最初体现为 ImageNet 预训练 backbone；在语音和语言里，则逐步发展为更大规模的自监督预训练。迁移学习显著降低了下游任务对标注数据量的依赖，也让模型参数本身成为可复用资产。</p>
<p>这一变化与大模型时代直接相连。大语言模型并不是凭空冒出来的新物种，而是深度学习在预训练、表示共享和规模扩展三条线上持续推进后的自然结果。因此，把深度学习理解成“大模型之前的旧阶段”并不准确；更准确的说法是，大模型建立在深度学习已经完成的方法论基础之上。</p>
<div class="blog_h2"><span class="graybg">可训练深度与残差学习</span></div>
<p>深度学习真正变成“深”这件事，并不是把层数机械堆高就结束了。模型一旦变深，前向信号尺度、反向梯度传播、优化地形和参数更新路径都会迅速恶化。也就是说，网络的理论表达能力和它能否被稳定训练，根本不是同一回事。可训练深度（Trainable Depth）讨论的正是这个问题：怎样让几十层、上百层甚至更深的网络，不只是写得出来，而是真的训得动。</p>
<div class="blog_h3"><span class="graybg">深层网络为什么难训练</span></div>
<p>深层网络的困难，不能只概括成“梯度消失或爆炸”。那当然是重要问题，但并不是全部。更本质地说，随着层数增加，模型必须在一连串非线性变换里同时维持三件事：有用信息不能太快丢失，梯度不能在回传时彻底衰减或失控，优化器还要能在高维参数空间里找到稳定下降方向。哪怕某个更深模型在理论上至少不比浅模型差，训练出来的结果也可能反而更糟，这就是深层退化（Degradation）问题。</p>
<p>若把一个深层块写成 <span displaypfx="inline-" class="mathjax-container">\(H(x)\)</span>，传统堆叠要求这一组层直接学习完整映射 <span displaypfx="inline-" class="mathjax-container">\(x\mapsto H(x)\)</span>。问题在于，当最优映射本身接近恒等映射，或只需要对输入做很小修正时，让网络从零学习整张映射会非常低效。层数越多，这种“每一层都要重新写一遍答案”的负担就越重。</p>
<div class="blog_h3"><span class="graybg">ResNet</span></div>
<p>ResNet（Residual Network）对这个难题给出的回答是残差学习（Residual Learning）。它不要求若干层直接学习 <span displaypfx="inline-" class="mathjax-container">\(H(x)\)</span>，而是改学相对输入的增量 <span displaypfx="inline-" class="mathjax-container">\(F(x)=H(x)-x\)</span>，于是输出变成</p>
<span displaypfx="" class="mathjax-container">\[y=F(x)+x\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是块输入， <span displaypfx="inline-" class="mathjax-container">\(F(x)\)</span> 是卷积、归一化和非线性组成的残差分支， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是块输出。这个重写非常关键，因为它把“学习完整映射”改成了“在已有表示上做局部修正”。若当前层不需要大改输入，只要让 <span displaypfx="inline-" class="mathjax-container">\(F(x)\approx 0\)</span> 即可；若需要修正，再通过残差分支逐步补上。</p>
<p>这等于给深层网络提供了一条默认可行的起点：最坏情况下，信息至少可以沿着恒等路径往后传，而不必在每一层都被迫经过强变换。ResNet 因此不是简单增加一个跳线技巧，而是在重新定义深层块应该学什么。</p>
<div class="blog_h3"><span class="graybg">残差连接为什么有效</span></div>
<p>残差连接（Residual Connection）之所以有效，可以从前向和反向两条路同时理解。前向上，主表示不再只能依赖一串层层覆盖的非线性变换，而是有一条更短、更稳定的通道把已有信息直接送到后面；这使模型更容易保留低层有用特征，不会因为层数增加而过快破坏已有表示。反向上，梯度也多了一条更直接的传播路径，因此不会被所有中间层的局部导数连续压缩。</p>
<p>若把损失记为 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span>，残差块输出为 <span displaypfx="inline-" class="mathjax-container">\(y=x+F(x)\)</span>，则对输入的梯度满足</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial x}=\frac{\partial \mathcal{L}}{\partial y}\left(I+\frac{\partial F(x)}{\partial x}\right)\]</span>
<p>这个式子的意义是：即使残差分支 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial F(x)}{\partial x}\)</span> 在某些区域学得不理想，梯度仍然至少能通过恒等项 <span displaypfx="inline-" class="mathjax-container">\(I\)</span> 保留一条直接路径。也正因为如此，残差连接缓解的不是“模型表达能力不够”，而是深层优化本身的困难。</p>
<p>具象地看，普通深网络像要求每一层都重写一遍完整草稿；残差网络则允许每一层只在上一版稿子旁边批注修改。真正需要改动的地方写入残差，不需要改的地方直接保留原文。这个抽象后来成为深层网络设计中极其通用的模式。</p>
<div class="blog_h3"><span class="graybg">影响超出 CNN</span></div>
<p>ResNet 最早在视觉里爆发，但残差学习的影响远远超出 CNN。Transformer 的每一层都依赖残差连接把注意力子层和 MLP 子层写回主表示流；扩散模型中的 U-Net 主干大量使用残差块；很多现代语音、视频、多模态和图模型也都把 skip connection 当作默认部件。原因很简单：残差连接解决的是深层网络的通用训练稳定性，而不是某种视觉特有问题。</p>
<p>因此，在知识体系里，ResNet 一方面是 CNN 家族中的里程碑架构，另一方面又代表了一条跨架构的方法学原则。后文在卷积神经网络部分仍会把 ResNet 作为经典视觉架构展开；这里更强调它在整个深度学习版图中的地位：它让“更深的网络”第一次大规模变成了工程上可持续扩展的现实路线。</p>
<div class="blog_h2"><span class="graybg">深度学习的实际应用</span></div>
<p>深度学习之所以成为主流，不是因为它在论文基准上偶尔领先，而是因为它在大量真实任务上持续改写了系统能力边界。它最擅长的场景，通常具备三个特征：输入高维、原始信号结构复杂、手工特征难以穷尽。只要这三个条件同时出现，表示学习的优势就会迅速放大。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">应用方向</td>
<td style="text-align: center;">深度学习在做什么</td>
<td style="text-align: center;">典型模型或系统</td>
<td style="text-align: center;">关键价值</td>
</tr>
</thead>
<tbody>
<tr>
<td>图像分类、检测与分割</td>
<td>从像素中直接学习目标、边界和语义区域的层级表示</td>
<td>CNN backbone、Faster R-CNN、U-Net、Mask R-CNN</td>
<td>显著降低人工视觉特征设计需求，并统一多类视觉任务的表示基础</td>
</tr>
<tr>
<td>人脸识别与设备认证</td>
<td>学习稳定的人脸嵌入，用于身份匹配、聚类和检索</td>
<td>FaceNet、ArcFace、Face ID 一类终端系统</td>
<td>把“看起来像不像”变成可度量的特征空间距离，并兼顾鲁棒性与低误识率</td>
</tr>
<tr>
<td>语音识别与语音合成</td>
<td>把连续波形映射为音素、文本或声学表示，再进一步合成自然语音</td>
<td>Deep Speech、Conformer、Tacotron、WaveNet</td>
<td>显著提升端到端语音系统的准确率与自然度</td>
</tr>
<tr>
<td>机器翻译与序列理解</td>
<td>学习跨语言或跨序列位置的上下文依赖与对齐关系</td>
<td>Seq2Seq、Attention、Transformer</td>
<td>把规则驱动和短上下文模型推进到可扩展的端到端序列建模</td>
</tr>
<tr>
<td>医学影像与工业质检</td>
<td>从高维图像中识别微小异常、边界结构和组织模式</td>
<td>ResNet、U-Net、3D CNN</td>
<td>在噪声高、细节密、人工判读成本高的场景中放大模型辅助价值</td>
</tr>
<tr>
<td>推荐、排序与多模态检索</td>
<td>把用户、内容、上下文和行为序列编码进共享表示空间</td>
<td>Wide &amp; Deep、DeepFM、双塔检索模型</td>
<td>提升匹配能力，并支持召回、排序、粗排到精排的分层建模</td>
</tr>
</tbody>
</table>
<p>人脸识别本身毫无疑问属于深度学习的典型落地方向，而 Face ID 这类系统则更接近<span style="background-color: #c0c0c0;">“深度学习模型 + 传感器 + 活体检测 + 安全芯片 + 阈值策略”</span>的产品级集成。深度网络负责学习人脸表征与匹配规则，但真正可商用的身份认证系统，还必须处理深度信息、环境光变化、攻击对抗、设备端隐私隔离和误识率控制等工程问题。这类例子很能说明：模型往往是核心能力来源，但完整系统从来不只是一张网络结构图。</p>
<div class="blog_h2"><span class="graybg">卷积神经网络（CNN）</span></div>
<div class="blog_h3"><span class="graybg">卷积层与池化层</span></div>
<p>卷积（Convolution）在连续形式下定义为函数重叠积分：</p>
<span displaypfx="" class="mathjax-container">\[(f*g)(t)=\int_{-\infty}^{+\infty} f(\tau)\,g(t-\tau)\,d\tau\]</span>
<p>在离散二维图像中常写为：</p>
<span displaypfx="" class="mathjax-container">\[(I*K)(i,j)=\sum_m\sum_n I(i-m,j-n)\,K(m,n)\]</span>
<p>深度学习框架里多数“卷积层”实现的是互相关（Cross-Correlation）而非严格翻转核的数学卷积，但工程上沿用“卷积”命名。卷积核（Kernel）共享权重（Weight Sharing），天然利用局部性（Locality）与平移等变（Translation Equivariance）。</p>
<p>直观上，卷积层做的事情是：对每个位置取一个局部窗口，把窗口里的像素与卷积核做加权求和，得到该位置的响应。不同卷积核学习不同的局部模式（边缘、纹理、角点等）。</p>
<p>一维互相关示例：输入 <span displaypfx="inline-" class="mathjax-container">\(x=[0,0,1,1,1,0,0]\)</span>，核 <span displaypfx="inline-" class="mathjax-container">\(w=[1,0,-1]\)</span>，则在从左到右滑动时，核会对“上升沿/下降沿”给出大幅响应（本质上是比较左右两侧的差异）。这就是经典的边缘检测直觉在离散信号上的对应。</p>
<p>卷积的“系统视角”能统一理解 CNN 与信号处理：把卷积核看作响应函数（Response Kernel），输出就是对历史输入的加权累积。参见“微积分 ➡ 卷积（Convolution）”中的因果卷积与“打点滴累积药效”直觉例子。</p>
<div class="blog_h3"><span class="graybg">经典架构</span></div>
<div class="blog_h4"><span class="graybg">LeNet</span></div>
<p>LeNet 可以看作现代卷积神经网络（Convolutional Neural Network, CNN）的原型。它面对的是手写数字识别这类“局部笔画决定整体类别”的任务，因此核心思想很直接：先用卷积层提取局部边缘、角点和笔画，再用池化（Pooling）降低分辨率与局部扰动敏感性，最后把高层特征送入全连接层做分类。</p>
<p>经典 LeNet-5 的结构可以概括为“卷积 - 池化 - 卷积 - 池化 - 全连接”。前面的卷积层负责把原始像素变成越来越抽象的特征图（Feature Map），后面的全连接层负责把这些局部特征整合成类别判断。它的重要性不只是“识别数字有效”，更在于证明了三件事可以协同工作：局部感受野（Local Receptive Field）、权重共享（Weight Sharing）和层级特征提取（Hierarchical Feature Learning）。</p>
<p>卷积层里的一个典型计算是：</p>
<span displaypfx="" class="mathjax-container">\[y_{i,j}^{(k)}=\sum_{u}\sum_{v}\sum_{c} W_{u,v,c}^{(k)}\,x_{i+u,j+v,c}+b^{(k)}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是输入图像或上一层特征图， <span displaypfx="inline-" class="mathjax-container">\(W^{(k)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个卷积核， <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 是输入通道索引， <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 是空间位置， <span displaypfx="inline-" class="mathjax-container">\(y_{i,j}^{(k)}\)</span> 是输出特征图在该位置上的响应。这个公式表达的就是：用同一个卷积核在整张图上滑动，寻找“哪里出现了我关心的局部模式”。</p>
<p>训练上，LeNet 已经体现出现代深度学习的基本闭环：前向传播得到类别 logits，损失函数衡量预测与真实标签的差距，反向传播把梯度传回卷积核和全连接层参数，再用梯度下降更新权重。它最典型的应用是 MNIST 手写数字识别，也常被用作理解 CNN 的第一块教学样板，因为结构短、计算图清晰、局部模式学习的直觉非常强。</p>
<div class="blog_h4"><span class="graybg">AlexNet</span></div>
<p>AlexNet 标志着深度卷积网络在大规模视觉识别上的突破。它面对的不是手写数字这种相对简单的灰度图，而是 ImageNet 级别的彩色自然图像，因此核心思想从“能提特征”进一步推进到“更深、更宽、更可训练”：增加网络容量，用 ReLU（Rectified Linear Unit）加快优化，用 Dropout 缓解过拟合，用数据增强提升泛化，再借助 GPU 让大模型真正训得动。</p>
<p>其典型结构是多层卷积堆叠后接全连接层，早期卷积核较大、步幅较大，用于迅速降采样并提取低层纹理；中后期卷积核变小，重点转向更细粒度的组合特征。AlexNet 还使用了局部响应归一化（Local Response Normalization, LRN）这一今天较少使用、但在当时有历史意义的设计。整体上，它把 CNN 从“可用于小型任务的模型”推向了“可以在大规模视觉基准上碾压传统方法的通用架构”。</p>
<p>它最有代表性的非线性是 ReLU：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{ReLU}(z)=\max(0,z)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是线性层或卷积层的输出。与 sigmoid / tanh 相比，ReLU 在正半轴不饱和，梯度传播更直接，因此深层网络更容易优化。AlexNet 的历史意义之一，就是把“激活函数的选择会显著影响深网络可训练性”这件事变成了工程共识。</p>
<p>训练上，AlexNet 典型地结合随机裁剪、翻转、颜色扰动等数据增强，并在全连接层使用 Dropout。它的直接应用是大规模图像分类；更深远的影响则是推动了整个视觉领域转向“预训练 CNN + 迁移学习”的工作流。很多后续检测、分割与检索系统，都曾把 AlexNet 当作特征提取骨干网络（Backbone）。</p>
<div class="blog_h4"><span class="graybg">VGG</span></div>
<p>VGG 的核心思想是：用结构极其规整的小卷积核反复堆叠，把网络做深。它放弃了早期“大卷积核 + 大步幅”的粗放设计，转而几乎全程使用 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积，通过增加层数来扩大感受野、提高非线性表达能力，并让整套架构在工程上更统一、更易复用。</p>
<p>VGG 的典型结构非常整齐：若干个 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积层组成一个 stage，stage 之间通过池化层下采样，最后再接全连接分类头。它的重要工程思想是“深度本身就是能力来源之一”。相较于更杂糅的早期网络，VGG 的层次感非常强：浅层提边缘和纹理，中层提局部部件，高层提更完整的语义结构。</p>
<p>为什么反复使用 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积有效，可以从感受野和参数量两方面理解。两个连续的 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积，其有效感受野接近一个 <span displaypfx="inline-" class="mathjax-container">\(5\times 5\)</span> 卷积，但中间多了一次非线性变换，参数量通常还更少。若忽略通道数变化，单层 <span displaypfx="inline-" class="mathjax-container">\(5\times 5\)</span> 卷积大约有 25 个核参数，而两层 <span displaypfx="inline-" class="mathjax-container">\(3\times 3\)</span> 卷积一共是 18 个核参数。</p>
<p>训练上，VGG 比 AlexNet 更依赖较好的初始化、较强的正则化和更大的算力预算，因为它的参数量尤其在全连接部分非常大。它的直接应用是图像分类，但工程上更著名的是作为“通用视觉特征提取器”：在风格迁移、感知损失（Perceptual Loss）、检测与分割早期系统中，VGG 特征长期是强基线。它的代价也很明显：参数多、推理重、显存占用高，这直接推动了后续更高效架构的出现。</p>
<div class="blog_h4"><span class="graybg">ResNet</span></div>
<p>若把上一节的“残差学习”放回视觉主线里看，ResNet（Residual Network）就是它在卷积神经网络中的代表性实现。它的关键突破不是再把卷积堆得更深，而是重新设计“深层网络应该学什么”。它提出残差学习（Residual Learning）：一层或一组层不必直接学习完整映射 <span displaypfx="inline-" class="mathjax-container">\(H(x)\)</span>，而是学习相对于输入的增量 <span displaypfx="inline-" class="mathjax-container">\(F(x)=H(x)-x\)</span>。这样网络输出就写成：</p>
<span displaypfx="" class="mathjax-container">\[y=F(x)+x\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是块输入， <span displaypfx="inline-" class="mathjax-container">\(F(x)\)</span> 是若干卷积层、归一化和激活组成的残差分支输出， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是该残差块的最终输出。若最优映射本身就接近恒等映射（Identity Mapping），那么让网络学习“只改一点点”通常比“从零重建整个映射”更容易优化。</p>
<p>具象地看，普通深网络像要求每一层都重新写一遍完整答案；ResNet 则允许每一层在原答案旁边写批注：需要修改的地方补上增量，不需要改的地方直接走捷径。这个“捷径连接（Skip Connection）”让梯度能沿更短路径传播，因此网络深到几十层、上百层时仍可训练。ResNet 解决的核心不是表达能力不足，而是深层优化退化（Degradation）问题：层数增加后，训练误差反而上升。</p>
<p>训练上，ResNet 通常结合 BatchNorm、较深的 stage 结构和全局平均池化（Global Average Pooling）来替代庞大的全连接头。它在图像分类上大获成功后，很快成为检测、分割、关键点、视频理解等视觉任务的主流骨干网络。更深远的影响是：残差连接后来成为 Transformer、扩散模型和许多现代深网络的标准部件，因为它本质上是在为深层优化建立稳定的信息主通路。</p>
<div class="blog_h2"><span class="graybg">循环神经网络（RNN）</span></div>
<div class="blog_h3"><span class="graybg">RNN</span></div>
<p>循环神经网络（Recurrent Neural Network, RNN）按时间步（Time Step）处理序列。第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个时刻输入是 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span>，隐藏状态（Hidden State）递推为：</p>
<span displaypfx="" class="mathjax-container">\[h_t=\phi(W_{xh}x_t+W_{hh}h_{t-1}+b_h),\quad y_t=W_{hy}h_t+b_y\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(W_{xh}\)</span>（输入到隐层）与 <span displaypfx="inline-" class="mathjax-container">\(W_{hh}\)</span>（隐层到隐层）在所有时刻共享，这是 RNN 能在变长序列上泛化的关键。</p>
<p>把它展开（Unroll）到时间轴上会更直观：同一套参数在每个时间步重复使用，形成一个深度为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 的计算图。这也是 RNN 训练常说的“通过时间的反向传播（Backpropagation Through Time, BPTT）”。</p>
<p>RNN 的经典难点是梯度消失/爆炸（Vanishing/Exploding Gradients）：反向传播时会反复乘以 <span displaypfx="inline-" class="mathjax-container">\(W_{hh}\)</span> 的雅可比，从而导致梯度范数指数级衰减或增长。LSTM/GRU 通过门控（Gating）与更“线性”的记忆通道缓解这一问题。</p>
<p>Seq2Seq（Sequence-to-Sequence）是任务范式，不限定具体单元。早期 Seq2Seq 常由 RNN/LSTM/GRU 的编码器-解码器（Encoder-Decoder）实现；后续被 Transformer 大规模替代。</p>
<p>“乘法 + 加法 + 非线性”为何有效：线性变换负责特征重表达，非线性激活（Nonlinearity）提供函数逼近能力，时间递推提供记忆路径，三者叠加形成高表达力。</p>
<div class="blog_h3"><span class="graybg">LSTM</span></div>
<p>LSTM（Long Short-Term Memory）是为了解决普通 RNN 难以稳定保留长程信息的问题而设计的门控循环结构。它的核心思想不是简单把隐藏状态不断往前传，而是显式维护一个记忆单元（Cell State） <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span>，并用门控决定“忘掉什么、写入什么、读出什么”。这使得序列中的关键信息可以沿着一条更接近线性的通道向后传播，而不必在每个时间步都被强非线性反复改写。</p>
<p>LSTM 的典型更新写成：</p>
<span displaypfx="" class="mathjax-container">\[f_t=\sigma(W_f x_t+U_f h_{t-1}+b_f),\quad i_t=\sigma(W_i x_t+U_i h_{t-1}+b_i)\]</span>
<span displaypfx="" class="mathjax-container">\[\tilde c_t=\tanh(W_c x_t+U_c h_{t-1}+b_c),\quad c_t=f_t\odot c_{t-1}+i_t\odot \tilde c_t\]</span>
<span displaypfx="" class="mathjax-container">\[o_t=\sigma(W_o x_t+U_o h_{t-1}+b_o),\quad h_t=o_t\odot \tanh(c_t)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 是当前输入， <span displaypfx="inline-" class="mathjax-container">\(h_{t-1}\)</span> 是前一时刻隐藏状态， <span displaypfx="inline-" class="mathjax-container">\(c_{t-1}\)</span> 是前一时刻记忆单元； <span displaypfx="inline-" class="mathjax-container">\(f_t\)</span> 是遗忘门（Forget Gate），控制旧记忆保留多少； <span displaypfx="inline-" class="mathjax-container">\(i_t\)</span> 是输入门（Input Gate），控制当前候选记忆 <span displaypfx="inline-" class="mathjax-container">\(\tilde c_t\)</span> 写入多少； <span displaypfx="inline-" class="mathjax-container">\(o_t\)</span> 是输出门（Output Gate），控制当前记忆暴露给隐藏状态多少； <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 是 sigmoid，把门值压到 <span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span> 区间； <span displaypfx="inline-" class="mathjax-container">\(\odot\)</span> 是逐元素乘法。</p>
<p>具象地看，LSTM 像一条带阀门的记忆水管。普通 RNN 每走一步都把旧信息和新输入一锅重拌，长距离信息容易被冲淡；LSTM 则允许旧记忆沿主管道直接往后流，同时用三个阀门分别控制“放掉旧水”“注入新水”“输出多少”。这正是它能比普通 RNN 更稳定地记住长距离依赖的原因。</p>
<p>训练上，LSTM 仍然通过时间反向传播（BPTT）学习参数，但门控结构显著改善了梯度流。它曾长期是机器翻译、语音识别、语言建模、时间序列预测和序列标注的主力模型。在 Transformer 出现之前，大量 Seq2Seq 系统都建立在双向 LSTM 或多层 LSTM 之上；在某些中小规模时序任务里，LSTM 今天仍然是强而稳的基线。</p>
<div class="blog_h3"><span class="graybg">GRU</span></div>
<p>GRU（Gated Recurrent Unit）可以看作 LSTM 的简化版本。它保留了“用门控控制信息保留与更新”的核心思想，但把记忆单元与隐藏状态合并，不再单独维护 <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span>，从而用更少参数换取更紧凑的结构与更快的训练速度。</p>
<p>GRU 的一组典型公式是：</p>
<span displaypfx="" class="mathjax-container">\[z_t=\sigma(W_z x_t+U_z h_{t-1}+b_z),\quad r_t=\sigma(W_r x_t+U_r h_{t-1}+b_r)\]</span>
<span displaypfx="" class="mathjax-container">\[\tilde h_t=\tanh(W_h x_t+U_h(r_t\odot h_{t-1})+b_h)\]</span>
<span displaypfx="" class="mathjax-container">\[h_t=(1-z_t)\odot h_{t-1}+z_t\odot \tilde h_t\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span> 是更新门（Update Gate），决定旧状态保留多少、新候选状态写入多少； <span displaypfx="inline-" class="mathjax-container">\(r_t\)</span> 是重置门（Reset Gate），决定在构造候选状态 <span displaypfx="inline-" class="mathjax-container">\(\tilde h_t\)</span> 时，历史信息应当被参考到什么程度。若 <span displaypfx="inline-" class="mathjax-container">\(z_t\)</span> 很小，模型更倾向保留旧记忆；若很大，模型更倾向用新信息刷新状态。</p>
<p>从直觉上看，GRU 把 LSTM 的三道阀门压缩成两道更紧凑的控制逻辑：一方面决定“要不要更新”，另一方面决定“生成候选更新时要不要忘掉旧状态的一部分”。这让它在很多任务上能以更少参数达到与 LSTM 相近的效果，尤其适合数据量不极大、模型容量受限或推理效率敏感的场景。</p>
<p>训练上，GRU 与 LSTM 一样使用 BPTT。应用上，它常见于语音、时序预测、较轻量的编码器-解码器模型，以及很多工业界的表格时间序列任务。若需要更强的显式记忆控制，LSTM 往往更稳；若更看重结构简洁和训练效率，GRU 常是更自然的选择。</p>
<div class="blog_h2"><span class="graybg">信息抽取</span></div>
<div class="blog_h3"><span class="graybg">命名实体识别（NER）</span></div>
<p>命名实体识别（Named Entity Recognition, NER）是信息抽取（Information Extraction）中最典型的任务之一。它关注的不是整句属于什么类别，而是要在输入序列中找出<span style="background-color: #c0c0c0;">哪些片段构成实体、每个实体的边界在哪里、每个实体属于什么类型</span>。若把一段文本中的实体集合写成</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{Y}=\{(s_k,e_k,c_k)\}_{k=1}^{m}\]</span>
<p>则 <span displaypfx="inline-" class="mathjax-container">\(s_k\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(e_k\)</span> 分别表示第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个实体的起止位置， <span displaypfx="inline-" class="mathjax-container">\(c_k\)</span> 表示实体类型。这个写法很重要，因为它直接说明了：NER 的本质不是单点分类，而是<span style="background-color: #c0c0c0;">区间抽取（Span Extraction）加类型判定</span>。</p>
<p>在早期系统里，NER 往往被写成序列标注问题，例如用 BIO 或 BIOES 标签体系逐 token 预测，再用 CRF 保证标签转移合法。进入深度学习时代后，任务目标没有变，但建模方式明显扩展了：除了传统的 token classification + CRF 之外，还出现了 span-based、pointer-based、grid-based、relation-based 等专门针对实体边界结构设计的抽取头。Global Pointer 和 W²NER 正是这一阶段的代表方法。</p>
<div class="blog_h4"><span class="graybg">它们在系统里的位置</span></div>
<p>需要先把层级关系分清。Global Pointer、W²NER 不是 BERT、RoBERTa、ModernBERT 这种通用编码器（Backbone / Encoder），也不是 HMM、CRF 那类经典概率图模型；它们更准确的定位是<span style="background-color: #c0c0c0;">接在编码器之上的 NER 专用任务头或结构化解码架构</span>。系统链路通常可以概括为：</p>
<span displaypfx="" class="mathjax-container">\[\text{Text}\rightarrow \text{Tokenizer}\rightarrow \text{Encoder}\rightarrow \text{NER Head}\rightarrow \text{Entities}\]</span>
<p>其中编码器负责把原始文本转成上下文化表示 <span displaypfx="inline-" class="mathjax-container">\(H=[h_1,\dots,h_n]\)</span>，而 NER head 决定如何把这些表示转成实体集合。若任务头选的是逐 token 分类，输出就更接近 BIO 标签；若任务头选的是 span 打分或词间关系建模，输出就更接近实体区间本身。Global Pointer 和 W²NER 的差别，主要就体现在“实体应该怎样从隐藏表示中被读出来”这一层。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">层级</td>
<td style="text-align: center;">主要作用</td>
<td style="text-align: center;">典型例子</td>
</tr>
</thead>
<tbody>
<tr>
<td>编码器（Backbone）</td>
<td>把文本编码成上下文化向量表示</td>
<td>BERT、RoBERTa、ModernBERT、BiLSTM</td>
</tr>
<tr>
<td>任务头（Task Head）</td>
<td>把隐藏表示转成实体边界和类型预测</td>
<td>Token Classification、CRF、Global Pointer、W²NER</td>
</tr>
<tr>
<td>输出形式</td>
<td>决定模型最终返回 BIO 标签还是实体 span 集合</td>
<td>B-ORG / I-ORG / O，或 <span displaypfx="inline-" class="mathjax-container">\((s,e,c)\)</span></td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">从序列标注到 span 建模</span></div>
<p>把 NER 当作序列标注有一个很自然的优点：实现简单，训练直接，和 token 级监督天然对齐。但它也有明显边界。第一，实体在语义上是一个整体区间，而序列标注把问题拆成逐位置判断，再依赖标签体系和解码规则把局部标签拼回实体，这会引入“局部预测正确但整体边界不稳”的误差。第二，当数据里存在嵌套实体（Nested Entity）时，例如较长实体内部还包含较短实体，单层 BIO 标注就会开始变得别扭。第三，若实体跨度很长，模型需要通过一串连续标签间接表达“这一整段属于同一个实体”，而不是直接对整个 span 打分。</p>
<p>因此，深度学习时代的很多 NER 方法开始直接面向 span 建模。核心想法是：与其先预测每个 token 的局部标签，再回头拼边界，不如直接让模型回答“从第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个位置到第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个位置，是否构成某一类实体”。这样，实体边界就从隐式结构变成了显式建模对象。Global Pointer 正是这条路线里非常典型的方法。</p>
<div class="blog_h4"><span class="graybg">Global Pointer</span></div>
<p>Global Pointer 的核心思想是：把每一种实体类型都看成一个“起点 - 终点匹配问题”。给定编码器输出 <span displaypfx="inline-" class="mathjax-container">\(H=[h_1,\dots,h_n]\)</span>，模型先把每个位置投影成适合做起点查询和终点键值匹配的向量，再对每种实体类型的每一对位置 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 计算一个 span 分数。若这个分数足够高，就认为“从 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 到 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的片段属于该类型实体”。</p>
<p>一种简化写法是：</p>
<span displaypfx="" class="mathjax-container">\[q_i^{(c)}=W_q^{(c)}h_i,\qquad k_j^{(c)}=W_k^{(c)}h_j\]</span>
<span displaypfx="" class="mathjax-container">\[\mathrm{score}_c(i,j)=\big(q_i^{(c)}\big)^\top k_j^{(c)},\qquad i\le j\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 表示实体类型， <span displaypfx="inline-" class="mathjax-container">\(q_i^{(c)}\)</span> 表示位置 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 作为该类实体起点时的表示， <span displaypfx="inline-" class="mathjax-container">\(k_j^{(c)}\)</span> 表示位置 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 作为终点时的表示。很多实现会进一步加入 RoPE（Rotary Position Embedding）一类位置编码，使相对位置信息直接进入打分过程。</p>
<p>这个结构的关键优势有三点。第一，它<span style="background-color: #c0c0c0;">天然输出 span</span>，而不是先输出 BIO 标签再回拼实体，因此边界目标与任务定义更一致。第二，它对嵌套实体非常自然：同一个起点可以与多个终点形成不同 span，不同类型之间也互不冲突。第三，它对长实体更友好，因为模型直接给 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 这一对边界打分，不必靠一长串内部标签层层传递“这是同一个实体”的信号。</p>
<p>从工程角度看，Global Pointer 也是很受欢迎的一类 NER 头。它的实现相对简洁，主要开销来自构造每种类型的 <span displaypfx="inline-" class="mathjax-container">\(n\times n\)</span> 打分矩阵，训练与推理都比较直接。它因此特别适合需要嵌套实体支持、但又希望结构尽量简单的场景。</p>
<div class="blog_h4"><span class="graybg">W²NER</span></div>
<p>W²NER（通常写作 W2NER）走的是另一条路线：它把 NER 视为<span style="background-color: #c0c0c0;">词与词之间关系的二维建模问题</span>。与其直接给一个 span 打分，它更关心“两个位置之间是什么关系”，再通过这些局部关系把完整实体组装出来。也就是说，模型不是直接回答“ <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 是不是实体”，而是先在一个 <span displaypfx="inline-" class="mathjax-container">\(n\times n\)</span> 的词对关系网格上预测关系标签。</p>
<p>它常见地使用两类关系标签：</p>
<ul>
<li><span style="background-color: #c0c0c0;">NNW（Next-Neighboring-Word）</span>：表示一个实体内部相邻词之间的连接关系。</li>
<li><span style="background-color: #c0c0c0;">THW-{type}（Tail-Head-Word）</span>：表示某个实体的尾词和头词之间闭合成一个特定类型实体。</li>
</ul>
<p>在这个框架里，模型先构造词对表示，再用 biaffine、条件层归一化（Conditional Layer Normalization, CLN）、二维卷积（2D Convolution）等模块在整个关系网格上做局部模式建模。最终，实体不是由一次 span 打分直接得出，而是由一条词间关系链和一个闭合关系共同定义出来。</p>
<p>这条思路的最大价值在于表达力更强。因为模型显式建模的是词与词之间的结构关系，所以它不仅能处理嵌套实体，还更容易扩展到不连续实体（Discontinuous Entity）或更复杂的局部结构。代价也同样明显：它的关系网格更重，中间表示更大，计算和显存开销通常高于较轻量的 span 打分方法；同时，二维卷积式建模也让工程实现和调参复杂度明显上升。</p>
<div class="blog_h4"><span class="graybg">Global Pointer 与 W²NER 的分工</span></div>
<p>二者都属于深度学习时代的 NER 架构，但它们的设计哲学并不相同。Global Pointer 更像“直接给候选实体区间打分”；W²NER 更像“先预测实体内部关系，再把实体结构拼出来”。前者更直接、更轻量，后者结构表达更强。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">维度</td>
<td style="text-align: center;">Global Pointer</td>
<td style="text-align: center;">W²NER</td>
</tr>
</thead>
<tbody>
<tr>
<td>基本单位</td>
<td>实体 span <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span></td>
<td>词对关系 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span></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>长 span 处理</td>
<td>更直接，因为直接做边界对打分</td>
<td>更依赖关系网格中的结构传播</td>
</tr>
<tr>
<td>工程复杂度</td>
<td>较低</td>
<td>较高</td>
</tr>
<tr>
<td>更像什么</td>
<td>span-based / pointer-based 抽取头</td>
<td>grid-based / relation-based 抽取架构</td>
</tr>
</tbody>
</table>
<p>因此，若任务重点是常规实体抽取、嵌套实体支持和较好的工程效率，Global Pointer 往往是更干净的设计；若任务存在复杂实体结构、关系链式表达或不连续实体，W²NER 一类 relation/grid 方法会更有吸引力。它们没有谁在所有场景中绝对更强，差异主要来自任务结构本身。</p>
<div class="blog_h2"><span class="graybg">生成模型</span></div>
<p>生成模型（Generative Model）关注的核心问题不是“这个样本属于哪一类”，而是“数据本身是如何产生出来的”。更形式化地说，它试图学习数据分布 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>，或条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(x|c)\)</span>，从而能够采样、重建、补全、去噪或按条件生成新样本。不同生成模型的主要差别在于它们选择了不同的概率建模路径：有的学隐变量，有的学博弈过程，有的学逐步去噪，有的更偏重表示压缩。</p>
<div class="blog_h3"><span class="graybg">自编码器（AE）</span></div>
<p>自编码器（Autoencoder, AE）的核心思想是“先压缩，再重建”。它把输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 通过编码器（Encoder）映射到低维或受约束的隐表示（Latent Representation） <span displaypfx="inline-" class="mathjax-container">\(z\)</span>，再通过解码器（Decoder）把 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 重建回 <span displaypfx="inline-" class="mathjax-container">\(\hat x\)</span>。如果模型在受限瓶颈下仍能较好重建输入，就说明 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 抓住了数据中的关键结构。</p>
<p>其基本形式可以写成：</p>
<span displaypfx="" class="mathjax-container">\[z=f_{\theta}(x),\qquad \hat x=g_{\phi}(z)\]</span>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{AE}}=\|x-\hat x\|_2^2\quad \text{或}\quad -\sum_i x_i\log \hat x_i\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f_{\theta}\)</span> 是编码器， <span displaypfx="inline-" class="mathjax-container">\(g_{\phi}\)</span> 是解码器， <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是瓶颈表示， <span displaypfx="inline-" class="mathjax-container">\(\hat x\)</span> 是重建结果。若输入是连续值，常用均方误差；若输入近似二值或归一化到概率意义，常用逐维交叉熵。这个目标迫使模型学习“怎样用更紧凑的表示保存足够重建原样本的信息”。</p>
<p>自编码器本身并不天然是强生成模型，因为它主要学会的是“给定输入怎么重建自己”，而不是如何从一个规则、可采样的潜空间中稳定地产生新样本。它更适合理解为表示学习或降维模型。通过在结构或训练目标上增加限制，例如稀疏自编码器（Sparse AE）、去噪自编码器（Denoising AE）或收缩自编码器（Contractive AE），它可以学到更稳健的隐表示。</p>
<p>应用上，AE 常用于降维、异常检测、去噪、预训练和表征学习。例如在工业异常检测里，模型只用正常样本训练，推理时若某个输入无法被良好重建，重建误差就可能提示该样本偏离了正常分布。</p>
<div class="blog_h3"><span class="graybg">变分自编码器（VAE）</span></div>
<p>变分自编码器（Variational Autoencoder, VAE）是在 AE 基础上把“隐空间可采样”这件事做成概率建模的方案。它不再把编码器输出一个确定的潜向量，而是输出一个潜变量分布 <span displaypfx="inline-" class="mathjax-container">\(q_{\phi}(z|x)\)</span>；同时假设存在生成分布 <span displaypfx="inline-" class="mathjax-container">\(p_{\theta}(x|z)\)</span>。这样，模型既能重建输入，又能从一个规则的潜空间里采样新样本。</p>
<p>VAE 的核心训练目标是证据下界（Evidence Lower Bound, ELBO）：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{VAE}}=\mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)]-D_{\mathrm{KL}}\big(q_{\phi}(z|x)\,\|\,p(z)\big)\]</span>
<p>其中第一项是重建项，鼓励给定 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 时能把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 生成回来；第二项是 KL 散度（Kullback-Leibler Divergence），把编码器输出的后验近似分布 <span displaypfx="inline-" class="mathjax-container">\(q_{\phi}(z|x)\)</span> 拉向先验分布 <span displaypfx="inline-" class="mathjax-container">\(p(z)\)</span>，通常取标准高斯 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(0,I)\)</span>。这一步的作用是把隐空间整理成“规则、连续、可插值、可采样”的形状。</p>
<p>训练上的关键技巧是重参数化（Reparameterization Trick）：若直接从 <span displaypfx="inline-" class="mathjax-container">\(q_{\phi}(z|x)\)</span> 采样，梯度难以回传；VAE 通常把采样写成 <span displaypfx="inline-" class="mathjax-container">\(z=\mu+\sigma\odot \epsilon\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\sim\mathcal{N}(0,I)\)</span>，而 <span displaypfx="inline-" class="mathjax-container">\(\mu,\sigma\)</span> 由编码器输出。这样随机性被转移到与参数无关的 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 上，梯度就能顺利穿过 <span displaypfx="inline-" class="mathjax-container">\(\mu,\sigma\)</span> 回传。</p>
<p>应用上，VAE 特别适合做潜空间操作，例如插值生成、属性控制、缺失补全和概率建模。与 GAN 相比，VAE 生成结果往往更平滑、更稳定，但图像清晰度常较弱；与扩散模型相比，VAE 在采样效率上更高，但生成质量上通常不是最强路线。</p>
<div class="blog_h3"><span class="graybg">生成对抗网络（GAN）</span></div>
<p>生成对抗网络（Generative Adversarial Network, GAN）的核心思想是对抗式学习：让生成器（Generator）负责“伪造样本”，让判别器（Discriminator）负责“分辨真假”，两者在博弈中共同提高。生成器学会把随机噪声变成越来越像真实数据的样本，判别器则学会识别这些样本是否来自真实分布。</p>
<p>经典 GAN 的目标写成：</p>
<span displaypfx="" class="mathjax-container">\[\min_G\max_D\ V(D,G)=\mathbb{E}_{x\sim p_{\mathrm{data}}}[\log D(x)]+\mathbb{E}_{z\sim p(z)}[\log(1-D(G(z)))] \]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 是从先验分布中采样的随机噪声， <span displaypfx="inline-" class="mathjax-container">\(G(z)\)</span> 是生成器产生的假样本， <span displaypfx="inline-" class="mathjax-container">\(D(x)\)</span> 输出输入样本为真样本的概率。判别器希望让真实样本分数高、伪样本分数低；生成器则希望骗过判别器，让 <span displaypfx="inline-" class="mathjax-container">\(G(z)\)</span> 看起来足够真实。</p>
<p>具象地看，GAN 像“伪造者”和“鉴定师”的对抗升级。鉴定师越强，伪造者就被迫学会更精细的伪造技巧；伪造者越强，鉴定师也必须学会更细致的辨别规则。理论上，这种动态会把生成分布推向真实数据分布；工程上，它带来的最大问题是训练不稳定，常见现象包括模式崩塌（Mode Collapse）、震荡和判别器过强导致生成器梯度过弱。</p>
<p>GAN 在图像生成、图像翻译、超分辨率和风格迁移里曾经极其成功，因为它特别善于生成锐利、感知上真实的图像纹理。但它对训练技巧依赖很强，后来在大规模高保真图像生成上逐渐被扩散模型压过；即便如此，GAN 在需要低步数快速生成或特定视觉变换任务中仍然很有生命力。</p>
<div class="blog_h3"><span class="graybg">扩散模型（Diffusion）</span></div>
<p>扩散模型（Diffusion Model）的核心思想是：把“直接学会生成复杂数据”拆成“先逐步加噪，再逐步去噪”这条更稳定的路径。前向过程把真实样本一步步污染成近似高斯噪声，反向过程则训练一个神经网络学会在每一步去掉一小部分噪声。最终从纯噪声出发，经过多步反向去噪，就能逐步生成结构清晰的样本。</p>
<p>记号上，前向扩散链从真实样本 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 出发，依次得到 <span displaypfx="inline-" class="mathjax-container">\(x_1,x_2,\dots,x_T\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 表示原始数据样本， <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步加噪后的样本。</p>
<p>一个常见的前向加噪过程写成：</p>
<span displaypfx="" class="mathjax-container">\[q(x_t|x_{t-1})=\mathcal{N}\!\left(x_t;\sqrt{1-\beta_t}\,x_{t-1},\beta_t I\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\beta_t\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步的噪声强度：它控制从 <span displaypfx="inline-" class="mathjax-container">\(x_{t-1}\)</span> 走到 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 时，原信号衰减多少、随机噪声注入多少。训练时常把从 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 到 <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span> 的若干步合并写成</p>
<span displaypfx="" class="mathjax-container">\[x_t=\sqrt{\bar\alpha_t}\,x_0+\sqrt{1-\bar\alpha_t}\,\epsilon,\qquad \epsilon\sim\mathcal{N}(0,I)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\bar\alpha_t\)</span> 是由噪声日程（Noise Schedule）累乘得到的系数，控制到第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 步时，原始信号保留了多少、噪声混入了多少。实际训练中，网络通常不直接预测 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span>，而是预测噪声 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>，典型损失是：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}_{\mathrm{diff}}=\mathbb{E}\big[\|\epsilon-\epsilon_{\theta}(x_t,t)\|_2^2\big]\]</span>
<p>这条路径之所以有效，在于“预测一步噪声”比“直接一次生成整张复杂图片”更容易优化。训练上，扩散模型相对稳定，较少出现 GAN 那种对抗不平衡；代价是采样通常需要多步迭代，推理速度较慢。应用上，扩散模型已经成为图像生成、文生图、图像编辑、超分辨率、视频生成和分子设计的重要主线。Stable Diffusion 一类系统，本质上就是把扩散过程放到了潜空间（Latent Space）里执行，以降低像素空间扩散的计算成本。</p>
<div class="blog_h2"><span class="graybg">图神经网络（GNN）</span></div>
<p>图神经网络（Graph Neural Network, GNN）处理的对象不是规则网格上的序列或图像，而是由节点（Node）和边（Edge）组成的图（Graph）。它的核心问题是：当样本之间存在不规则连接关系时，如何让一个节点的表示同时反映“自己的特征”和“邻居结构中的上下文”。因此，GNN 的基本直觉不是滑动卷积核，也不是按时间递推，而是让节点在图上传递消息、聚合邻居信息，再更新自身表示。</p>
<div class="blog_h3"><span class="graybg">图卷积网络（GCN）</span></div>
<p>图卷积网络（Graph Convolutional Network, GCN）把卷积思想推广到图结构上。它的核心思想是：一个节点的新表示，不应只由自己决定，还应由其邻居节点的表示共同决定；但这种聚合不能简单相加，而需要根据图结构做适当归一化，否则高度节点会在信息聚合中占据过大权重。</p>
<p>GCN 的经典一层更新公式写成：</p>
<span displaypfx="" class="mathjax-container">\[H^{(l+1)}=\sigma\!\left(\tilde D^{-1/2}\tilde A\tilde D^{-1/2}H^{(l)}W^{(l)}\right)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(H^{(l)}\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层所有节点的表示矩阵； <span displaypfx="inline-" class="mathjax-container">\(W^{(l)}\)</span> 是该层可学习权重； <span displaypfx="inline-" class="mathjax-container">\(\tilde A=A+I\)</span> 表示在原邻接矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 上加自环（Self-loop），让节点保留自己的信息； <span displaypfx="inline-" class="mathjax-container">\(\tilde D\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(\tilde A\)</span> 的度矩阵（Degree Matrix）； <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 是非线性激活。中间那项对邻居求和并按度做归一化，本质上是在做“平滑的邻居平均”。</p>
<p>具象地看，GCN 像在社交网络里更新一个人的画像：不仅看这个人自己填写的特征，还参考他一跳邻居的大致特征，再做归一化，避免“朋友特别多的人”把自己的表示稀释得过于严重。多层堆叠后，一个节点就能间接接触到两跳、三跳甚至更远范围的信息。</p>
<p>训练上，GCN 常用于节点分类、图分类和链路预测。它的经典应用包括引文网络分类、分子图预测和推荐系统图表示学习。局限也很典型：层数太深时，节点表示会越来越相似，出现过平滑（Oversmoothing）；大图上直接用全图邻接矩阵训练也会带来显存与计算压力。</p>
<div class="blog_h3"><span class="graybg">图注意力网络（GAT）</span></div>
<p>图注意力网络（Graph Attention Network, GAT）在 GCN 的“统一归一化邻居平均”之上进一步提出：不同邻居的重要性不应被预先固定，而应由模型动态学习。它的核心思想是把注意力机制引入图结构，使每个节点在聚合邻居时，能够自适应决定“更该听谁的话”。</p>
<p>一层 GAT 的典型计算是：</p>
<span displaypfx="" class="mathjax-container">\[e_{ij}=a(Wh_i,Wh_j),\qquad \alpha_{ij}=\frac{\exp(e_{ij})}{\sum_{k\in \mathcal{N}(i)}\exp(e_{ik})}\]</span>
<span displaypfx="" class="mathjax-container">\[h_i'=\sigma\!\left(\sum_{j\in \mathcal{N}(i)} \alpha_{ij}\,Wh_j\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(h_i\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 的输入表示， <span displaypfx="inline-" class="mathjax-container">\(W\)</span> 是线性变换矩阵， <span displaypfx="inline-" class="mathjax-container">\(a(\cdot,\cdot)\)</span> 是注意力打分函数， <span displaypfx="inline-" class="mathjax-container">\(e_{ij}\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 对节点 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 的未归一化重要性分数， <span displaypfx="inline-" class="mathjax-container">\(\alpha_{ij}\)</span> 是在邻居集合 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(i)\)</span> 内 softmax 归一化后的注意力权重。</p>
<p>与 GCN 相比，GAT 的关键收益是更灵活：若某个邻居特别重要，模型可以给它更高权重；若某个邻居噪声较大，则可自动抑制。训练上，GAT 仍通过监督或自监督目标学习节点表示，常用多头注意力（Multi-head Attention）稳定训练。应用上，它常见于异质关系更复杂、邻居重要性差异显著的图任务，例如社交网络分析、知识图谱局部推断和分子性质预测。</p>
<div class="blog_h3"><span class="graybg">GraphSAGE</span></div>
<p>GraphSAGE（Graph Sample and Aggregate）的核心思想是把 GNN 从“转导式（Transductive）图编码”推进到“归纳式（Inductive）图表示学习”。传统 GCN 常依赖整张训练图；GraphSAGE 则强调：即使测试时出现训练中未见过的新节点，只要能拿到它的邻居特征，也应能在线生成它的表示。</p>
<p>其典型更新形式是先采样邻居，再做聚合：</p>
<span displaypfx="" class="mathjax-container">\[h_{\mathcal{N}(v)}^{(k)}=\mathrm{AGG}^{(k)}\big(\{h_u^{(k-1)}:u\in \mathcal{N}(v)\}\big)\]</span>
<span displaypfx="" class="mathjax-container">\[h_v^{(k)}=\sigma\!\left(W^{(k)}[h_v^{(k-1)}\|h_{\mathcal{N}(v)}^{(k)}]\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(h_v^{(k)}\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(v\)</span> 在第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 层的表示， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(v)\)</span> 是其邻居集合， <span displaypfx="inline-" class="mathjax-container">\(\mathrm{AGG}\)</span> 可以是均值、池化或 LSTM 聚合器， <span displaypfx="inline-" class="mathjax-container">\([\cdot\|\cdot]\)</span> 表示向量拼接。GraphSAGE 的关键工程点是“采样”，因为超大图中不可能每次把全部邻居完整展开。</p>
<p>具象地看，GraphSAGE 像为每个节点建立一套“从邻居摘要中构造自我画像”的规则。它不要求记住整张训练图中每个节点的专属嵌入，而是学会一套可迁移的邻域聚合函数。这正是它能处理新节点、动态图和大规模图数据的原因。</p>
<p>应用上，GraphSAGE 在推荐系统、社交网络、风控图谱和工业知识图谱中非常常见，因为这些场景经常不断出现新节点、新边，归纳式能力比单纯在固定图上做转导预测更重要。</p>
<div class="blog_h3"><span class="graybg">消息传递机制（Message Passing）</span></div>
<p>消息传递（Message Passing）不是某一个具体模型，而是理解 GNN 的统一抽象框架。无论是 GCN、GAT 还是 GraphSAGE，本质上都可以拆成两步：第一步，节点从邻居那里接收消息；第二步，把这些消息与自己的旧表示结合，更新成新的节点表示。</p>
<p>这一抽象常写成：</p>
<span displaypfx="" class="mathjax-container">\[m_v^{(l+1)}=\mathrm{AGG}\Big(\{M^{(l)}(h_v^{(l)},h_u^{(l)},e_{uv})\,:\,u\in\mathcal{N}(v)\}\Big)\]</span>
<span displaypfx="" class="mathjax-container">\[h_v^{(l+1)}=U^{(l)}\big(h_v^{(l)},m_v^{(l+1)}\big)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(h_v^{(l)}\)</span> 是节点 <span displaypfx="inline-" class="mathjax-container">\(v\)</span> 在第 <span displaypfx="inline-" class="mathjax-container">\(l\)</span> 层的表示， <span displaypfx="inline-" class="mathjax-container">\(e_{uv}\)</span> 是边特征， <span displaypfx="inline-" class="mathjax-container">\(M^{(l)}\)</span> 是消息函数，决定一条边上传递什么信息； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{AGG}\)</span> 是聚合函数，如求和、均值、最大值或注意力加权和； <span displaypfx="inline-" class="mathjax-container">\(U^{(l)}\)</span> 是更新函数，把旧表示与聚合后的消息合成为新表示。</p>
<p>这个框架的重要性在于，它把看似不同的图模型放进了一套统一语言里：GCN 相当于使用归一化线性消息与均值式聚合，GAT 相当于把注意力权重写进聚合，GraphSAGE 相当于强调采样与归纳式聚合。理解了消息传递，就能把很多图模型看成“消息函数、聚合函数、更新函数”三处设计选择的不同组合。</p>
<p>训练上，消息传递式 GNN 常用于三类任务：节点级任务（例如节点分类）、边级任务（例如链路预测）、图级任务（例如分子性质预测）。它们的共同难点包括：过平滑、邻居爆炸（Neighborhood Explosion）、异质图关系复杂，以及深层堆叠后长程依赖难以稳定传播。很多现代 GNN 改进，本质上都在围绕这几个瓶颈重新设计消息传递规则。</p>
<div class="blog_h2"><span class="graybg">ONNX</span></div>
<p>ONNX（Open Neural Network Exchange，开放神经网络交换格式）是一种描述机器学习模型的开放标准。它的核心作用，不是训练模型，而是把已经定义并训练好的神经网络表示成一种<span style="background-color: #c0c0c0;">可交换、可部署、跨框架可读</span>的中间格式，使模型能够从训练环境转移到不同推理环境中运行。</p>
<div class="blog_h3"><span class="graybg">它解决什么问题</span></div>
<p>训练阶段常用 PyTorch、TensorFlow、JAX 等框架；部署阶段则常落到 ONNX Runtime、TensorRT、OpenVINO、移动端推理引擎或嵌入式执行环境。问题在于，这些系统各自拥有不同的内部表示方式、图执行器和算子实现，原生模型格式并不天然互通。ONNX 解决的正是这条链路中的“中间表示”问题。</p>
<p>可以把 ONNX 理解为模型世界里的 PDF：训练框架先把网络导出为 <pre class="crayon-plain-tag">.onnx</pre> 文件，部署侧再由相应 runtime 读取、优化并执行。它并不等于某一种具体推理引擎，而更像一个让不同框架与部署后端彼此对接的交换层。</p>
<div class="blog_h3"><span class="graybg">一个 ONNX 文件里有什么</span></div>
<p>从结构上看，ONNX 文件本质上是一份<span style="background-color: #c0c0c0;">静态计算图（Static Computation Graph）加权重参数的描述</span>。其中通常包含四类核心信息：</p>
<ul>
<li>计算图：模型由哪些算子组成，例如卷积（Conv）、矩阵乘法（MatMul）、归一化、激活、注意力等。</li>
<li>张量元信息：输入、输出与中间张量的形状（Shape）和数据类型（Data Type）。</li>
<li>参数权重：训练后得到的权重矩阵、偏置和其他可学习参数；大模型也可能采用外部权重文件。</li>
<li>算子版本信息：模型依赖的 ONNX opset 版本，以及每个算子的语义约定。</li>
</ul>
<p>这种表示方式的关键价值，是把“Python 前向代码如何写”转写成“部署时应该执行哪条算子图”。一旦导出完成，部署系统通常不再依赖训练时的 Python 类定义，而是按 ONNX 图里的算子与依赖关系直接执行。</p>
<div class="blog_h3"><span class="graybg">常见用途</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">场景</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>部署加速</td>
<td>交给 ONNX Runtime、TensorRT 等后端做图优化、算子融合、低精度执行和量化推理</td>
</tr>
<tr>
<td>跨框架部署</td>
<td>例如用 PyTorch 训练，再导出 ONNX，交由另一套推理栈加载运行</td>
</tr>
<tr>
<td>多端适配</td>
<td>同一模型可进一步转接到服务器、边缘设备、移动端或嵌入式环境，前提是目标引擎支持相应算子</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">与 PyTorch 的关系</span></div>
<p>在 PyTorch 生态里，ONNX 最常见的入口是导出。开发者通常先在 PyTorch 中定义并训练模型，再用 <pre class="crayon-plain-tag">torch.onnx.export(...)</pre> 把模型转换成 ONNX 图。这个过程的本质，是把 PyTorch 里的前向路径从“解释执行的 Python 模块”改写成“显式算子图”。</p>
<p>导出之后，部署侧执行的已不再是原始 Python 前向代码，而是 ONNX 图中的算子序列。因此，依赖复杂动态控制流、框架私有算子或运行时分支逻辑的模型，在导出时往往需要额外改写、简化或替换成更可静态化的结构。</p>
<div class="blog_h3"><span class="graybg">局限与边界</span></div>
<p>ONNX 的价值很大，但它并不是“只要导出就一定能无缝部署”的万能格式。第一，并非所有训练框架中的算子都存在完美的 ONNX 映射，复杂模型可能导出失败，或需要改写成等价但更标准的图结构。第二，不同推理引擎对 ONNX 的支持程度并不完全一致：即使同样读取 ONNX 文件，也可能只支持某些 opset 版本或某一部分算子子集。第三，ONNX 更适合表达相对稳定的前向计算图；当模型强依赖高度动态的运行逻辑时，静态导出路径会更受约束。</p>
<p>因此，ONNX 更准确的定位不是“另一种训练框架”，而是<span style="background-color: #c0c0c0;">训练环境与部署环境之间的中间表示层</span>。它的意义在于把模型从原始框架内部释放出来，交给更适合推理、优化和跨平台执行的后端系统。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-2">人工智能理论知识 - 算法和经典机器学习</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ai-knowledge-quick-ref-2/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>人工智能理论知识 - 数学基础</title>
		<link>https://blog.gmem.cc/ai-knowledge-quick-ref-1</link>
		<comments>https://blog.gmem.cc/ai-knowledge-quick-ref-1#comments</comments>
		<pubDate>Wed, 15 Apr 2026 12:39:04 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[Math]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=40317</guid>
		<description><![CDATA[<p>这一篇整理 AI 所需的数学基础，包括基础数学、线性代数、微积分与概率论统计。它回答的核心问题是：模型里的向量、矩阵、导数、积分、概率分布、期望与信息量分别是什么意思，以及它们为什么会成为后续机器学习与深度学习的共同语言；后续篇章将在这些数学工具之上进入算法、学习理论与模型结构。 基础数学 代数基础 运算律与代数式 代数式（Algebraic Expression）由常数、变量与运算构成；给定变量取值后即可求值。化简（Simplification）的目标是把表达式写成更可读、更便于推导/比较的等价形式：合并同类项（Like Terms）、提取公因子（Common Factor）、展开（Expansion）与因式分解（Factorization）是最常见的操作。 运算律（Algebraic Laws）本质上是在某个数系/代数结构（例如实数域）中成立的恒等式（Identity）；它们允许在不改变值的前提下重排/重写表达式。需要区分：加法/乘法满足交换律与结合律，但减法/除法一般不满足。 性质 公式 备注 交换律（Commutativity） 不适用于 、 结合律（Associativity） 允许不改变括号结构地分组 分配律（Distributivity） 展开与提因式的核心 <a class="read-more" href="https://blog.gmem.cc/ai-knowledge-quick-ref-1">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-1">人工智能理论知识 - 数学基础</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>这一篇整理 AI 所需的数学基础，包括基础数学、线性代数、微积分与概率论统计。它回答的核心问题是：模型里的向量、矩阵、导数、积分、概率分布、期望与信息量分别是什么意思，以及它们为什么会成为后续机器学习与深度学习的共同语言；后续篇章将在这些数学工具之上进入算法、学习理论与模型结构。</p>
<div class="blog_h1"><span class="graybg">基础数学</span></div>
<div class="blog_h2"><span class="graybg">代数基础</span></div>
<div class="blog_h3"><span class="graybg">运算律与代数式</span></div>
<p>代数式（Algebraic Expression）由常数、变量与运算构成；给定变量取值后即可求值。化简（Simplification）的目标是把表达式写成更可读、更便于推导/比较的等价形式：合并同类项（Like Terms）、提取公因子（Common Factor）、展开（Expansion）与因式分解（Factorization）是最常见的操作。</p>
<p>运算律（Algebraic Laws）本质上是在某个数系/代数结构（例如实数域）中成立的恒等式（Identity）；它们允许在不改变值的前提下重排/重写表达式。需要区分：加法/乘法满足交换律与结合律，但减法/除法一般不满足。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">性质</td>
<td style="text-align: center;">公式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>交换律（Commutativity）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a+b=b+a\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(ab=ba\)</span></td>
<td>不适用于 <span displaypfx="inline-" class="mathjax-container">\(a-b\)</span>、<span displaypfx="inline-" class="mathjax-container">\(a/b\)</span></td>
</tr>
<tr>
<td>结合律（Associativity）</td>
<td><span displaypfx="inline-" class="mathjax-container">\((a+b)+c=a+(b+c)\)</span><br /><span displaypfx="inline-" class="mathjax-container">\((ab)c=a(bc)\)</span></td>
<td>允许不改变括号结构地分组</td>
</tr>
<tr>
<td>分配律（Distributivity）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a(b+c)=ab+ac\)</span></td>
<td>展开与提因式的核心</td>
</tr>
<tr>
<td>恒等元（Identity Element）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a+0=a\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(a\cdot 1=a\)</span></td>
<td>0 与 1 在代数推导中常被隐式使用</td>
</tr>
<tr>
<td>逆元（Inverse）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a+(-a)=0\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(a\cdot a^{-1}=1\)</span></td>
<td>第二式要求 <span displaypfx="inline-" class="mathjax-container">\(a\ne 0\)</span></td>
</tr>
<tr>
<td>零乘积原则（Zero Product）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(ab=0\Rightarrow a=0\ \text{或}\ b=0\)</span></td>
<td>把方程从“乘积=0”转为“因子=0”</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">因式分解（Factorization）</span></div>
<p>因式分解（Factorization）把一个表达式改写成若干因子（Factor）的乘积。它的直接价值是：把“值为 0 / 符号变化 / 约束条件”转成对各因子的分析。与之对偶的是展开（Expansion）：把乘积写成和式。</p>
<p>对多项式（Polynomial）<span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>，根（Root/Zero）与线性因子之间存在精确对应：若 <span displaypfx="inline-" class="mathjax-container">\(p(r)=0\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\((x-r)\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span> 的因子（因子定理（Factor Theorem））。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">套路/恒等式</td>
<td style="text-align: center;">形式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>提公因子（Common Factor）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(ax+ay=a(x+y)\)</span></td>
<td>先把最大公因子提出来</td>
</tr>
<tr>
<td>平方差（Difference of Squares）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^2-b^2=(a-b)(a+b)\)</span></td>
<td>常用于构造可约因子</td>
</tr>
<tr>
<td>完全平方（Perfect Square）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^2\pm 2ab+b^2=(a\pm b)^2\)</span></td>
<td>与配方（Completing the Square）等价</td>
</tr>
<tr>
<td>立方和/差（Sum/Difference of Cubes）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^3-b^3=(a-b)(a^2+ab+b^2)\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(a^3+b^3=(a+b)(a^2-ab+b^2)\)</span></td>
<td>二次因子在实数域可能不可再分</td>
</tr>
<tr>
<td>二次式按根分解</td>
<td><span displaypfx="inline-" class="mathjax-container">\(ax^2+bx+c=a(x-r_1)(x-r_2)\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(r_1,r_2\)</span> 可为复数</td>
</tr>
<tr>
<td>分组分解（Grouping）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(ax+ay+bx+by=(a+b)(x+y)\)</span></td>
<td>目标是制造共同因子</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">多项式（Polynomial）</span></div>
<p>一元多项式（Univariate Polynomial）是形如 <span displaypfx="inline-" class="mathjax-container">\(p(x)=\sum_{k=0}^{n} a_k x^k\)</span> 的函数，其中 <span displaypfx="inline-" class="mathjax-container">\(a_k\)</span> 是系数（Coefficient），<span displaypfx="inline-" class="mathjax-container">\(n\)</span> 是次数（Degree）。多项式在实数域上处处可导、可积；在局部逼近（例如 Taylor）与特征构造中非常常用。</p>
<p>多元多项式（Multivariate Polynomial）可写成对多重指数（Multi-index）求和：若 <span displaypfx="inline-" class="mathjax-container">\(x\in\mathbb{R}^d\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[p(x)=\sum_{\alpha\in\mathbb{N}^d} c_\alpha\,x^\alpha,\quad x^\alpha=\prod_{i=1}^{d} x_i^{\alpha_i}\]</span>
<p>补充：多元二次多项式（Quadratic Polynomial）的纯二次部分在线性代数里通常称为二次型（Quadratic Form），可写成矩阵形式 <span displaypfx="inline-" class="mathjax-container">\(q(\mathbf{x})=\mathbf{x}^\top A\mathbf{x}\)</span>；其中 <span displaypfx="inline-" class="mathjax-container">\(x_ix_j\)</span>（例如 <span displaypfx="inline-" class="mathjax-container">\(xy\)</span>）是交叉项（Cross Term）。</p>
<p>在机器学习里，<span style="background-color: #c0c0c0;">多项式特征（Polynomial Features）</span>把输入映射到包含高阶项的特征空间，等价于显式构造某些核函数（Kernel）的有限维版本。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">概念</td>
<td style="text-align: center;">表述</td>
<td style="text-align: center;">用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>首项/首项系数（Leading Term/Coefficient）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a_n x^n\)</span> / <span displaypfx="inline-" class="mathjax-container">\(a_n\)</span></td>
<td>决定远端增长阶与符号</td>
</tr>
<tr>
<td>余数定理（Remainder Theorem）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(p(x)=q(x)(x-a)+p(a)\)</span></td>
<td>快速计算 <span displaypfx="inline-" class="mathjax-container">\(p(a)\)</span></td>
</tr>
<tr>
<td>因子定理（Factor Theorem）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(p(a)=0\Leftrightarrow (x-a)\mid p(x)\)</span></td>
<td>把“根”与“线性因子”连接起来</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">一元二次方程（Quadratic Equation）</span></div>
<p>一元二次方程（Quadratic Equation）标准形式为 <span displaypfx="inline-" class="mathjax-container">\(ax^2+bx+c=0\)</span>（<span displaypfx="inline-" class="mathjax-container">\(a\ne 0\)</span>）。核心量是判别式（Discriminant）<span displaypfx="inline-" class="mathjax-container">\(\Delta=b^2-4ac\)</span>：它决定解的个数与类型。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">结论</td>
<td style="text-align: center;">公式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>求根公式（Quadratic Formula）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(x=\frac{-b\pm\sqrt{\Delta}}{2a}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\Delta<0[/latex] 时根为共轭复数</td>
</tr>
<tr>
<td>配方（Completing the Square）</td>
<td>[latex syntax=inline]ax^2+bx+c=a\left(x+\frac{b}{2a}\right)^2-\frac{\Delta}{4a}\)</span></td>
<td>同时给出顶点与最值</td>
</tr>
<tr>
<td>顶点（Vertex）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(x_v=-\frac{b}{2a},\quad f(x_v)=-\frac{\Delta}{4a}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a>0\)</span> 时为全局最小；<span displaypfx="inline-" class="mathjax-container">\(a<0[/latex] 时为全局最大</td>
</tr>
<tr>
<td>韦达定理（Vieta）</td>
<td>[latex syntax=inline]r_1+r_2=-\frac{b}{a},\quad r_1r_2=\frac{c}{a}\)</span></td>
<td>无需显式求根即可得到对称量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">分式与有理式（Rational Expressions）</span></div>
<p>分式/有理式（Rational Expression）是两个多项式之比：<span displaypfx="inline-" class="mathjax-container">\(\frac{p(x)}{q(x)}\)</span>，并要求 <span displaypfx="inline-" class="mathjax-container">\(q(x)\ne 0\)</span>。任何化简都必须保留定义域（Domain）约束：约分（Cancellation）只是在允许的点上重写表达式，不会“把不可取值点变得可取”。</p>
<p>典型例子： <span displaypfx="inline-" class="mathjax-container">\(\frac{x^2-1}{x-1}=\frac{(x-1)(x+1)}{x-1}=x+1\)</span>，但仍需强调 <span displaypfx="inline-" class="mathjax-container">\(x\ne 1\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(x=1\)</span> 是可去间断点（Removable Discontinuity）：原式无定义，而约分后的表达式在该点有值。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">操作</td>
<td style="text-align: center;">规则</td>
<td style="text-align: center;">要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>加减</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{a}{b}\pm\frac{c}{d}=\frac{ad\pm bc}{bd}\)</span></td>
<td>先通分（Common Denominator）</td>
</tr>
<tr>
<td>乘除</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{a}{b}\cdot\frac{c}{d}=\frac{ac}{bd}\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\frac{a}{b}\div\frac{c}{d}=\frac{a}{b}\cdot\frac{d}{c}\)</span></td>
<td>除法要求 <span displaypfx="inline-" class="mathjax-container">\(c\ne 0\)</span></td>
</tr>
<tr>
<td>约分</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{(x-r)u(x)}{(x-r)v(x)}=\frac{u(x)}{v(x)}\)</span></td>
<td>仍需保留 <span displaypfx="inline-" class="mathjax-container">\(x\ne r\)</span></td>
</tr>
<tr>
<td>符号分析</td>
<td>把数轴按零点/极点分段</td>
<td>有理不等式常用“区间符号表”</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">绝对值与分段（Absolute Value &amp; Piecewise）</span></div>
<p>绝对值（Absolute Value）<span displaypfx="inline-" class="mathjax-container">\(|x|\)</span> 表示到 0 的距离（Distance to Zero）。它把“正负”信息丢掉，只保留大小；因此绝对值相关方程/不等式通常要通过分段（Piecewise）把符号情况拆开讨论。</p>
<span displaypfx="" class="mathjax-container">\[|x|=\begin{cases}x,&amp; x\ge 0\\ -x,&amp; x<0\end{cases}[/latex]
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">形式</td>
<td style="text-align: center;">等价条件</td>
<td style="text-align: center;">前提</td>
</tr>
</thead>
<tbody>
<tr>
<td>[latex syntax=inline]|x|=a\]</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x=a\ \text{或}\ x=-a\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a\ge 0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(|x|<a[/latex]</td>
<td>[latex syntax=inline]-a<x<a[/latex]</td>
<td>[latex syntax=inline]a>0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(|x|\le a\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(-a\le x\le a\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a\ge 0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(|x|\ge a\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x\le -a\ \text{或}\ x\ge a\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a\ge 0\)</span></td>
</tr>
</tbody>
</table>
<p>三角不等式（Triangle Inequality）<span displaypfx="inline-" class="mathjax-container">\(|x+y|\le |x|+|y|\)</span> 是绝对值最重要的性质之一；它把“求和后的误差”上界化为“各自误差的和”，在误差分析与泛化界推导中高频出现。</p>
<p>深度学习里常见的 ReLU、hinge loss 等都是分段函数：<span style="background-color: #c0c0c0;">分段点处通常不可导，但仍可用次梯度（Subgradient）做优化</span>。</p>
<div class="blog_h3"><span class="graybg">常用代数技巧：配方 / 换元 / 估计</span></div>
<div class="blog_h4"><span class="graybg">配方（Completing the Square）</span></div>
<p>配方（Completing the Square）把二次式改写成“平方 + 常数”，从而直接读出最值、解的结构与可行区间：</p>
<span displaypfx="" class="mathjax-container">\[ax^2+bx+c=a\left(x+\frac{b}{2a}\right)^2-\frac{b^2-4ac}{4a}\]</span>
<p>例如 <span displaypfx="inline-" class="mathjax-container">\(x^2+6x+5=(x+3)^2-4\)</span>，因此方程 <span displaypfx="inline-" class="mathjax-container">\(x^2+6x+5=0\)</span> 等价于 <span displaypfx="inline-" class="mathjax-container">\((x+3)^2=4\)</span>，解为 <span displaypfx="inline-" class="mathjax-container">\(x=-1,-5\)</span>。</p>
<div class="blog_h4"><span class="graybg">换元（Substitution）</span></div>
<p>换元（Substitution）通过引入新变量，把原问题变成更低复杂度的标准形式，尤其适用于“重复结构”。例如：</p>
<span displaypfx="" class="mathjax-container">\[x^4-5x^2+4=0,\ \text{令 }u=x^2\Rightarrow u^2-5u+4=0\]</span>
<p>解得 <span displaypfx="inline-" class="mathjax-container">\(u=1,4\)</span>，再回代得到 <span displaypfx="inline-" class="mathjax-container">\(x=\pm 1,\pm 2\)</span>。</p>
<div class="blog_h4"><span class="graybg">估计（Bounding / Estimation）</span></div>
<p>估计（Bounding/Estimation）常把表达式改写为“非负项 + 常数”，或利用单调性把复杂项夹逼到可控区间。最常见的来源是“平方非负”（<span displaypfx="inline-" class="mathjax-container">\((\cdot)^2\ge 0\)</span>）：</p>
<span displaypfx="" class="mathjax-container">\[(x-1)^2\ge 0\Rightarrow x^2+1\ge 2x,\quad (|x|-1)^2\ge 0\Rightarrow x^2+1\ge 2|x|\]</span>
<p>这类估计在证明最值、构造上界/下界、以及把损失函数改写成“凸的主项 + 可控余项”时很有效。</p>
<div class="blog_h3"><span class="graybg">数系（Number Systems）</span></div>
<p>数系（Number Systems）描述“允许使用哪些数，以及这些数上哪些运算是封闭的”。常见链条是</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{N}\subset \mathbb{Z}\subset \mathbb{Q}\subset \mathbb{R}\subset \mathbb{C}\]</span>
<p>其中实数（Real Numbers）是有序（Ordered）且完备（Complete）的；复数（Complex Numbers）扩展了方程可解性（例如 <span displaypfx="inline-" class="mathjax-container">\(x^2+1=0\)</span> 在实数无解，但在复数有解）。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">数系</td>
<td style="text-align: center;">记号</td>
<td style="text-align: center;">典型元素</td>
<td style="text-align: center;">结构要点</td>
</tr>
</thead>
<tbody>
<tr>
<td>自然数（Natural Numbers）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{N}\)</span></td>
<td>0,1,2,…（是否含 0 取决于约定）</td>
<td>对加法/乘法封闭；一般不可做减法/除法</td>
</tr>
<tr>
<td>整数（Integers）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{Z}\)</span></td>
<td>…,-2,-1,0,1,2,…</td>
<td>对加减乘封闭；除法不封闭</td>
</tr>
<tr>
<td>有理数（Rational Numbers）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{Q}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(p/q\)</span>（<span displaypfx="inline-" class="mathjax-container">\(p,q\in\mathbb{Z},q\ne 0\)</span>）</td>
<td>域（Field）：非零元素存在乘法逆元</td>
</tr>
<tr>
<td>实数（Real Numbers）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span></td>
<td>包含无理数（Irrational），如 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{2},\pi\)</span></td>
<td>有序完备域；极限/连续的基础</td>
</tr>
<tr>
<td>复数（Complex Numbers）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{C}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a+bi\)</span></td>
<td>代数闭包（Algebraic Closure）；不可定义全序</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">函数基础</span></div>
<div class="blog_h3"><span class="graybg">定义域与值域</span></div>
<p>函数（Function）是映射：把输入集合中的每个元素映到一个输出。定义域（Domain）是允许的输入集合；值域（Range/Image）是实际能取到的输出集合（通常是陪域（Codomain）的子集）。常用记法：</p>
<span displaypfx="" class="mathjax-container">\[f:D\to Y,\quad x\mapsto f(x)\]</span>
<p>从表达式读定义域的常见约束：</p>
<ul>
<li>分母不为 0： <span displaypfx="inline-" class="mathjax-container">\(\frac{p(x)}{q(x)}\)</span> 要求 <span displaypfx="inline-" class="mathjax-container">\(q(x)\ne 0\)</span>。</li>
<li>偶次根非负： <span displaypfx="inline-" class="mathjax-container">\(\sqrt{g(x)}\)</span> 要求 <span displaypfx="inline-" class="mathjax-container">\(g(x)\ge 0\)</span>（实数域）。</li>
<li>对数正数： <span displaypfx="inline-" class="mathjax-container">\(\ln g(x)\)</span> 要求 <span displaypfx="inline-" class="mathjax-container">\(g(x)>0\)</span>。</li>
</ul>
<p>值域分析常用“解方程 + 约束”思路：令 <span displaypfx="inline-" class="mathjax-container">\(y=f(x)\)</span>，把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 表示成 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 并推导可行条件；若 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在某区间单调，则可用反函数直接得到值域。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">函数</td>
<td style="text-align: center;">定义域</td>
<td style="text-align: center;">值域</td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\sqrt{x}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x\ge 0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(y\ge 0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x>0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span></td>
</tr>
<tr>
<td>sigmoid（<span displaypfx="inline-" class="mathjax-container">\(\sigma(z)=\frac{1}{1+e^{-z}}\)</span>）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\((0,1)\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\tanh z\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\((-1,1)\)</span></td>
</tr>
<tr>
<td>ReLU（<span displaypfx="inline-" class="mathjax-container">\(\max(0,z)\)</span>）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\([0,+\infty)\)</span></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">复合函数</span></div>
<p>复合函数（Function Composition）把一个函数的输出作为另一个函数的输入：若 <span displaypfx="inline-" class="mathjax-container">\(g:D\to E\)</span>、<span displaypfx="inline-" class="mathjax-container">\(f:E\to Y\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[(f\circ g)(x)=f(g(x))\]</span>
<p>定义域必须同时满足两层约束： <span displaypfx="inline-" class="mathjax-container">\(x\in\mathrm{dom}(g)\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(g(x)\in\mathrm{dom}(f)\)</span>。例如 <span displaypfx="inline-" class="mathjax-container">\(f(x)=\sqrt{x}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(g(x)=x^2-1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\((f\circ g)(x)=\sqrt{x^2-1}\)</span> 的定义域是 <span displaypfx="inline-" class="mathjax-container">\(|x|\ge 1\)</span>。</p>
<p>复合满足结合律（Associativity）：<span displaypfx="inline-" class="mathjax-container">\((f\circ g)\circ h=f\circ(g\circ h)\)</span>，但一般不满足交换律： <span displaypfx="inline-" class="mathjax-container">\(f\circ g\ne g\circ f\)</span>。</p>
<div class="blog_h3"><span class="graybg">反函数</span></div>
<p>反函数（Inverse Function）把映射“倒过来”。若 <span displaypfx="inline-" class="mathjax-container">\(f:D\to Y\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 上是双射（Bijection），则存在 <span displaypfx="inline-" class="mathjax-container">\(f^{-1}:\mathrm{range}(f)\to D\)</span> 满足</p>
<span displaypfx="" class="mathjax-container">\[f^{-1}(f(x))=x,\quad f(f^{-1}(y))=y\]</span>
<p>注意 <span displaypfx="inline-" class="mathjax-container">\(f^{-1}\)</span> 表示反函数，不是倒数 <span displaypfx="inline-" class="mathjax-container">\(1/f\)</span>。求反函数的常用步骤是：设 <span displaypfx="inline-" class="mathjax-container">\(y=f(x)\)</span>，交换 <span displaypfx="inline-" class="mathjax-container">\(x,y\)</span> 并解出 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>。</p>
<p>例：线性函数 <span displaypfx="inline-" class="mathjax-container">\(y=ax+b\)</span>（<span displaypfx="inline-" class="mathjax-container">\(a\ne 0\)</span>）的反函数是 <span displaypfx="inline-" class="mathjax-container">\(f^{-1}(y)=\frac{y-b}{a}\)</span>。sigmoid 的反函数是 logit：若 <span displaypfx="inline-" class="mathjax-container">\(p=\sigma(z)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(z=\log\frac{p}{1-p}\)</span>（要求 <span displaypfx="inline-" class="mathjax-container">\(p\in(0,1)\)</span>）。</p>
<p>若函数不单调或不可一一对应（如 <span displaypfx="inline-" class="mathjax-container">\(f(x)=x^2\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span> 上），则必须限制定义域（例如限制为 <span displaypfx="inline-" class="mathjax-container">\(x\ge 0\)</span>）才能得到真正的反函数。</p>
<div class="blog_h3"><span class="graybg">奇偶性与单调性</span></div>
<p>奇偶性（Parity）描述对称性：偶函数（Even Function）满足 <span displaypfx="inline-" class="mathjax-container">\(f(-x)=f(x)\)</span>（关于 y 轴对称），奇函数（Odd Function）满足 <span displaypfx="inline-" class="mathjax-container">\(f(-x)=-f(x)\)</span>（关于原点对称）。</p>
<p>单调性（Monotonicity）描述“随输入增加，输出是否不减/不增”。在区间 <span displaypfx="inline-" class="mathjax-container">\(I\)</span> 上：</p>
<ul>
<li>单调递增（Monotone Increasing）：<span displaypfx="inline-" class="mathjax-container">\(x_1<x_2\Rightarrow f(x_1)\le f(x_2)[/latex]。</li>
<li>严格递增（Strictly Increasing）：[latex syntax=inline]x_1<x_2\Rightarrow f(x_1)<f(x_2)[/latex]。</li>
<li>单调递减/严格递减同理。</li>
</ul>
<p>单调函数在区间上必为单射（Injective），因此在该区间上可定义反函数。这也是为什么很多“不可逆”的函数（如 [latex syntax=inline]x^2\)</span>）在限制到某个单调区间后就变得可逆。</p>
<div class="blog_h3"><span class="graybg">凸性与凹性</span></div>
<p>凸性（Convexity）是优化与泛化分析的核心几何性质。函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在区间/凸集上是凸函数（Convex Function），当且仅当对任意 <span displaypfx="inline-" class="mathjax-container">\(x_1,x_2\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\lambda\in[0,1]\)</span> 都有</p>
<span displaypfx="" class="mathjax-container">\[f(\lambda x_1+(1-\lambda)x_2)\le \lambda f(x_1)+(1-\lambda)f(x_2)\]</span>
<p>凹函数（Concave Function）则把不等号方向反过来。几何上：凸函数“弦在图像上方”，凹函数“弦在图像下方”。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 二阶可导，则一维判别很简单： <span displaypfx="inline-" class="mathjax-container">\(f''(x)\ge 0\)</span> 则凸， <span displaypfx="inline-" class="mathjax-container">\(f''(x)\le 0\)</span> 则凹；多变量情形把 <span displaypfx="inline-" class="mathjax-container">\(f''\)</span> 替换为 Hessian，要求其半正定/半负定。</p>
<p>典型例子： <span displaypfx="inline-" class="mathjax-container">\(x^2\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(e^x\)</span> 是凸函数；<span displaypfx="inline-" class="mathjax-container">\(\log x\)</span>（<span displaypfx="inline-" class="mathjax-container">\(x>0\)</span>）是凹函数。很多经典损失（如 MSE、logistic loss）对模型输出是凸的，但对深度网络参数整体通常非凸。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/convex.jpg"><img class="alignnone size-full wp-image-40553" src="https://blog.gmem.cc/wp-content/uploads/2026/03/convex.jpg" alt="convex" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">方程与超平面</span></div>
<div class="blog_h3"><span class="graybg">线性方程（Ax + By + C = 0）</span></div>
<p>二维平面中，线性方程（Linear Equation）<span displaypfx="inline-" class="mathjax-container">\(Ax+By+C=0\)</span>（<span displaypfx="inline-" class="mathjax-container">\((A,B)\ne(0,0)\)</span>）表示一条直线（Line）。向量 <span displaypfx="inline-" class="mathjax-container">\((A,B)\)</span> 是法向量（Normal Vector）：它与直线方向垂直；常数项 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 控制沿法向量方向的平移。</p>
<p>该形式与超平面形式 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\cdot\mathbf{x}+b=0\)</span> 完全一致：只需取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x,y)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=(A,B)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(b=C\)</span>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">等价形式</td>
<td style="text-align: center;">表达式</td>
<td style="text-align: center;">条件/说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>斜截式（Slope-Intercept）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y=mx+b\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(B\ne 0\)</span> 时 <span displaypfx="inline-" class="mathjax-container">\(m=-A/B,\ b=-C/B\)</span></td>
</tr>
<tr>
<td>截距（Intercepts）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(x\text{-截距}=-C/A\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(y\text{-截距}=-C/B\)</span></td>
<td>分别要求 <span displaypfx="inline-" class="mathjax-container">\(A\ne 0\)</span>、<span displaypfx="inline-" class="mathjax-container">\(B\ne 0\)</span></td>
</tr>
<tr>
<td>点法式（Point-Normal）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\cdot(\mathbf{x}-\mathbf{x}_0)=0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_0\)</span> 是直线上一点</td>
</tr>
<tr>
<td>点到直线距离</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{dist}=\frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}}\)</span></td>
<td>来自把点沿法向量投影到直线</td>
</tr>
</tbody>
</table>
<p>两条直线相交/平行可由法向量判断：若 <span displaypfx="inline-" class="mathjax-container">\((A_1,B_1)\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\((A_2,B_2)\)</span> 共线，则两直线平行（或重合）；否则相交，交点可由 2×2 线性方程组求解。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/linear-equation-geometry.jpg"><img class="alignnone size-full wp-image-40541" src="https://blog.gmem.cc/wp-content/uploads/2026/03/linear-equation-geometry.jpg" alt="linear-equation-geometry" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">超平面（w·x + b = 0）</span></div>
<p>超平面（Hyperplane）是高维空间中的“线性边界”。在 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^d\)</span> 中，方程</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}\cdot \mathbf{x}+b=0\]</span>
<p>定义一个 <span displaypfx="inline-" class="mathjax-container">\((d-1)\)</span> 维的仿射子空间（Affine Subspace）。其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 是法向量（Normal Vector），决定边界的朝向；<span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是偏置（Bias），决定边界沿法向量方向的平移。</p>
<p>工程与论文里常写成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top \mathbf{x}+b\)</span>：这里的转置（Transpose）符号 <span displaypfx="inline-" class="mathjax-container">\(^\top\)</span> 只是为了把列向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 变成行向量，从而与列向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 做矩阵乘法；数值上它等价于点积：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}^\top \mathbf{x}=\sum_{i=1}^{d} w_i x_i\]</span>
<p>例：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=(2,3)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(4,5)^\top\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top \mathbf{x}=2\cdot4+3\cdot5=23\)</span>。</p>
<p>在机器学习里，线性分类器（Linear Classifier）可写成 <span displaypfx="inline-" class="mathjax-container">\(\hat y=\mathrm{sign}(\mathbf{w}^\top\mathbf{x}+b)\)</span>；逻辑回归（Logistic Regression）把它送入 sigmoid： <span displaypfx="inline-" class="mathjax-container">\(p(y=1|\mathbf{x})=\sigma(\mathbf{w}^\top\mathbf{x}+b)\)</span>。</p>
<div class="blog_h3"><span class="graybg">法向量与半空间</span></div>
<p>超平面把空间划分成两个半空间（Half-space）：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}\cdot\mathbf{x}+b\ge 0,\quad \mathbf{w}\cdot\mathbf{x}+b\le 0\]</span>
<p>法向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 指向“值更大”的一侧：沿 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 方向移动会增大 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\cdot\mathbf{x}+b\)</span>。这也是为什么在线性分类里，得分的正负号自然对应类别划分。</p>
<div class="blog_h3"><span class="graybg">点到超平面的距离</span></div>
<p>点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_0\)</span> 到超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\cdot\mathbf{x}+b=0\)</span> 的欧氏距离（Euclidean Distance）为：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}(\mathbf{x}_0,\ \mathbf{w}\cdot\mathbf{x}+b=0)=\frac{|\mathbf{w}\cdot\mathbf{x}_0+b|}{\|\mathbf{w}\|_2}\]</span>
<p>推导直觉：把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_0\)</span> 沿法向量方向投影到超平面上；分子是“沿法向量方向的带符号位移”，除以 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{w}\|\)</span> 把它变成真实距离。</p>
<div class="blog_h3"><span class="graybg">约束函数、梯度与法向量</span></div>
<p>约束优化中的边界通常由一个定义在整个空间上的标量函数（Scalar Function）<span displaypfx="inline-" class="mathjax-container">\(g(x)\)</span> 给出，并通过等值方程 <span displaypfx="inline-" class="mathjax-container">\(g(x)=0\)</span> 表示边界本身。函数 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 是求导对象；边界则是满足该方程的点集。梯度（Gradient）算子 <span displaypfx="inline-" class="mathjax-container">\(\nabla\)</span> 作用在约束函数 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 上，由此把边界的法向几何信息编码为一个向量场。</p>
<p>线性超平面的情形最直接。边界写成</p>
<span displaypfx="" class="mathjax-container">\[g(\mathbf{x})=\mathbf{w}^\top\mathbf{x}+b=0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 是定义在整个 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^d\)</span> 上的线性函数，而边界只是它的零等值面（Zero Level Set）。由于线性函数的一阶导数处处相同，立刻得到</p>
<span displaypfx="" class="mathjax-container">\[\nabla g(\mathbf{x})=\mathbf{w}\]</span>
<p>因此，线性超平面的法向量是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span>，本质上等价于“定义该超平面的约束函数 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 的梯度等于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span>”。</p>
<p>这一结论对一般光滑边界同样成立。设 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 是边界 <span displaypfx="inline-" class="mathjax-container">\(g(x)=0\)</span> 上一点，且 <span displaypfx="inline-" class="mathjax-container">\(\nabla g(x^*)\ne 0\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 是该点处的切向量（Tangent Vector），则沿着 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 做无穷小移动仍停留在同一条边界上，因而 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 的一阶变化为 0：</p>
<span displaypfx="" class="mathjax-container">\[\frac{d}{d\epsilon}g(x^*+\epsilon t)\Big|_{\epsilon=0}=0\]</span>
<p>应用链式法则（Chain Rule）得到</p>
<span displaypfx="" class="mathjax-container">\[\nabla g(x^*)^\top t=0\]</span>
<p>这表示边界上的任意切向量都与 <span displaypfx="inline-" class="mathjax-container">\(\nabla g(x^*)\)</span> 正交。因此 <span displaypfx="inline-" class="mathjax-container">\(\nabla g(x^*)\)</span> 沿法向方向指向边界外侧或内侧，而不沿边界本身滑动；梯度正是边界法向量的解析表达。</p>
<p>KKT 条件中的 <span displaypfx="inline-" class="mathjax-container">\(\nabla g(x^*)\)</span> 正是以这种方式出现的。约束优化不是对“边界这个集合”求导，而是借助约束函数 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 的梯度，把边界的法向几何结构写入一阶最优性条件。驻点条件</p>
<span displaypfx="" class="mathjax-container">\[\nabla f(x^*)+\lambda^*\nabla g(x^*)=0\]</span>
<p>表达的是：在活跃边界上，目标函数剩余的下降趋势完全落在约束边界的法向空间里，并与乘子加权后的法向量达到平衡。</p>
<div class="blog_h2"><span class="graybg">基础几何（Basic Geometry）</span></div>
<p>解析几何（Analytic Geometry）、向量（Vector）、三角函数（Trigonometric Functions）以及很多 AI 中的空间直觉，都建立在更基础的几何概念上。这里先把最常用的几块地基补齐：距离、角度、弧度、比例与面积。</p>
<div class="blog_h3"><span class="graybg">点、线、角与角度单位</span></div>
<p>平面几何最基本的对象是点（Point）、线段（Segment）、直线（Line）与角（Angle）。角度描述两条射线的张开程度；常见有两种单位：</p>
<span displaypfx="" class="mathjax-container">\[360^\circ=2\pi\ \text{rad},\quad 180^\circ=\pi\ \text{rad},\quad 90^\circ=\frac{\pi}{2}\ \text{rad}\]</span>
<p>度数（Degree）更适合日常表达，弧度（Radian）更适合数学推导，因为它和圆弧长度、三角函数、导数公式天然兼容。后面遇到旋转矩阵（Rotation Matrix）、复数极坐标（Polar Form）、傅里叶分析（Fourier Analysis）时，默认几乎都使用弧度。</p>
<div class="blog_h3"><span class="graybg">勾股定理与欧氏距离</span></div>
<p>勾股定理（Pythagorean Theorem）是平面距离公式的根源。对直角三角形，若两条直角边长为 <span displaypfx="inline-" class="mathjax-container">\(a,b\)</span>，斜边长为 <span displaypfx="inline-" class="mathjax-container">\(c\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[a^2+b^2=c^2\]</span>
<p>把它应用到坐标平面，就得到两点之间的欧氏距离（Euclidean Distance）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}(P_1,P_2)=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\]</span>
<p>在高维空间里，这个公式直接推广为 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}-\mathbf{y}\|_2\)</span>。因此从二维几何到机器学习里的向量距离，本质上是一条连续的概念链。KNN、K-means、embedding 检索、对比学习（Contrastive Learning）都在反复使用这套“距离越小越相似”的几何直觉。</p>
<div class="blog_h3"><span class="graybg">弧度、弧长与扇形</span></div>
<p>弧度由圆弧长度直接定义：若半径为 <span displaypfx="inline-" class="mathjax-container">\(r\)</span> 的圆上有一段弧长 <span displaypfx="inline-" class="mathjax-container">\(s\)</span>，对应圆心角为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>（弧度），则</p>
<span displaypfx="" class="mathjax-container">\[\theta=\frac{s}{r},\quad s=r\theta\]</span>
<p>对应扇形面积（Sector Area）是：</p>
<span displaypfx="" class="mathjax-container">\[A=\frac{1}{2}r^2\theta\]</span>
<p>这正是弧度“自然”的原因：一旦用弧度记角，弧长与面积公式会变得非常干净。AI 里很多周期性表示都默认使用弧度输入，例如 <span displaypfx="inline-" class="mathjax-container">\(\sin\theta\)</span> / <span displaypfx="inline-" class="mathjax-container">\(\cos\theta\)</span> 的位置编码（Positional Encoding）、旋转位置编码（RoPE）以及频域特征（Fourier Features）。</p>
<div class="blog_h3"><span class="graybg">相似、缩放与比例</span></div>
<p>相似（Similarity）指图形形状相同、大小可以不同；等价地说，对应角相等、对应边成比例。若把一个图形按比例 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 缩放，则长度变为原来的 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 倍，面积变为原来的 <span displaypfx="inline-" class="mathjax-container">\(k^2\)</span> 倍。</p>
<p>这个看似初等的事实，在 AI 图像处理中极其常见：图片 resize、本征尺度（Scale）、特征金字塔（Feature Pyramid）、多尺度检测（Multi-scale Detection）都在处理“同一对象在不同尺度下如何保持可识别性”的问题。若缩放时不保持纵横比（Aspect Ratio），就会引入几何畸变，进而影响分类、检测与分割结果。</p>
<div class="blog_h3"><span class="graybg">面积、重叠与 IoU</span></div>
<p>基础几何里，面积（Area）衡量二维区域所占的大小。矩形面积是长乘宽，圆面积是</p>
<span displaypfx="" class="mathjax-container">\[A=\pi r^2\]</span>
<p>在 AI 的目标检测（Object Detection）与实例分割（Instance Segmentation）中，一个高频几何量是交并比（Intersection over Union, IoU）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{IoU}=\frac{\text{Intersection Area}}{\text{Union Area}}\]</span>
<p>它衡量预测框/预测区域与真实标注的重叠程度。这里用到的不是高深数学，而是最朴素的面积与重叠概念。</p>
<div class="blog_h3"><span class="graybg">基础几何和AI</span></div>
<p>若把这些内容压缩成一句话，它们在 AI 中分别承担不同角色：</p>
<ul>
<li>距离（Distance）：支撑近邻搜索、聚类、向量检索与损失函数中的相似度刻画。</li>
<li>角度（Angle）：支撑方向、夹角、余弦相似度（Cosine Similarity）与旋转直觉。</li>
<li>弧度（Radian）：支撑三角函数、周期建模、位置编码与频域表示。</li>
<li>比例与缩放（Scale）：支撑图像 resize、数据增强、特征金字塔与多尺度建模。</li>
<li>面积与重叠（Area &amp; Overlap）：支撑 IoU、检测框评估与分割质量度量。</li>
</ul>
<div class="blog_h2"><span class="graybg">解析几何（Analytic Geometry）</span></div>
<p>解析几何（Analytic Geometry）的核心做法是：选定坐标系（Coordinate System），用代数方程描述几何对象。一个几何对象可以被理解为“满足某个方程（或方程组）的所有点”的集合。</p>
<p>高中阶段最常见的两类：</p>
<ul>
<li>一次方程：直线（Line）/平面（Plane）/超平面（Hyperplane）。</li>
<li>二次方程：圆（Circle）与圆锥曲线（Conic Sections）；在三维中推广为二次曲面（Quadric Surfaces）。</li>
</ul>
<div class="blog_h3"><span class="graybg">坐标、距离与圆</span></div>
<p>在直角坐标系（Cartesian Coordinate System）中，点 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 用坐标 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 表示。两点 <span displaypfx="inline-" class="mathjax-container">\(P_1(x_1,y_1)\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(P_2(x_2,y_2)\)</span> 的欧氏距离（Euclidean Distance）是：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}(P_1,P_2)=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\]</span>
<p>圆（Circle）是到某个固定点距离恒定的点集；这个固定点叫圆心（Center）。若圆心为 <span displaypfx="inline-" class="mathjax-container">\((h,k)\)</span>、半径（Radius）为 <span displaypfx="inline-" class="mathjax-container">\(r\)</span>，则圆的方程是：</p>
<span displaypfx="" class="mathjax-container">\[(x-h)^2+(y-k)^2=r^2\]</span>
<p>例：方程 <span displaypfx="inline-" class="mathjax-container">\(x^2+y^2-4x+6y-12=0\)</span> 通过配方（Completing the Square）可化为 <span displaypfx="inline-" class="mathjax-container">\((x-2)^2+(y+3)^2=25\)</span>，因此它表示圆心 <span displaypfx="inline-" class="mathjax-container">\((2,-3)\)</span>、半径 <span displaypfx="inline-" class="mathjax-container">\(5\)</span> 的圆。</p>
<div class="blog_h3"><span class="graybg">圆锥曲线（Conic Sections）：二次方程的几何形状</span></div>
<p>圆锥曲线（Conic Sections）最初来自“平面截圆锥”的几何构造，但在解析几何里，它们等价于二维的二次方程曲线（Second-degree Plane Curves，方程里变量的最高次数为 2）：</p>
<span displaypfx="" class="mathjax-container">\[Ax^2+Bxy+Cy^2+Dx+Ey+F=0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(Bxy\)</span> 是交叉项（Cross Term）。交叉项的存在通常意味着曲线的主轴（Principal Axes）与坐标轴不对齐；通过旋转坐标轴（Rotation of Axes）可以把交叉项消掉，从而得到更“标准”的形状表达。</p>
<p>而一次项 <span displaypfx="inline-" class="mathjax-container">\(Dx+Ey\)</span> 与常数项 <span displaypfx="inline-" class="mathjax-container">\(F\)</span> 扮演的是另一类角色：它们通常不改变主轴方向，而主要影响图形在平面中的<span style="background-color: #c0c0c0;">位置与尺度</span>。更具体地说，一次项往往意味着曲线的中心/顶点不在原点；在消去交叉项之后，再通过平移坐标（Translation of Axes）与配方（Completing the Square）可以把一次项吸收到平方项里。常数项则相当于改变“等号右边的阈值”，会影响曲线是否有实点、整体大小以及离原点的偏置。简言之：<span style="background-color: #c0c0c0;">交叉项主要对应旋转，一次项主要对应平移，常数项主要对应整体偏移/尺度调整</span>。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/ellipse-term-effects.png"><img class="alignnone size-full wp-image-40767" src="https://blog.gmem.cc/wp-content/uploads/2026/03/ellipse-term-effects.png" alt="ellipse-term-effects" width="100%" /></a></p>
<p>下面给出最常用的四类圆锥曲线的标准方程（Standard Form）与直观定义：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">名称</td>
<td style="text-align: center;">标准方程</td>
<td style="text-align: center;">几何定义（直观）</td>
<td style="text-align: center;">形状关键词</td>
</tr>
</thead>
<tbody>
<tr>
<td>圆（Circle）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(x^2+y^2=r^2\)</span></td>
<td>到圆心距离恒为 <span displaypfx="inline-" class="mathjax-container">\(r\)</span></td>
<td>闭合；各向同性</td>
</tr>
<tr>
<td>椭圆（Ellipse）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{x^2}{a^2}+\frac{y^2}{b^2}=1\ (a\ge b>0)\)</span></td>
<td>到两个焦点（Focus）距离和为常数</td>
<td>闭合；主轴/次轴</td>
</tr>
<tr>
<td>抛物线（Parabola）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y^2=4px\ (p>0)\)</span></td>
<td>到焦点（Focus）与准线（Directrix）距离相等</td>
<td>开口；无中心</td>
</tr>
<tr>
<td>双曲线（Hyperbola）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{x^2}{a^2}-\frac{y^2}{b^2}=1\)</span></td>
<td>到两个焦点（Focus）距离差的绝对值为常数</td>
<td>两支；有渐近线（Asymptotes）</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">椭圆（Ellipse）：两焦点“距离和恒定”</span></div>
<p>椭圆（Ellipse）可以用一句话定义：平面内到两个固定点 <span displaypfx="inline-" class="mathjax-container">\(F_1,F_2\)</span> 的距离之和为常数的点的集合。若该常数为 <span displaypfx="inline-" class="mathjax-container">\(2a\)</span>，则对椭圆上任意点 <span displaypfx="inline-" class="mathjax-container">\(P\)</span> 有：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{dist}(P,F_1)+\mathrm{dist}(P,F_2)=2a\]</span>
<p>在以原点为中心、长轴沿 x 轴的标准位置下，椭圆方程是：</p>
<span displaypfx="" class="mathjax-container">\[\frac{x^2}{a^2}+\frac{y^2}{b^2}=1,\quad a\ge b>0\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 分别是半长轴（Semi-major Axis）与半短轴（Semi-minor Axis）。焦距参数 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 定义为焦点到中心的距离，满足</p>
<span displaypfx="" class="mathjax-container">\[c^2=a^2-b^2\]</span>
<p>因此焦点坐标是 <span displaypfx="inline-" class="mathjax-container">\((\pm c,0)\)</span>。偏心率（Eccentricity）定义为 <span displaypfx="inline-" class="mathjax-container">\(e=c/a\)</span>，它量化“椭圆有多扁”： <span displaypfx="inline-" class="mathjax-container">\(e\in[0,1)\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(a=b\)</span> 时 <span displaypfx="inline-" class="mathjax-container">\(e=0\)</span>，椭圆退化为圆。</p>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(a=5,b=3\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(c=4\)</span>、<span displaypfx="inline-" class="mathjax-container">\(e=0.8\)</span>，椭圆为 <span displaypfx="inline-" class="mathjax-container">\(\frac{x^2}{25}+\frac{y^2}{9}=1\)</span>，焦点为 <span displaypfx="inline-" class="mathjax-container">\((\pm 4,0)\)</span>。</p>
<div class="blog_h3"><span class="graybg">三维推广：二次曲面（Quadric Surfaces）</span></div>
<p>在三维中，圆锥曲线推广为二次曲面（Quadric Surfaces）：满足三元二次方程的点集。最一般的形式是：</p>
<span displaypfx="" class="mathjax-container">\[Ax^2+By^2+Cz^2+Dxy+Exz+Fyz+Gx+Hy+Iz+J=0\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(Dxy,Exz,Fyz\)</span> 是交叉项（Cross Term），对应“坐标轴没有对齐到曲面的主轴方向”。通过平移（Translation）与旋转（Rotation）可以把它化为标准型（Standard Form）：平移等价于把原点挪到合适的位置（通常是“中心”附近），旋转等价于把坐标轴转到主轴方向，从而一眼看出是“球/椭球/抛物面/双曲面”等哪一类。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">名称</td>
<td style="text-align: center;">典型方程（标准型）</td>
<td style="text-align: center;">直观描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>球（Sphere）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(x^2+y^2+z^2=r^2\)</span></td>
<td>到中心距离恒定的点集（3D 的圆）</td>
</tr>
<tr>
<td>椭球（Ellipsoid）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{x^2}{a^2}+\frac{y^2}{b^2}+\frac{z^2}{c^2}=1\)</span></td>
<td>三个方向缩放不同的“球”；仍然闭合</td>
</tr>
<tr>
<td>椭圆抛物面（Elliptic Paraboloid）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(z=\frac{x^2}{a^2}+\frac{y^2}{b^2}\)</span></td>
<td>“碗状”；水平截面是椭圆</td>
</tr>
<tr>
<td>双曲抛物面（Hyperbolic Paraboloid）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(z=\frac{x^2}{a^2}-\frac{y^2}{b^2}\)</span></td>
<td>“马鞍形”；沿一个方向上凸、另一个方向下凹</td>
</tr>
<tr>
<td>单叶双曲面（Hyperboloid of One Sheet）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{x^2}{a^2}+\frac{y^2}{b^2}-\frac{z^2}{c^2}=1\)</span></td>
<td>连通的一张曲面；截面随方向变化</td>
</tr>
<tr>
<td>双叶双曲面（Hyperboloid of Two Sheets）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(-\frac{x^2}{a^2}-\frac{y^2}{b^2}+\frac{z^2}{c^2}=1\)</span></td>
<td>上下分离的两张曲面</td>
</tr>
</tbody>
</table>
<p>二次曲面与优化中的“二次型/曲率”是同一套数学语言：把坐标轴旋到主轴方向后，表达式会变成各轴平方项的加权和/差，从而直接暴露“碗状（局部最小）”与“鞍形（Saddle）”的结构。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-quadric.png"><img class="alignnone size-full wp-image-40777" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-quadric.png" alt="plot-quadric" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">指数函数</span></div>
<p>指数函数（Exponential Function）最常用的是自然指数 <span displaypfx="inline-" class="mathjax-container">\(e^x\)</span>。指数运算把“加法结构”映射为“乘法结构”，对数（Logarithm）作为反函数则把乘法结构拉平成加法结构。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">性质/公式</td>
<td style="text-align: center;">表达式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>指数基本性质（Exponential Laws）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{a+b}=e^a e^b\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(e^{a-b}=\frac{e^a}{e^b}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a,b\in\mathbb{R}\)</span></td>
</tr>
<tr>
<td>指数运算律（Exponent Rules）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^0=1\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(a^m a^n=a^{m+n}\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\frac{a^m}{a^n}=a^{m-n}\)</span><br /><span displaypfx="inline-" class="mathjax-container">\((a^m)^n=a^{mn}\)</span><br /><span displaypfx="inline-" class="mathjax-container">\((ab)^n=a^n b^n\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a>0,a\ne 1\)</span>；对整数指数最直接</td>
</tr>
<tr>
<td>自然常数（Euler's Number）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(e=\lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n\)</span></td>
<td>极限刻画连续复利（Continuous Compounding）</td>
</tr>
<tr>
<td>微积分性质</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{d}{dx}e^x=e^x\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\frac{d}{dx}\ln x=\frac{1}{x}\)</span></td>
<td>以 <span displaypfx="inline-" class="mathjax-container">\(e\)</span> 为底时形式最简</td>
</tr>
<tr>
<td>连续增长微分方程</td>
<td><span displaypfx="inline-" class="mathjax-container">\(y'(t)=y(t),\ y(0)=1\Rightarrow y(t)=e^t\)</span></td>
<td>“增长率与当前值成正比”</td>
</tr>
<tr>
<td>与对数互逆</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln(e^x)=x\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(e^{\ln x}=x\)</span></td>
<td>第二式要求 <span displaypfx="inline-" class="mathjax-container">\(x>0\)</span></td>
</tr>
</tbody>
</table>
<p>常用取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">输入</td>
<td style="text-align: center;"><span displaypfx="inline-" class="mathjax-container">\(e^x\)</span></td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x=0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^0=1\)</span></td>
<td>基准点</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x=1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^1=e\approx 2.71828\)</span></td>
<td>自然对数底</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x=-1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{-1}=\frac{1}{e}\approx 0.36788\)</span></td>
<td>常见衰减尺度</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x=\ln 2\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{\ln 2}=2\)</span></td>
<td>对数域与线性域互换时常用</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x=\ln 10\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{\ln 10}=10\)</span></td>
<td>与 <span displaypfx="inline-" class="mathjax-container">\(\log_{10}\)</span> 换底相关</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">对数函数</span></div>
<p>对数函数（Logarithm）<span displaypfx="inline-" class="mathjax-container">\(\log x\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(x>0\)</span> 上严格单调递增（Strictly Increasing），因此 <span displaypfx="inline-" class="mathjax-container">\(-\log x\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(x>0\)</span> 上严格单调递减（Strictly Decreasing）。</p>
<p>复合后的单调性由内层决定：若 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 在某区间上单调递增且 <span displaypfx="inline-" class="mathjax-container">\(f(x)>0\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(-\log(f(x))\)</span> 在该区间上单调递减；若 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 不单调，则外层单调并不能推出整体单调。</p>
<p>在损失函数里常见的形式是 <span displaypfx="inline-" class="mathjax-container">\(-\log\sigma(z)\)</span>（<span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 为 sigmoid）。因为 sigmoid 单调递增，所以该损失对 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 单调递减：增大 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 会降低损失。需要注意的是，这只说明“对中间量 z 的单调性”；对模型参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的单调性一般不成立，因为 <span displaypfx="inline-" class="mathjax-container">\(z=z(\theta)\)</span> 是高维非线性函数。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">性质/公式</td>
<td style="text-align: center;">表达式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>对数运算律（Logarithm Rules）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\log(ab)=\log a+\log b\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\log\!\left(\frac{a}{b}\right)=\log a-\log b\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\log(a^k)=k\log a\)</span></td>
<td>同一底数；典型要求 <span displaypfx="inline-" class="mathjax-container">\(a>0,b>0\)</span></td>
</tr>
<tr>
<td>换底公式（Change of Base）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\log_a x=\frac{\ln x}{\ln a}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a>0,a\ne 1,x>0\)</span></td>
</tr>
<tr>
<td>与指数互逆</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^{\log_a x}=x\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\log_a(a^x)=x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x>0\)</span></td>
</tr>
</tbody>
</table>
<p>常用取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">表达式</td>
<td style="text-align: center;">值</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln 1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x=1\)</span> 为基准点</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln e\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(1\)</span></td>
<td>自然对数的定义性质</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln 2\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\approx 0.6931\)</span></td>
<td>二进制相关常数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln 10\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\approx 2.3026\)</span></td>
<td>十进制相关常数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\log_{10}2\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(=\frac{\ln 2}{\ln 10}\approx 0.3010\)</span></td>
<td>工程里常用于数量级估算</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\log_2 10\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(=\frac{\ln 10}{\ln 2}\approx 3.3219\)</span></td>
<td>bit 与十进制数量级换算</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\log_2 e\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(=\frac{1}{\ln 2}\approx 1.4427\)</span></td>
<td>nats 与 bits 换算常数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\log_{10}e\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(=\frac{1}{\ln 10}\approx 0.4343\)</span></td>
<td>自然对数与常用对数换算</td>
</tr>
</tbody>
</table>
<p>在语言模型 softmax 中，logit 经过指数再归一化：<span displaypfx="inline-" class="mathjax-container">\(\exp(\text{logit})\)</span> 把分数映射为正数权重；取 log 则把乘法结构拉平成加法结构，便于用和式写出似然与损失。</p>
<div class="blog_h2"><span class="graybg">幂函数</span></div>
<p>幂函数（Power Function）里常见的两个扩展是负指数（Negative Exponent）与分数指数（Rational Exponent）。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">公式</td>
<td style="text-align: center;">条件/备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>负指数（Negative Exponent）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^{-n}=\frac{1}{a^n}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a\ne 0\)</span>；<span displaypfx="inline-" class="mathjax-container">\(n\)</span> 为正整数</td>
</tr>
<tr>
<td>分数指数（Rational Exponent）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^{p/q}=\sqrt[q]{a^p}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(q>0,\gcd(p,q)=1\)</span>；实数域通常要求 <span displaypfx="inline-" class="mathjax-container">\(a>0\)</span></td>
</tr>
<tr>
<td>例</td>
<td><span displaypfx="inline-" class="mathjax-container">\(2^{-3}=\frac{1}{8}\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(9^{1/2}=3\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(8^{2/3}=4\)</span></td>
<td>偶次根要求被开方数非负</td>
</tr>
</tbody>
</table>
<p>常用取值：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">表达式</td>
<td style="text-align: center;">值</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(2^{-3}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(=\frac{1}{8}=0.125\)</span></td>
<td>负指数转倒数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(10^{-3}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(=0.001\)</span></td>
<td>毫（<span displaypfx="inline-" class="mathjax-container">\(10^{-3}\)</span>）尺度</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(2^{1/2}=\sqrt{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\approx 1.4142\)</span></td>
<td>最常见的无理数根</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(2^{-1/2}=\frac{1}{\sqrt{2}}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\approx 0.7071\)</span></td>
<td>正交归一化、幅度缩放常用</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(10^{1/2}=\sqrt{10}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\approx 3.1623\)</span></td>
<td>对数刻度下的“半个数量级”</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(10^{-1/2}=\frac{1}{\sqrt{10}}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\approx 0.3162\)</span></td>
<td>与上式互为倒数</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">三角函数</span></div>
<div class="blog_h3"><span class="graybg">基本三角恒等式</span></div>
<p>三角函数（Trigonometric Functions）可以用单位圆（Unit Circle）定义：在圆上角度为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的点坐标是 <span displaypfx="inline-" class="mathjax-container">\((\cos\theta,\sin\theta)\)</span>。由此得到最基本恒等式：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类别</td>
<td style="text-align: center; width: 50%;">公式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>基本恒等式</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin^2\theta+\cos^2\theta=1\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\tan\theta=\frac{\sin\theta}{\cos\theta}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\cos\theta\ne 0\)</span> 时定义 <span displaypfx="inline-" class="mathjax-container">\(\tan\theta\)</span></td>
</tr>
<tr>
<td>与 <span displaypfx="inline-" class="mathjax-container">\(\tan,\cot\)</span> 相关</td>
<td><span displaypfx="inline-" class="mathjax-container">\(1+\tan^2\theta=\sec^2\theta\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(1+\cot^2\theta=\csc^2\theta\)</span></td>
<td>定义域同 <span displaypfx="inline-" class="mathjax-container">\(\tan,\cot\)</span></td>
</tr>
<tr>
<td>和差公式（Angle Addition）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin(\alpha\pm\beta)=\sin\alpha\cos\beta\pm\cos\alpha\sin\beta\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\cos(\alpha\pm\beta)=\cos\alpha\cos\beta\mp\sin\alpha\sin\beta\)</span></td>
<td>傅里叶分析、RoPE 等直觉常用</td>
</tr>
<tr>
<td>二倍角（Double-Angle）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin 2\theta=2\sin\theta\cos\theta\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\cos 2\theta=\cos^2\theta-\sin^2\theta=1-2\sin^2\theta=2\cos^2\theta-1\)</span></td>
<td>同一恒等式的不同等价写法</td>
</tr>
<tr>
<td>周期性（Periodicity）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin(\theta+2\pi)=\sin\theta\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\cos(\theta+2\pi)=\cos\theta\)</span></td>
<td>一个周期（Period）为 <span displaypfx="inline-" class="mathjax-container">\(2\pi\)</span></td>
</tr>
<tr>
<td>常用极限（<span displaypfx="inline-" class="mathjax-container">\(x\to 0\)</span>）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\lim_{x\to 0}\frac{\sin x}{x}=1\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\lim_{x\to 0}\frac{\tan x}{x}=1\)</span><br /><span displaypfx="inline-" class="mathjax-container">\(\lim_{x\to 0}\frac{1-\cos x}{x^2}=\frac{1}{2}\)</span></td>
<td>推导导数与近似时高频出现</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">常用特殊角（Special Angles）</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>（弧度）</td>
<td style="text-align: center;">角度制</td>
<td style="text-align: center;"><span displaypfx="inline-" class="mathjax-container">\(\sin\theta\)</span></td>
<td style="text-align: center;"><span displaypfx="inline-" class="mathjax-container">\(\cos\theta\)</span></td>
<td style="text-align: center;"><span displaypfx="inline-" class="mathjax-container">\(\tan\theta\)</span></td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td>0°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\pi/6\)</span></td>
<td>30°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{\sqrt{3}}{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{\sqrt{3}}\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\pi/4\)</span></td>
<td>45°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{\sqrt{2}}{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{\sqrt{2}}{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(1\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\pi/3\)</span></td>
<td>60°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{\sqrt{3}}{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sqrt{3}\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\pi/2\)</span></td>
<td>90°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td>未定义（undefined）</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\pi\)</span></td>
<td>180°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(-1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(3\pi/2\)</span></td>
<td>270°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(-1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td>未定义（undefined）</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(2\pi\)</span></td>
<td>360°</td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(1\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">欧拉公式</span></div>
<p>欧拉公式（Euler's Formula）把指数与三角函数在复数域（Complex Domain）里统一起来：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">结论</td>
<td style="text-align: center;">公式</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td>欧拉公式（Euler's Formula）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{i\theta}=\cos\theta+i\sin\theta\)</span></td>
<td>把“旋转”写成复指数</td>
</tr>
<tr>
<td>辐角相加对应指数相乘</td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{i\theta_1}e^{i\theta_2}=e^{i(\theta_1+\theta_2)}\)</span></td>
<td>复数乘法：模相乘、辐角相加</td>
</tr>
<tr>
<td>欧拉恒等式（Euler's Identity）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^{i\pi}+1=0\)</span></td>
<td>连接 <span displaypfx="inline-" class="mathjax-container">\(e,i,\pi,1,0\)</span></td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">基础函数图像</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/image-of-common-fns.jpg"><img class="alignnone size-full wp-image-40557" src="https://blog.gmem.cc/wp-content/uploads/2026/03/image-of-common-fns.jpg" alt="image-of-common-fns" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">复数</span></div>
<p>复数（Complex Number）是对实数系（Real Number System）的扩展，写作 <span displaypfx="inline-" class="mathjax-container">\(a+bi\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(a,b\in\mathbb{R}\)</span>，虚数单位（Imaginary Unit）满足 <span displaypfx="inline-" class="mathjax-container">\(i^2=-1\)</span>。几何上，复数可以表示为复平面（Complex Plane）上的点 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span>；但更重要的是，它在二维平面上提供了一套封闭、可逆且与乘法兼容的代数结构。</p>
<p>也正因为如此，复数不能简单等同于 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^2\)</span> 里的二维向量。表面上看，二维向量 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 与复数 <span displaypfx="inline-" class="mathjax-container">\(a+bi\)</span> 确实对应同一个平面点；但它们的<span style="background-color: #c0c0c0;">代数结构</span>完全不同，关键差别在于“乘法”是否自然、闭合且可逆。</p>
<p>对二维向量而言，加法非常自然，但乘法并不形成一个像实数那样稳定的数系：点乘（Inner Product）会把两个向量变成标量，叉乘（Cross Product）又会把结果带到垂直方向；因此二维向量空间本身并没有一个同时兼顾<span style="background-color: #c0c0c0;">封闭性（Closure）</span>与<span style="background-color: #c0c0c0;">可除性</span>的内建乘法。复数则不同：两个复数相乘后仍是复数，非零复数还总能做除法。这使得复平面不只是“几何上的二维平面”，而是一个可自由做加减乘除的完整数系。</p>
<p>这带来两个二维向量本身不具备的优势。第一，复数把二维旋转直接写进了乘法：若 <span displaypfx="inline-" class="mathjax-container">\(z=re^{i\theta}\)</span>，则乘以另一个复数时会自动实现“模相乘、角相加”。换句话说，<span style="background-color: #c0c0c0;">复数乘法天然就是缩放 + 旋转</span>；而若只用二维向量，通常还需要额外引入旋转矩阵。第二，复数让多项式方程的可解性闭合：例如 <span displaypfx="inline-" class="mathjax-container">\(x^2+1=0\)</span> 在实数域无解，但在复数域有解 <span displaypfx="inline-" class="mathjax-container">\(\pm i\)</span>。更深一层地，代数基本定理（Fundamental Theorem of Algebra）说明任意非常数多项式在复数域里都有根，因此复数成为代数方程的自然终点。</p>
<p>因此，二维向量更像是在描述“箭头、位移、速度、受力”的线性对象；复数则是在同一个平面上额外安装了一套兼容乘法、旋转与方程求解的代数机制。对于信号处理（Signal Processing）、交流电分析、傅里叶变换（Fourier Transform）、量子力学以及很多 AI 中的频域方法，复数都不是“二维向量的重复发明”，而是一个更强的二维代数系统。</p>
<div class="blog_h3"><span class="graybg">复数的表示：直角坐标（Rectangular Form）</span></div>
<p>复数（Complex Number）写作 <span displaypfx="inline-" class="mathjax-container">\(z=a+bi\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(a,b\in\mathbb{R}\)</span>，虚数单位（Imaginary Unit）满足 <span displaypfx="inline-" class="mathjax-container">\(i^2=-1\)</span>。把 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 视为二维平面上的点 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span>，就得到直角坐标（Rectangular Form）。</p>
<div class="blog_h3"><span class="graybg">复数的表示：极坐标（Polar Form）</span></div>
<p>同一个点也可用极坐标（Polar Form）表示：令 <span displaypfx="inline-" class="mathjax-container">\(r=|z|\)</span> 为模（Modulus），<span displaypfx="inline-" class="mathjax-container">\(\theta=\arg(z)\)</span> 为辐角（Argument），则</p>
<span displaypfx="" class="mathjax-container">\[z=r(\cos\theta+i\sin\theta)=re^{i\theta}\]</span>
<p>两种坐标之间的转换：</p>
<span displaypfx="" class="mathjax-container">\[r=\sqrt{a^2+b^2},\quad a=r\cos\theta,\quad b=r\sin\theta\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 通常用 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{atan2}(b,a)\)</span> 计算，并且 <span displaypfx="inline-" class="mathjax-container">\(\arg(z)\)</span> 不是唯一的：加上任意 <span displaypfx="inline-" class="mathjax-container">\(2\pi k\)</span>（<span displaypfx="inline-" class="mathjax-container">\(k\in\mathbb{Z}\)</span>）表示同一个方向。</p>
<p>例：<span displaypfx="inline-" class="mathjax-container">\(z=1+i\)</span> 的模是 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{2}\)</span>，辐角是 <span displaypfx="inline-" class="mathjax-container">\(\pi/4\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(z=\sqrt{2}\,e^{i\pi/4}\)</span>。</p>
<p>棣莫弗公式（De Moivre's Formula）给出幂运算的快捷形式：</p>
<span displaypfx="" class="mathjax-container">\[(\cos\theta+i\sin\theta)^n=\cos(n\theta)+i\sin(n\theta)\]</span>
<div class="blog_h3"><span class="graybg">共轭与模</span></div>
<p>复数 <span displaypfx="inline-" class="mathjax-container">\(z = a + bi\)</span> 的模（Modulus）定义为 <span displaypfx="inline-" class="mathjax-container">\(|z|=\sqrt{a^2+b^2}\)</span>，表示复平面（Complex Plane）中点 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 到原点的欧氏距离（Euclidean Distance）。</p>
<p>共轭（Conjugate）记作 <span displaypfx="inline-" class="mathjax-container">\(\bar z = a-bi\)</span>。几何上，它把点 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 关于实轴（Real Axis）镜像到 <span displaypfx="inline-" class="mathjax-container">\((a,-b)\)</span>；数值上，它把“相位（Phase）”取反而保持“幅值（Magnitude）”不变。</p>
<p>共轭与模的核心关系是 <span displaypfx="inline-" class="mathjax-container">\(z\bar z = |z|^2\)</span>，展开即可验证：</p>
<span displaypfx="" class="mathjax-container">\[(a+bi)(a-bi)=a^2+b^2=|z|^2\]</span>
<p>这个恒等式的一个直接用途是复数除法：为了避免分母含有虚部，把分母乘以共轭进行“有理化（Rationalization）”。</p>
<span displaypfx="" class="mathjax-container">\[\frac{a+bi}{c+di}=\frac{(a+bi)(c-di)}{(c+di)(c-di)}=\frac{(a+bi)(c-di)}{c^2+d^2}\]</span>
<p>例： <span displaypfx="inline-" class="mathjax-container">\(\frac{1+2i}{3-4i}=\frac{(1+2i)(3+4i)}{3^2+4^2}=\frac{-5+10i}{25}=-\frac{1}{5}+\frac{2}{5}i\)</span>。这里分母变成实数，是因为 <span displaypfx="inline-" class="mathjax-container">\((3-4i)(3+4i)=3^2+4^2\)</span>。</p>
<div class="blog_h3"><span class="graybg">复数乘法与旋转</span></div>
<p>把复数写成极坐标（Polar Form）：<span displaypfx="inline-" class="mathjax-container">\(z=r(\cos\theta+i\sin\theta)=re^{i\theta}\)</span>。此时复数乘法的几何意义非常直接：</p>
<span displaypfx="" class="mathjax-container">\[z_1 z_2 = (r_1 e^{i\theta_1})(r_2 e^{i\theta_2}) = (r_1 r_2)e^{i(\theta_1+\theta_2)}\]</span>
<p>也就是说：<span style="background-color: #c0c0c0;">模相乘、辐角相加</span>。乘法同时完成缩放（Scaling）与旋转（Rotation）。</p>
<p>例：乘以 <span displaypfx="inline-" class="mathjax-container">\(i=e^{i\pi/2}\)</span> 会把任意复数逆时针旋转 90° 且不改变模；乘以 <span displaypfx="inline-" class="mathjax-container">\(-1=e^{i\pi}\)</span> 会旋转 180°。例如 <span displaypfx="inline-" class="mathjax-container">\((1+2i)\cdot i=-2+i\)</span>，几何上就是把点 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span> 旋到 <span displaypfx="inline-" class="mathjax-container">\((-2,1)\)</span>。</p>
<div class="blog_h2"><span class="graybg">数列与级数</span></div>
<p>求和符号（Summation Symbol）<span displaypfx="inline-" class="mathjax-container">\(\sum\)</span> 与乘积符号（Product Symbol）<span displaypfx="inline-" class="mathjax-container">\(\prod\)</span> 是数列与级数推导里最常见的两个“聚合”记号：</p>
<span displaypfx="" class="mathjax-container">\[\sum_{i=1}^{n} a_i=a_1+a_2+\cdots+a_n,\quad \prod_{i=1}^{n} a_i=a_1\cdot a_2\cdots a_n\]</span>
<p>英文里通常把 <span displaypfx="inline-" class="mathjax-container">\(\sum\)</span> 读作 “sigma” 或 “summation”，把 <span displaypfx="inline-" class="mathjax-container">\(\prod\)</span> 读作 “capital pi” 或 “product”。</p>
<div class="blog_h3"><span class="graybg">等差数列 / 等比数列</span></div>
<p>数列（Sequence）是一列按整数下标排列的数 <span displaypfx="inline-" class="mathjax-container">\(\{a_n\}_{n\ge 1}\)</span>。最常见的两类是等差数列（Arithmetic Sequence）与等比数列（Geometric Sequence）。它们都可用“递推定义 + 通项公式 + 前 n 项和”三件套描述。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类型</td>
<td style="text-align: center;">递推定义</td>
<td style="text-align: center;">通项（<span displaypfx="inline-" class="mathjax-container">\(a_n\)</span>）</td>
<td style="width: 35%; text-align: center;">前 n 项和（<span displaypfx="inline-" class="mathjax-container">\(S_n=\sum_{k=1}^{n} a_k\)</span>）</td>
</tr>
</thead>
<tbody>
<tr>
<td>等差数列（Arithmetic）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a_{n}=a_{n-1}+d\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a_n=a_1+(n-1)d\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(S_n=\frac{n}{2}(a_1+a_n)=\frac{n}{2}\left(2a_1+(n-1)d\right)\)</span></td>
</tr>
<tr>
<td>等比数列（Geometric）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(a_{n}=ra_{n-1}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a_n=a_1 r^{n-1}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(S_n=\begin{cases}\frac{a_1(1-r^n)}{1-r},&amp; r\ne 1 \\ na_1,&amp; r=1\end{cases}\)</span></td>
</tr>
</tbody>
</table>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(a_1=1,d=2\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(a_n=2n-1\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(S_n=n^2\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(a_1=1,r=\frac{1}{2}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(S_n=2\left(1-2^{-n}\right)\)</span>，并随 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 增大趋近于 2。</p>
<div class="blog_h3"><span class="graybg">无穷级数与收敛</span></div>
<p>无穷级数（Infinite Series）<span displaypfx="inline-" class="mathjax-container">\(\sum_{n=1}^{\infty} a_n\)</span> 的核心对象是部分和（Partial Sum）序列 <span displaypfx="inline-" class="mathjax-container">\(S_N=\sum_{n=1}^{N} a_n\)</span>。若极限 <span displaypfx="inline-" class="mathjax-container">\(\lim_{N\to\infty} S_N\)</span> 存在且为有限值，则级数收敛（Convergence）；否则发散（Divergence）。</p>
<p>必要条件：若 <span displaypfx="inline-" class="mathjax-container">\(\sum_{n=1}^{\infty} a_n\)</span> 收敛，则必有 <span displaypfx="inline-" class="mathjax-container">\(\lim_{n\to\infty} a_n=0\)</span>。反之不成立（例如调和级数 <span displaypfx="inline-" class="mathjax-container">\(\sum 1/n\)</span> 发散）。</p>
<p>绝对收敛（Absolute Convergence）指 <span displaypfx="inline-" class="mathjax-container">\(\sum |a_n|\)</span> 收敛；绝对收敛必推出原级数收敛。仅 <span displaypfx="inline-" class="mathjax-container">\(\sum a_n\)</span> 收敛但 <span displaypfx="inline-" class="mathjax-container">\(\sum |a_n|\)</span> 发散则为条件收敛（Conditional Convergence），此时项的重排可能改变和（甚至导致发散）。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/infinite-series.jpg"><img class="alignnone size-full wp-image-40597" src="https://blog.gmem.cc/wp-content/uploads/2026/03/infinite-series.jpg" alt="infinite-series" width="100%" /></a></p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">判别法</td>
<td style="text-align: center;">条件/计算量</td>
<td style="text-align: center;">结论</td>
</tr>
</thead>
<tbody>
<tr>
<td>几何级数（Geometric Series）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sum_{n=0}^{\infty} ar^n\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(|r|<1[/latex] 时收敛，且和为 [latex syntax=inline]\frac{a}{1-r}[/latex]；[latex syntax=inline]|r|\ge 1[/latex] 时发散</td>
</tr>
<tr>
<td>p-级数（p-series）</td>
<td>[latex syntax=inline]\sum_{n=1}^{\infty}\frac{1}{n^p}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(p>1\)</span> 收敛；<span displaypfx="inline-" class="mathjax-container">\(p\le 1\)</span> 发散（调和级数为 <span displaypfx="inline-" class="mathjax-container">\(p=1\)</span>）</td>
</tr>
<tr>
<td>比较判别（Comparison）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\le a_n\le b_n\)</span>（充分大时）</td>
<td>若 <span displaypfx="inline-" class="mathjax-container">\(\sum b_n\)</span> 收敛，则 <span displaypfx="inline-" class="mathjax-container">\(\sum a_n\)</span> 收敛；若 <span displaypfx="inline-" class="mathjax-container">\(\sum a_n\)</span> 发散，则 <span displaypfx="inline-" class="mathjax-container">\(\sum b_n\)</span> 发散</td>
</tr>
<tr>
<td>比值判别（Ratio Test）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(L=\limsup_{n\to\infty}\left|\frac{a_{n+1}}{a_n}\right|\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(L<1[/latex] 绝对收敛；[latex syntax=inline]L>1\)</span>（或无穷大）发散；<span displaypfx="inline-" class="mathjax-container">\(L=1\)</span> 不定</td>
</tr>
<tr>
<td>根值判别（Root Test）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\rho=\limsup_{n\to\infty}\sqrt[n]{|a_n|}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\rho<1[/latex] 绝对收敛；[latex syntax=inline]\rho>1\)</span> 发散；<span displaypfx="inline-" class="mathjax-container">\(\rho=1\)</span> 不定</td>
</tr>
<tr>
<td>交错级数（Alternating Series）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sum (-1)^{n-1}b_n\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(b_n\downarrow 0\)</span></td>
<td>收敛；截断误差满足 <span displaypfx="inline-" class="mathjax-container">\(|S-S_N|\le b_{N+1}\)</span></td>
</tr>
</tbody>
</table>
<div class="blog_h4"><span class="graybg">调和级数与调和数（Harmonic Series / Harmonic Numbers）</span></div>
<p>调和级数（Harmonic Series）是 <span displaypfx="inline-" class="mathjax-container">\(\sum_{n=1}^{\infty}\frac{1}{n}\)</span>。它是最经典的“项趋于 0 但级数仍发散”的例子：虽然 <span displaypfx="inline-" class="mathjax-container">\(\lim_{n\to\infty}\frac{1}{n}=0\)</span>，但部分和会无界增长。</p>
<p>其部分和称为调和数（Harmonic Number）：</p>
<span displaypfx="" class="mathjax-container">\[H_n=\sum_{k=1}^{n}\frac{1}{k}\]</span>
<p>调和数的渐近行为与对数紧密相关：<span displaypfx="inline-" class="mathjax-container">\(H_n=\log n+\gamma+o(1)\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\gamma\)</span> 是欧拉-马歇罗尼常数（Euler–Mascheroni constant）。因此很多“累积量随步数缓慢增长”的分析最终都会出现 <span displaypfx="inline-" class="mathjax-container">\(\log n\)</span>。</p>
<p>在 AI/优化里，调和级数最常见的用途是解释与验证学习率（Learning Rate）衰减的收敛条件。经典随机逼近（Stochastic Approximation）的一个常用充分条件是：</p>
<span displaypfx="" class="mathjax-container">\[\sum_{t=1}^{\infty}\eta_t=\infty,\quad \sum_{t=1}^{\infty}\eta_t^2<\infty[/latex]
<p>取 [latex syntax=inline]\eta_t=\frac{1}{t}\]</span> 时，第一项对应调和级数发散（保证“走得足够远”），第二项对应 <span displaypfx="inline-" class="mathjax-container">\(\sum 1/t^2\)</span> 收敛（保证噪声的累计影响有限）。很多 SGD（Stochastic Gradient Descent）及在线学习（Online Learning）的理论推导，会用这组“一个发散、一个收敛”的对比来控制偏差与方差项。</p>
<div class="blog_h3"><span class="graybg">Taylor 展开</span></div>
<p>Taylor 展开（Taylor Expansion）用多项式在局部逼近光滑函数。Taylor 定理（Taylor's Theorem）强调“有限阶近似 + 余项（Remainder）”，Taylor 级数（Taylor Series）强调“无限项级数在收敛时等于原函数”。</p>
<span displaypfx="" class="mathjax-container">\[f(x)=\sum_{k=0}^{n}\frac{f^{(k)}(a)}{k!}(x-a)^k+R_{n+1}(x)\]</span>
<p>当余项在 <span displaypfx="inline-" class="mathjax-container">\(n\to\infty\)</span> 时收敛到 0，才有 <span displaypfx="inline-" class="mathjax-container">\(f(x)=\sum_{k=0}^{\infty}\frac{f^{(k)}(a)}{k!}(x-a)^k\)</span>。并非所有光滑函数都等于其 Taylor 级数。</p>
<p>例：在 <span displaypfx="inline-" class="mathjax-container">\(a=0\)</span> 展开，<span displaypfx="inline-" class="mathjax-container">\(e^x\approx 1+x+\frac{x^2}{2}\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(|x|\)</span> 很小时，用低阶项就能得到高精度近似。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/taylor-expansion.jpg"><img class="alignnone size-full wp-image-40583" src="https://blog.gmem.cc/wp-content/uploads/2026/03/taylor-expansion.jpg" alt="taylor-expansion" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">组合数学</span></div>
<p>组合数学（Combinatorics）研究离散结构的计数、构造与存在性。它的典型问题是“有多少种可能”：当选择空间巨大时，计数结果直接决定搜索/采样的复杂度；当把计数结果归一化为概率时，就得到二项分布、超几何分布等常见模型。</p>
<div class="blog_h3"><span class="graybg">排列与组合</span></div>
<p>排列（Permutation）与组合（Combination）的区别只在一个点：<span style="background-color: #c0c0c0;">是否区分顺序</span>。把“先选哪些元素”与“再怎么排序”分开理解，可以避免大量记忆负担。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">对象</td>
<td style="text-align: center;">记号</td>
<td style="width: 30%; text-align: center;">公式</td>
<td style="text-align: center;">直觉</td>
</tr>
</thead>
<tbody>
<tr>
<td>阶乘（Factorial）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(n!\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(n!=n(n-1)\cdots 2\cdot 1,\ 0!=1\)</span></td>
<td>把 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个互异元素全部排列的方式数</td>
</tr>
<tr>
<td>排列（k-permutation）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(P(n,k)\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(P(n,k)=\frac{n!}{(n-k)!}\)</span></td>
<td>从 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个里选 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个并排序：一步一步填位置</td>
</tr>
<tr>
<td>组合（k-combination）</td>
<td><span displaypfx="inline-" class="mathjax-container">\({n \choose k}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\({n \choose k}=\frac{n!}{k!(n-k)!}\)</span></td>
<td>从 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个里选 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个，不关心顺序：先选集合</td>
</tr>
<tr>
<td>排列与组合关系</td>
<td><span displaypfx="inline-" class="mathjax-container">\(P(n,k)\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(P(n,k)={n \choose k}\,k!\)</span></td>
<td>先选 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个元素，再把它们排序</td>
</tr>
</tbody>
</table>
<p>例：从 5 个候选特征里挑 3 个做一个无序子集，有 <span displaypfx="inline-" class="mathjax-container">\({5 \choose 3}=10\)</span> 种；如果还要给这 3 个特征排“处理顺序”，则变为排列 <span displaypfx="inline-" class="mathjax-container">\(P(5,3)=5\cdot4\cdot3=60\)</span> 种。</p>
<div class="blog_h3"><span class="graybg">二项式定理</span></div>
<p>二项式定理（Binomial Theorem）描述 <span displaypfx="inline-" class="mathjax-container">\((a+b)^n\)</span> 的展开结构：</p>
<span displaypfx="" class="mathjax-container">\[(a+b)^n=\sum_{k=0}^{n}{n \choose k}a^{n-k}b^k\]</span>
<p>系数 <span displaypfx="inline-" class="mathjax-container">\({n \choose k}\)</span> 的组合解释非常直接：把 <span displaypfx="inline-" class="mathjax-container">\((a+b)^n\)</span> 看作 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个括号相乘，每一项由“从每个括号里选 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>”组成。若最终选了 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 次 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>，就需要从 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个位置里挑出这 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 个位置，因此系数是 <span displaypfx="inline-" class="mathjax-container">\({n \choose k}\)</span>。</p>
<p>例：<span displaypfx="inline-" class="mathjax-container">\((x+2)^4={4 \choose 0}x^4+{4 \choose 1}x^3\cdot2+{4 \choose 2}x^2\cdot2^2+{4 \choose 3}x\cdot2^3+{4 \choose 4}2^4=x^4+8x^3+24x^2+32x+16\)</span>。</p>
<p>二项式系数还满足递推（Pascal's Rule）：<span displaypfx="inline-" class="mathjax-container">\({n \choose k}={n-1 \choose k-1}+{n-1 \choose k}\)</span>。这等价于“选与不选某个固定元素”的分类讨论。</p>
<p>与概率的连接：若独立伯努利试验（Bernoulli Trial）成功概率为 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，做 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 次恰好成功 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 次的概率是 <span displaypfx="inline-" class="mathjax-container">\({n \choose k}p^k(1-p)^{n-k}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\({n \choose k}\)</span> 计数的是“成功出现在哪些轮次”。</p>
<div class="blog_h2"><span class="graybg">不等式</span></div>
<p>不等式（Inequality）是把“难算/难优化/难比较”的表达式替换为“可控的上界或下界”的工具。机器学习中的许多目标函数与泛化分析，本质上都在用不等式把复杂量压到可操作的形式（例如把非线性函数用线性或二次上界近似）。</p>
<div class="blog_h3"><span class="graybg">基本不等式</span></div>
<p>基本不等式（Basic Inequalities）常见来源有两类：一类来自非负量（例如 <span displaypfx="inline-" class="mathjax-container">\((\cdot)^2\ge 0\)</span>），另一类来自范数（Norm）与凸性（Convexity）的结构性质。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">不等式</td>
<td style="text-align: center;">形式</td>
<td style="text-align: center;">典型用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>平方非负</td>
<td><span displaypfx="inline-" class="mathjax-container">\((a-b)^2\ge 0\Rightarrow a^2+b^2\ge 2ab\)</span></td>
<td>把乘积项 <span displaypfx="inline-" class="mathjax-container">\(ab\)</span> 转成平方项，便于求界或优化（常见于“配方”）</td>
</tr>
<tr>
<td>三角不等式（Triangle Inequality）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(|x+y|\le |x|+|y|\)</span></td>
<td>把“相加后的绝对值”上界成“绝对值之和”；用于误差传播与残差界</td>
</tr>
<tr>
<td>均值不等式（AM-GM）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{a+b}{2}\ge \sqrt{ab}\quad (a,b\ge 0)\)</span></td>
<td>在“和固定时乘积最大”或“乘积/几何平均”相关问题里给出最紧的经典界</td>
</tr>
</tbody>
</table>
<p>例（AM-GM 的“最大乘积”直觉）：在 <span displaypfx="inline-" class="mathjax-container">\(a,b\ge 0\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(a+b=10\)</span> 的约束下，乘积满足 <span displaypfx="inline-" class="mathjax-container">\(ab\le \left(\frac{a+b}{2}\right)^2=25\)</span>，并且当且仅当 <span displaypfx="inline-" class="mathjax-container">\(a=b=5\)</span> 取等号。</p>
<div class="blog_h3"><span class="graybg">Jensen 不等式</span></div>
<p>Jensen 不等式（Jensen's Inequality）回答一个很具体的问题：<span style="background-color: #c0c0c0;">“平均”与“非线性”交换顺序会发生什么</span>。它是把抽象的凸性（Convexity）变成可用结论的最常用工具之一。</p>
<p>先把术语说清楚：</p>
<ul>
<li>加权平均（Weighted Average）：给一组数 <span displaypfx="inline-" class="mathjax-container">\(x_1,\dots,x_n\)</span> 分配权重 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i\ge 0\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\sum_i\lambda_i=1\)</span>，则加权平均是 <span displaypfx="inline-" class="mathjax-container">\(\sum_i\lambda_i x_i\)</span>（把 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i\)</span> 视为“占比”即可）。</li>
<li>凸函数（Convex Function）：函数 <span displaypfx="inline-" class="mathjax-container">\(\varphi\)</span> 若满足对任意 <span displaypfx="inline-" class="mathjax-container">\(x_1,x_2\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\lambda\in[0,1]\)</span> 都有 <span displaypfx="inline-" class="mathjax-container">\(\varphi(\lambda x_1+(1-\lambda)x_2)\le \lambda\varphi(x_1)+(1-\lambda)\varphi(x_2)\)</span>，则称 <span displaypfx="inline-" class="mathjax-container">\(\varphi\)</span> 是凸的。直观上：它“向上弯”，因此对它做平均会被“惩罚”。</li>
<li>随机变量（Random Variable）：一个在不同试验/样本上会取不同值的量；把 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 视为“每次取到的数”。</li>
<li>期望（Expectation）<span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[\cdot]\)</span>：可把它理解为“按概率加权的平均”。</li>
</ul>
<p>Jensen 的形式（离散加权平均）：若 <span displaypfx="inline-" class="mathjax-container">\(\varphi\)</span> 凸，则</p>
<span displaypfx="" class="mathjax-container">\[\varphi\!\left(\sum_i \lambda_i x_i\right)\le \sum_i \lambda_i \varphi(x_i)\]</span>
<p>等价的期望形式：对随机变量 <span displaypfx="inline-" class="mathjax-container">\(X\)</span>（且 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[\varphi(X)]\)</span> 存在），有</p>
<span displaypfx="" class="mathjax-container">\[\varphi(\mathbb{E}[X])\le \mathbb{E}[\varphi(X)]\]</span>
<p>如果 <span displaypfx="inline-" class="mathjax-container">\(\varphi\)</span> 是凹函数（Concave Function），不等号方向会反过来。</p>
<p>场景 1：凸惩罚下，“波动”本身有代价。设某个系统的代价函数是 <span displaypfx="inline-" class="mathjax-container">\(\varphi(t)=t^2\)</span>（二次惩罚，Convex Penalty），比较两种延迟（Latency）：</p>
<ul>
<li>稳定方案：每次都是 100ms，则平均代价是 <span displaypfx="inline-" class="mathjax-container">\(100^2=10000\)</span>。</li>
<li>波动方案：一半时间 50ms、一半时间 150ms（平均同样是 100ms），则平均代价是 <span displaypfx="inline-" class="mathjax-container">\(\frac{50^2+150^2}{2}=12500\)</span>。</li>
</ul>
<p>两者平均延迟一样，但二次代价更偏好稳定方案；这就是 Jensen 在 <span displaypfx="inline-" class="mathjax-container">\(\varphi(t)=t^2\)</span> 下的直接体现：<span displaypfx="inline-" class="mathjax-container">\(\left(\frac{50+150}{2}\right)^2\le \frac{50^2+150^2}{2}\)</span>。</p>
<p>场景 2：对数损失下，“偶尔很错”会被放大。分类里常用对数损失（Log Loss）<span displaypfx="inline-" class="mathjax-container">\(\varphi(p)=-\log p\)</span>（<span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是正确类别的预测概率），它在 <span displaypfx="inline-" class="mathjax-container">\((0,1]\)</span> 上是凸函数。假设一个模型在同一个样本上两次输出 <span displaypfx="inline-" class="mathjax-container">\(p=0.9\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(p=0.1\)</span>（一次很自信、一次几乎反过来），则</p>
<span displaypfx="" class="mathjax-container">\[-\log\!\left(\frac{0.9+0.1}{2}\right)=-\log 0.5\approx 0.693\le \frac{-\log 0.9-\log 0.1}{2}\approx 1.204\]</span>
<p>含义：在凸损失下，预测的波动会提高平均损失；这也是许多“用不等式构造上界/下界”方法（例如把难优化的期望目标变成可算的界）背后的数学原因。</p>
<p>何时取等号：当所有 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 相等（没有波动），或 <span displaypfx="inline-" class="mathjax-container">\(\varphi\)</span> 在相关区间上近似线性时，不等式可取等号。</p>
<div class="blog_h3"><span class="graybg">Cauchy-Schwarz 不等式</span></div>
<p>Cauchy-Schwarz 不等式（Cauchy–Schwarz Inequality）回答另一个非常基础的问题：<span style="background-color: #c0c0c0;">两组数“对齐相乘再求和”的结果，最多能有多大</span>。它是内积（Inner Product）与范数（Norm）体系的核心约束。</p>
<p>把术语说清楚后，这个不等式就不神秘：</p>
<ul>
<li>向量（Vector）：把一组数按顺序排成列表，例如 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}=(a_1,\dots,a_n)\)</span>。</li>
<li>点积/内积（Dot Product / Inner Product）：<span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}^\top\mathbf{b}=\sum_i a_i b_i\)</span>，可理解为“两组数在同一位置上的重叠程度”。</li>
<li>L2 范数（<span displaypfx="inline-" class="mathjax-container">\(\ell_2\)</span> Norm）：<span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\|_2=\sqrt{\sum_i a_i^2}\)</span>，几何上是向量长度（Length）。</li>
</ul>
<p>不等式本身是：</p>
<span displaypfx="" class="mathjax-container">\[|\mathbf{a}^\top\mathbf{b}|\le \|\mathbf{a}\|_2\,\|\mathbf{b}\|_2\]</span>
<p>几何解释：把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}^\top\mathbf{b}\)</span> 写成 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\|_2\|\mathbf{b}\|_2\cos\theta\)</span>，Cauchy-Schwarz 等价于 <span displaypfx="inline-" class="mathjax-container">\(|\cos\theta|\le 1\)</span>。当且仅当两向量同向或反向（线性相关（Linearly Dependent））时取等号。</p>
<p>场景 1：为什么检索里常用“余弦相似度（Cosine Similarity）”。在向量检索中，常用点积 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{q}^\top\mathbf{d}\)</span> 衡量查询向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{q}\)</span> 与文档向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{d}\)</span> 的相似度。但点积同时受“方向”和“长度”影响：如果某个向量范数很大，即使方向一般，点积也可能很大。</p>
<p>一个最小例子：令查询 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{q}=(1,1)\)</span>，两篇候选文档向量为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{d}_1=(100,0)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{d}_2=(2,2)\)</span>。点积分别是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{q}^\top\mathbf{d}_1=100\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{q}^\top\mathbf{d}_2=4\)</span>，点积会更偏向 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{d}_1\)</span>。但如果把向量归一化到单位范数（Unit Norm）再比较，则</p>
<span displaypfx="" class="mathjax-container">\[\cos(\mathbf{q},\mathbf{d}_1)=\frac{\mathbf{q}^\top\mathbf{d}_1}{\|\mathbf{q}\|_2\|\mathbf{d}_1\|_2}=\frac{1}{\sqrt{2}},\quad \cos(\mathbf{q},\mathbf{d}_2)=\frac{\mathbf{q}^\top\mathbf{d}_2}{\|\mathbf{q}\|_2\|\mathbf{d}_2\|_2}=1\]</span>
<p>归一化后 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{d}_2\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{q}\)</span> 方向完全一致，更符合“语义对齐”的直觉。Cauchy-Schwarz 保证余弦相似度一定落在 [-1,1]，从而成为一个尺度稳定、可解释的相似度。</p>
<p>场景 2：为什么相关系数（Correlation Coefficient）不可能超过 1。把两组已中心化（Centered，均值为 0）的数据序列看成向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x},\mathbf{y}\)</span>，它们的“协方差方向”可以写成点积 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top\mathbf{y}\)</span>，而各自的尺度由 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_2,\|\mathbf{y}\|_2\)</span> 给出。Cauchy-Schwarz 直接推出</p>
<span displaypfx="" class="mathjax-container">\[\left|\frac{\mathbf{x}^\top\mathbf{y}}{\|\mathbf{x}\|_2\|\mathbf{y}\|_2}\right|\le 1\]</span>
<p>这就是“线性相关强度最多 100%”的数学原因；取等号对应完全线性关系，即 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}=c\mathbf{x}\)</span>。</p>
<div class="blog_h2"><span class="graybg">集合论基础</span></div>
<p>集合论（Set Theory）是现代数学语言的底座：几乎所有“对象 + 结构”的定义都能归结为集合及其上的运算。工程语境里，把对象抽象为集合的好处是：边界清晰、可组合、可用代数规则推导。</p>
<div class="blog_h3"><span class="graybg">集合运算（并 / 交 / 补）</span></div>
<p>集合（Set）是元素（Element）的无序聚合。记 <span displaypfx="inline-" class="mathjax-container">\(x\in A\)</span> 表示 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 属于集合 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，记 <span displaypfx="inline-" class="mathjax-container">\(A\subseteq B\)</span> 表示子集（Subset）。常用的运算如下。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">运算</td>
<td style="text-align: center; width: 150px;">记号</td>
<td style="text-align: center;">定义</td>
<td style="text-align: center;">例子</td>
</tr>
</thead>
<tbody>
<tr>
<td>并（Union）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(A\cup B\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\{x\mid x\in A\ \text{或}\ x\in B\}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\{1,2,3\}\cup\{3,4\}=\{1,2,3,4\}\)</span></td>
</tr>
<tr>
<td>交（Intersection）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(A\cap B\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\{x\mid x\in A\ \text{且}\ x\in B\}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\{1,2,3\}\cap\{3,4\}=\{3\}\)</span></td>
</tr>
<tr>
<td>差（Difference）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(A\setminus B\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\{x\mid x\in A,\ x\notin B\}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\{1,2,3\}\setminus\{3,4\}=\{1,2\}\)</span></td>
</tr>
<tr>
<td>补（Complement）</td>
<td><span displaypfx="inline-" class="mathjax-container">\(A^c\)</span></td>
<td>相对于全集 <span displaypfx="inline-" class="mathjax-container">\(U\)</span>： <span displaypfx="inline-" class="mathjax-container">\(A^c=U\setminus A\)</span></td>
<td>若 <span displaypfx="inline-" class="mathjax-container">\(U=\{1,2,3,4\}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\{1,2,3\}^c=\{4\}\)</span></td>
</tr>
</tbody>
</table>
<p>De Morgan 律（De Morgan's Laws）是“补运算”与“并/交”之间的互换规则：</p>
<span displaypfx="" class="mathjax-container">\[(A\cup B)^c=A^c\cap B^c,\quad (A\cap B)^c=A^c\cup B^c\]</span>
<p>计数场景常用容斥原理（Inclusion–Exclusion）：<span displaypfx="inline-" class="mathjax-container">\(|A\cup B|=|A|+|B|-|A\cap B|\)</span>。例：一个数据集里“命中规则 A”的样本有 120 个，“命中规则 B”的有 80 个，同时命中的有 30 个，则命中至少一个规则的样本数是 <span displaypfx="inline-" class="mathjax-container">\(120+80-30=170\)</span>。</p>
<div class="blog_h3"><span class="graybg">映射与关系</span></div>
<p>映射（Mapping / Function）用来描述“从输入到输出”的确定性规则。写作 <span displaypfx="inline-" class="mathjax-container">\(f:A\to B\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 是定义域（Domain），<span displaypfx="inline-" class="mathjax-container">\(B\)</span> 是陪域（Codomain）。对 <span displaypfx="inline-" class="mathjax-container">\(x\in A\)</span>，输出写作 <span displaypfx="inline-" class="mathjax-container">\(f(x)\in B\)</span>。像集（Image / Range）为 <span displaypfx="inline-" class="mathjax-container">\(f(A)=\{f(x)\mid x\in A\}\)</span>。</p>
<p>关系（Relation）是更一般的概念：它不要求“每个输入对应唯一输出”。在集合论里，一个二元关系 <span displaypfx="inline-" class="mathjax-container">\(R\)</span> 可以看成笛卡尔积（Cartesian Product）上的子集：</p>
<span displaypfx="" class="mathjax-container">\[R\subseteq A\times B,\quad (a,b)\in R\ \text{表示}\ a\ R\ b\]</span>
<p>典型例子：</p>
<ul>
<li>等价关系（Equivalence Relation）：满足自反、对称、传递。例如定义 <span displaypfx="inline-" class="mathjax-container">\(x\sim y\Leftrightarrow x-y\)</span> 能被 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 整除（同余），它把整数划分为若干等价类（Equivalence Class）。</li>
<li>偏序（Partial Order）：满足自反、反对称、传递。例如 <span displaypfx="inline-" class="mathjax-container">\(\le\)</span> 是实数上的偏序；集合的包含关系 <span displaypfx="inline-" class="mathjax-container">\(\subseteq\)</span> 也是偏序。</li>
<li>一般关系：例如“相似度大于阈值”定义了一个关系，但它通常不具备传递性，因此不是等价关系。</li>
</ul>
<p>把关系写成矩阵/邻接矩阵（Adjacency Matrix）是常用表示：若 <span displaypfx="inline-" class="mathjax-container">\(A=B=\{1,\ldots,n\}\)</span>，定义 <span displaypfx="inline-" class="mathjax-container">\(M_{ij}=1\)</span> 当且仅当 <span displaypfx="inline-" class="mathjax-container">\((i,j)\in R\)</span>。在图论（Graph Theory）与推荐/检索（Retrieval）里，这种表示会直接进入线性代数计算。</p>
<div class="blog_h1"><span class="graybg">线性代数</span></div>
<div class="blog_h2"><span class="graybg">向量</span></div>
<div class="blog_h3"><span class="graybg">向量加减法（Vector Addition / Subtraction）</span></div>
<p>在 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^n\)</span> 中，向量加法与减法按分量（Component-wise）进行。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}=(a_1,\ldots,a_n)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}=(b_1,\ldots,b_n)\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{a}+\mathbf{b}=(a_1+b_1,\ldots,a_n+b_n),\quad \mathbf{a}-\mathbf{b}=(a_1-b_1,\ldots,a_n-b_n)\]</span>
<p>几何上，加法对应向量合成；减法 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}-\mathbf{b}\)</span> 是从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 指向 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\)</span> 的位移向量。</p>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}=(1,2)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}=(3,-1)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}+\mathbf{b}=(4,1)\)</span>，<span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}-\mathbf{b}=(-2,3)\)</span>。</p>
<p>在 AI 里，残差连接（Residual Connection）是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}+f(\mathbf{x})\)</span>；梯度下降（Gradient Descent）的更新可写成 <span displaypfx="inline-" class="mathjax-container">\(\theta\leftarrow\theta-\eta\nabla L(\theta)\)</span>。它们都直接依赖向量加减法。</p>
<div class="blog_h3"><span class="graybg">点积（Dot Product）</span></div>
<p>点积（Dot Product）把两个向量映射为标量（Scalar），常用于相似度、投影和方向一致性判断。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a},\mathbf{b}\in\mathbb{R}^n\)</span>，定义为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\cdot\mathbf{b}=\sum_{i=1}^n a_i b_i\)</span>。当 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a},\mathbf{b}\ne\mathbf{0}\)</span> 时，也可写为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\cdot\mathbf{b}=\|\mathbf{a}\|\|\mathbf{b}\|\cos\theta\)</span>。</p>
<p>点积满足交换律（Commutativity）：<span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\cdot\mathbf{b}=\mathbf{b}\cdot\mathbf{a}\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\cdot\mathbf{b}=0\)</span>，两向量正交（Orthogonal）。</p>
<p>向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 方向上的标量投影是 <span displaypfx="inline-" class="mathjax-container">\(\frac{\mathbf{a}\cdot\mathbf{b}}{\|\mathbf{b}\|}\)</span>。投影更常用的是向量形式（Vector Projection）：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{proj}_{\mathbf{b}}(\mathbf{a})=\frac{\mathbf{a}\cdot\mathbf{b}}{\|\mathbf{b}\|^2}\mathbf{b}\]</span>
<p>当把向量归一化到单位范数（Unit Norm）后，点积就等于余弦相似度（Cosine Similarity）：<span displaypfx="inline-" class="mathjax-container">\(\cos\theta=\mathbf{\hat a}\cdot\mathbf{\hat b}\)</span>，这也是检索与表示学习中常见的相似度度量。</p>
<p>单位向量（Unit Vector）是范数为 1 的向量，常用来“只表示方向”。对非零向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\)</span>，其单位向量是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{\hat a}=\frac{\mathbf{a}}{\|\mathbf{a}\|}\)</span>。</p>
<p>方向角（Direction Angles）/方向余弦（Direction Cosines）描述单位向量与各坐标轴的夹角：在三维中，若单位向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}=(u_x,u_y,u_z)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(u_x=\cos\alpha\)</span>、<span displaypfx="inline-" class="mathjax-container">\(u_y=\cos\beta\)</span>、<span displaypfx="inline-" class="mathjax-container">\(u_z=\cos\gamma\)</span>，并满足 <span displaypfx="inline-" class="mathjax-container">\(\cos^2\alpha+\cos^2\beta+\cos^2\gamma=1\)</span>。</p>
<p>点积之所以会出现“乘积”，是因为它等于“被投影长度 × 参照向量长度”： <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\cdot\mathbf{b}=\|\mathbf{b}\|\cdot \mathrm{projLen}_{\mathbf{b}}(\mathbf{a})\)</span>。当两向量同向时，投影长度就是 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\|\)</span>，于是点积变为 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\|\|\mathbf{b}\|\)</span>。</p>
<div class="blog_h4"><span class="graybg">二维向量的复数表示</span></div>
<p>在二维空间里，向量 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 可以写成复数 <span displaypfx="inline-" class="mathjax-container">\(z=a+bi\)</span>。这不是把向量“变成另一种对象”，而是给同一个二维量换一种记法：实部对应 x 轴分量，虚部对应 y 轴分量。</p>
<p>若另一向量 <span displaypfx="inline-" class="mathjax-container">\((c,d)\)</span> 写成 <span displaypfx="inline-" class="mathjax-container">\(w=c+di\)</span>，则复共轭乘积为</p>
<span displaypfx="" class="mathjax-container">\[z\bar w=(a+bi)(c-di)=(ac+bd)+i(bc-ad)\]</span>
<p>其中实部 <span displaypfx="inline-" class="mathjax-container">\(ac+bd\)</span> 恰好就是二维向量的点积：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{a}\cdot\mathbf{b}=ac+bd=\mathrm{Re}(z\bar w)\]</span>
<p>若再把它们写成极坐标形式 <span displaypfx="inline-" class="mathjax-container">\(z=r_1e^{i\theta_1}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(w=r_2e^{i\theta_2}\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{a}\cdot\mathbf{b}=\mathrm{Re}(z\bar w)=r_1r_2\cos(\theta_1-\theta_2)\]</span>
<p>这里实部的来源可以直接从指数形式读出：因为 <span displaypfx="inline-" class="mathjax-container">\(\bar w=r_2e^{-i\theta_2}\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(z\bar w=r_1r_2e^{i(\theta_1-\theta_2)}\)</span>。再用欧拉公式 <span displaypfx="inline-" class="mathjax-container">\(e^{i\phi}=\cos\phi+i\sin\phi\)</span> 展开，就得到 <span displaypfx="inline-" class="mathjax-container">\(z\bar w=r_1r_2\big(\cos(\theta_1-\theta_2)+i\sin(\theta_1-\theta_2)\big)\)</span>；其中实部正是 <span displaypfx="inline-" class="mathjax-container">\(r_1r_2\cos(\theta_1-\theta_2)\)</span>。</p>
<p>因此，点积既可以看成分量乘加，也可以看成“复数乘积取实部”。后一种写法把<span style="background-color: #c0c0c0;">长度</span>与<span style="background-color: #c0c0c0;">相位差</span>放进同一个式子里，在位置编码、旋转表示与频域分析中尤其方便。后文 RoPE 的复数视角正是沿用这层关系：二维块先写成复数，再让相位随位置旋转。</p>
<p>例：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}=(3,4)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}=(4,0)\)</span>。则 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\cdot\mathbf{b}=12\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\|=5\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{b}\|=4\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(\cos\theta=\frac{12}{20}=0.6\)</span>。而 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 方向上的标量投影是 <span displaypfx="inline-" class="mathjax-container">\(\frac{12}{4}=3\)</span>，恰好对应 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\)</span> 的 x 分量。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/vector-ops.jpg"><img class="alignnone size-full wp-image-40599" src="https://blog.gmem.cc/wp-content/uploads/2026/03/vector-ops.jpg" alt="vector-ops" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">叉积（Cross Product）</span></div>
<p>叉积（Cross Product）定义在三维空间（3D Space）。结果是同时垂直于两个输入向量的向量，大小为 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\times\mathbf{b}\|=\|\mathbf{a}\|\|\mathbf{b}\|\sin\theta\)</span>，等于两向量张成平行四边形的面积。</p>
<p>计算上，若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}=(a_1,a_2,a_3)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}=(b_1,b_2,b_3)\)</span>，则：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{a}\times\mathbf{b}=(a_2b_3-a_3b_2,\ a_3b_1-a_1b_3,\ a_1b_2-a_2b_1)\]</span>
<p>例： <span displaypfx="inline-" class="mathjax-container">\((1,0,0)\times(0,1,0)=(0,0,1)\)</span>。这个例子在几何上对应两个单位正交基向量张成的“正方形面积为 1”，方向由右手定则给出。</p>
<p>叉积为零当且仅当两向量平行（Parallel/Colinear）或至少有一个为零向量：这是因为 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\times\mathbf{b}\|=\|\mathbf{a}\|\|\mathbf{b}\|\sin\theta\)</span>，为 0 只能来自 <span displaypfx="inline-" class="mathjax-container">\(\sin\theta=0\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{a}\|=0\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{b}\|=0\)</span>。</p>
<p>方向由右手定则（Right-Hand Rule）确定：四指从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\)</span> 旋向 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span>，拇指方向即 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}\times\mathbf{b}\)</span> 方向。旋转“正负”不是绝对物理事实，而是由坐标系（Coordinate System）与观察方向（View Direction）约定决定。</p>
<p>力矩（Torque）可作为叉积方向的直观例子： <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\tau}=\mathbf{r}\times\mathbf{F}\)</span>。这里保留物理解释的唯一目的，是帮助理解叉积的方向性。</p>
<div class="blog_h2"><span class="graybg">基（Basis）</span></div>
<p>在 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^n\)</span> 中，一组向量 <span displaypfx="inline-" class="mathjax-container">\(\{\mathbf{b}_1,\ldots,\mathbf{b}_n\}\)</span> 若线性无关（Linearly Independent）且张成（Span）整个空间，则称为一组基（Basis）。任何向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 都能唯一表示为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=\sum_{i=1}^{n} c_i\mathbf{b}_i\)</span>；系数向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}=(c_1,\ldots,c_n)^\top\)</span> 就是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 在该基下的坐标（Coordinates）。</p>
<p>把基向量按列堆成矩阵 <span displaypfx="inline-" class="mathjax-container">\(B=[\mathbf{b}_1\ \cdots\ \mathbf{b}_n]\in\mathbb{R}^{n\times n}\)</span>，则坐标与原向量满足 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=B\mathbf{c}\)</span>。换基（Change of Basis）在推导里本质就是在不同 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 之间切换坐标表示。</p>
<div class="blog_h3"><span class="graybg">换基（Change of Basis）：同一个向量，不同坐标</span></div>
<p>换基（Change of Basis）指的是：在不改变几何向量本身的前提下，改用另一组基（Basis）来描述它的坐标（Coordinates）。因此，换基改变的是<span style="background-color: #c0c0c0;">表示方式</span>，不是向量对象本身。直观上，可把几何向量理解为平面/空间里的一支箭头。</p>
<p>这也是为什么“换基”不能理解成“把向量变形”。真正发生变化的是参考基（Basis），因此同一向量在不同基下的坐标数值会不同，而几何对象本身保持不变。</p>
<p>为了不混淆“向量本身”和“向量的坐标表示”，可以用一个约定把它们分开：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span>：把同一个几何向量用标准基（Standard Basis）写出来的分量列向量（数值计算里最常用的表示）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}\)</span>：把同一个几何向量用某组新基 <span displaypfx="inline-" class="mathjax-container">\(\{\mathbf{b}_i\}\)</span> 写出来的坐标向量（Coordinate Vector），也就是“在新基上要乘的系数”。</li>
</ul>
<p>把新基向量在标准基下的分量按列组成 <span displaypfx="inline-" class="mathjax-container">\(B=[\mathbf{b}_1\ \cdots\ \mathbf{b}_n]\)</span>（可称为基矩阵（Basis Matrix）），则</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}=B\mathbf{c}\]</span>
<p>读法：右边 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}\)</span> 是“在新基下的坐标”，左边 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 是“在标准基下的分量”。乘上 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 就把“新基坐标”换算回“标准基分量”。</p>
<p>反过来，如果你已知标准基下的分量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span>，想求新基坐标 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}\)</span>，就需要解线性方程组 <span displaypfx="inline-" class="mathjax-container">\(B\mathbf{c}=\mathbf{x}\)</span>。由于基向量线性无关，矩阵 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 必可逆（Invertible），因此可写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{c}=B^{-1}\mathbf{x}\]</span>
<p>更一般地，若旧基矩阵为 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>、新基矩阵为 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>，同一几何向量满足 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=B\mathbf{c}_{B}=C\mathbf{c}_{C}\)</span>，于是坐标之间的换算是：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{c}_{C}=C^{-1}B\,\mathbf{c}_{B}\]</span>
<p>矩阵 <span displaypfx="inline-" class="mathjax-container">\(P=C^{-1}B\)</span> 常被称为换基矩阵（Change-of-basis Matrix）：它把“旧基坐标”直接映射为“新基坐标”。</p>
<p>例：在 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^2\)</span> 取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}_1=(1,0)^\top,\ \mathbf{b}_2=(1,1)^\top\)</span>。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(2,3)^\top\)</span>，解 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=c_1\mathbf{b}_1+c_2\mathbf{b}_2\)</span> 得 <span displaypfx="inline-" class="mathjax-container">\(c_2=3,\ c_1=-1\)</span>，因此该基下坐标为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}=(-1,3)^\top\)</span>。同一个几何向量，在不同基下的坐标会不同。</p>
<p>把基矩阵写出来会更便于计算： <span displaypfx="inline-" class="mathjax-container">\(B=[\mathbf{b}_1\ \mathbf{b}_2]=\begin{bmatrix}1 &amp; 1\\ 0 &amp; 1\end{bmatrix}\)</span>。此时 <span displaypfx="inline-" class="mathjax-container">\(B\mathbf{c}=\begin{bmatrix}1 &amp; 1\\ 0 &amp; 1\end{bmatrix}\begin{bmatrix}-1\\ 3\end{bmatrix}=\begin{bmatrix}2\\ 3\end{bmatrix}=\mathbf{x}\)</span>；反过来，解 <span displaypfx="inline-" class="mathjax-container">\(B\mathbf{c}=\mathbf{x}\)</span> 等价于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}=B^{-1}\mathbf{x}\)</span>，因此“换基”在计算上就是解一个线性方程组。</p>
<div class="blog_h3"><span class="graybg">标准基（Standard Basis）</span></div>
<p>标准基（Standard Basis）是最常用的一组基。第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个标准基向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{e}_i\)</span> 只有第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量为 1，其余为 0：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{e}_1=(1,0,\ldots,0)^\top,\ \mathbf{e}_2=(0,1,0,\ldots,0)^\top,\ \ldots,\ \mathbf{e}_n=(0,\ldots,0,1)^\top\]</span>
<p>例：在 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^2\)</span> 中，<span displaypfx="inline-" class="mathjax-container">\((2,3)^\top=2\mathbf{e}_1+3\mathbf{e}_2\)</span>。把 <span displaypfx="inline-" class="mathjax-container">\(\{\mathbf{e}_i\}\)</span> 作为列组成矩阵就是单位矩阵 <span displaypfx="inline-" class="mathjax-container">\(I\)</span>，因此标准基下 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=I\mathbf{x}\)</span> 对应“坐标与分量一致”。</p>
<div class="blog_h3"><span class="graybg">正交基（Orthogonal Basis）</span></div>
<p>正交基（Orthogonal Basis）是指基向量两两正交：对 <span displaypfx="inline-" class="mathjax-container">\(i\ne j\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}_i^\top\mathbf{b}_j=0\)</span>。它不要求单位长度。</p>
<p>正交基的一个关键性质是：坐标可以用投影直接算出。若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=\sum_i c_i\mathbf{b}_i\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\{\mathbf{b}_i\}\)</span> 正交，则</p>
<span displaypfx="" class="mathjax-container">\[c_i=\frac{\mathbf{x}^\top\mathbf{b}_i}{\mathbf{b}_i^\top\mathbf{b}_i}\]</span>
<p>例：取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}_1=(1,1)^\top,\ \mathbf{b}_2=(1,-1)^\top\)</span>，它们点积为 0。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(2,1)^\top\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(c_1=\frac{3}{2},\ c_2=\frac{1}{2}\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=\frac{3}{2}\mathbf{b}_1+\frac{1}{2}\mathbf{b}_2\)</span>。</p>
<div class="blog_h3"><span class="graybg">正交标准基（Orthonormal Basis）</span></div>
<p>正交标准基（Orthonormal Basis）要求两两正交且每个基向量单位长度： <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{u}_i\|_2=1\)</span>，并满足 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}_i^\top\mathbf{u}_j=\delta_{ij}\)</span>（克罗内克 delta（Kronecker Delta））。</p>
<p>此时坐标就是内积/投影：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=\sum_i c_i\mathbf{u}_i\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(c_i=\mathbf{x}^\top\mathbf{u}_i\)</span>。计算上，这等价于把向量投影到各基方向。</p>
<p>例：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}_1=\frac{1}{\sqrt{2}}(1,1)^\top,\ \mathbf{u}_2=\frac{1}{\sqrt{2}}(1,-1)^\top\)</span>。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(2,1)^\top\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(c_1=\frac{3}{\sqrt{2}},\ c_2=\frac{1}{\sqrt{2}}\)</span>，于是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=c_1\mathbf{u}_1+c_2\mathbf{u}_2\)</span>。</p>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\{\mathbf{u}_i\}\)</span> 按列组成矩阵 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(Q^\top Q=I\)</span>，并且坐标变换可写成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}=Q^\top\mathbf{x}\)</span>、重构为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=Q\mathbf{c}\)</span>。在 PCA（Principal Component Analysis）与 SVD 中，主方向/奇异向量就是正交标准基；投影与重构只需要转置，不需要显式求逆。</p>
<div class="blog_h2"><span class="graybg">矩阵运算</span></div>
<p>矩阵运算（Matrix Operations）是机器学习（Machine Learning）中最核心的计算骨架。前向传播、反向传播和参数更新都可以表示为矩阵与向量的组合。</p>
<div class="blog_h3"><span class="graybg">加法与广播（Addition / Broadcasting）</span></div>
<p>矩阵加法按元素（Element-wise）进行：若 <span displaypfx="inline-" class="mathjax-container">\(X,B\in\mathbb{R}^{m\times n}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\((X+B)_{ij}=X_{ij}+B_{ij}\)</span>。减法同理。</p>
<p>在深度学习框架里常见的是广播（Broadcasting）：例如对 batch 特征 <span displaypfx="inline-" class="mathjax-container">\(X\in\mathbb{R}^{B\times d}\)</span> 与偏置 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\in\mathbb{R}^{d}\)</span>，写作 <span displaypfx="inline-" class="mathjax-container">\(Y=X+\mathbf{b}\)</span> 意味着把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 复制到每一行后再相加。这是线性层 <span displaypfx="inline-" class="mathjax-container">\(Y=XW+\mathbf{b}\)</span> 的标准形式。</p>
<div class="blog_h4"><span class="graybg">例：矩阵加法</span></div>
<span displaypfx="" class="mathjax-container">\[X=\begin{bmatrix}1 &amp; 2\\ 3 &amp; 4\end{bmatrix},\ B=\begin{bmatrix}10 &amp; 20\\ 30 &amp; 40\end{bmatrix}\Rightarrow X+B=\begin{bmatrix}11 &amp; 22\\ 33 &amp; 44\end{bmatrix}\]</span>
<div class="blog_h4"><span class="graybg">例：广播加偏置（Bias Broadcasting）</span></div>
<p>令 <span displaypfx="inline-" class="mathjax-container">\(X\in\mathbb{R}^{2\times 3}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\in\mathbb{R}^{3}\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[X=\begin{bmatrix}1 &amp; 2 &amp; 3\\ 4 &amp; 5 &amp; 6\end{bmatrix},\ \mathbf{b}=\begin{bmatrix}10 &amp; 20 &amp; 30\end{bmatrix}\]</span>
<p>广播的语义是“把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 复制到每一行”，因此</p>
<span displaypfx="" class="mathjax-container">\[Y=X+\mathbf{b}=\begin{bmatrix}11 &amp; 22 &amp; 33\\ 14 &amp; 25 &amp; 36\end{bmatrix}\]</span>
<p>从线性代数角度，把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 视作行向量，则广播等价于 <span displaypfx="inline-" class="mathjax-container">\(Y=X+\mathbf{1}\mathbf{b}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{1}\in\mathbb{R}^{B\times 1}\)</span> 是全 1 列向量。</p>
<div class="blog_h3"><span class="graybg">矩阵乘法（Matrix Multiplication）</span></div>
<p>矩阵乘法（Matrix Multiplication）在神经网络里通常对应线性层（Linear Layer）：<span displaypfx="inline-" class="mathjax-container">\(Y=XW\)</span>。从“每个输出维度是一个点积”来看更容易记：</p>
<span displaypfx="" class="mathjax-container">\[y_j=\sum_{i=1}^{d_{\text{in}}} x_i W_{ij}\]</span>
<p>在批处理（Batch）场景下，常用形状约定是 <span displaypfx="inline-" class="mathjax-container">\(X\in\mathbb{R}^{B\times d_{\text{in}}}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(W\in\mathbb{R}^{d_{\text{in}}\times d_{\text{out}}}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(Y=XW\in\mathbb{R}^{B\times d_{\text{out}}}\)</span>。矩阵乘法一般不满足交换律（Non-commutativity），形状不匹配时也无法相乘。</p>
<p>从几何（Geometry）角度看，矩阵定义了一个线性变换（Linear Transformation）：它把空间中的点/向量整体“变形”（旋转、缩放、剪切、投影等）。一种实用记法是看基向量（Basis Vectors）如何被映射：如果用列向量约定，矩阵的每一列就是某个基向量变换后的像。</p>
<p>例（二维）：标准基向量（Standard Basis Vectors）为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{e}_1=(1,0)^\top\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{e}_2=(0,1)^\top\)</span>。对</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}a &amp; c\\ b &amp; d\end{bmatrix}\]</span>
<p>有</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{e}_1=\begin{bmatrix}a\\ b\end{bmatrix},\quad A\mathbf{e}_2=\begin{bmatrix}c\\ d\end{bmatrix}\]</span>
<p>因此 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的第 1/2 列分别是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{e}_1,\mathbf{e}_2\)</span> 的像。对任意 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=x_1\mathbf{e}_1+x_2\mathbf{e}_2\)</span>，根据线性变换的线性性（Linearity）<span displaypfx="inline-" class="mathjax-container">\(A(\alpha\mathbf{u}+\beta\mathbf{v})=\alpha A\mathbf{u}+\beta A\mathbf{v}\)</span>，可得</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{x}=x_1A\mathbf{e}_1+x_2A\mathbf{e}_2=x_1\begin{bmatrix}a\\ b\end{bmatrix}+x_2\begin{bmatrix}c\\ d\end{bmatrix}\]</span>
<p>数值例子：取</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}2 &amp; 1\\ 0 &amp; 1\end{bmatrix}\]</span>
<p>则 <span displaypfx="inline-" class="mathjax-container">\(A\mathbf{e}_1=(2,0)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(A\mathbf{e}_2=(1,1)^\top\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(3,4)^\top=3\mathbf{e}_1+4\mathbf{e}_2\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{x}=3A\mathbf{e}_1+4A\mathbf{e}_2=3\begin{bmatrix}2\\ 0\end{bmatrix}+4\begin{bmatrix}1\\ 1\end{bmatrix}=\begin{bmatrix}10\\ 4\end{bmatrix}\]</span>
<p>这就是“看列向量理解变换”的核心：先画出两条基向量被送到哪里，整个网格会按相同线性组合随之平移/剪切/旋转/缩放。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/matrix-trans.jpg"><img class="alignnone size-full wp-image-40629" src="https://blog.gmem.cc/wp-content/uploads/2026/03/matrix-trans.jpg" alt="matrix-trans" width="100%" /></a></p>
<p>这对应两种等价视角：</p>
<ul>
<li>变换向量（Active View）：固定坐标轴，让向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 变成 <span displaypfx="inline-" class="mathjax-container">\(A\mathbf{x}\)</span>。</li>
<li>变换坐标轴（Passive View）：固定几何点，把坐标系按 <span displaypfx="inline-" class="mathjax-container">\(A^{-1}\)</span> 变换；同一个点在新坐标系下的坐标会变化。</li>
</ul>
<p>两种视角描述的是同一个线性映射，只是“变的对象”不同。在做特征变换/白化（Whitening）/坐标变换推导时，这个区分能避免符号混乱。</p>
<p>矩阵乘法只有在内维度相等时才有定义：<span displaypfx="inline-" class="mathjax-container">\((m\times n)(n\times p)=(m\times p)\)</span>。把向量视为列向量（<span displaypfx="inline-" class="mathjax-container">\(m\times 1\)</span>）或行向量（<span displaypfx="inline-" class="mathjax-container">\(1\times m\)</span>）后，外积与点积也都可以统一为矩阵乘法的特例。</p>
<ol>
<li>
<p>一般矩阵乘法：<span displaypfx="inline-" class="mathjax-container">\((m\times n)(n\times p)=(m\times p)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}1 &amp; 2 &amp; 3\\ 4 &amp; 5 &amp; 6\end{bmatrix}\in\mathbb{R}^{2\times 3},\quad B=\begin{bmatrix}7 &amp; 8\\ 9 &amp; 10\\ 11 &amp; 12\end{bmatrix}\in\mathbb{R}^{3\times 2}\]</span>
<span displaypfx="" class="mathjax-container">\[AB=\begin{bmatrix}58 &amp; 64\\ 139 &amp; 154\end{bmatrix}\in\mathbb{R}^{2\times 2}\]</span>
</li>
<li>
<p>外积（Outer Product）：<span displaypfx="inline-" class="mathjax-container">\((m\times 1)(1\times n)=(m\times n)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{u}=\begin{bmatrix}1\\ 2\\ 3\end{bmatrix}\in\mathbb{R}^{3\times 1},\quad \mathbf{v}=\begin{bmatrix}4 &amp; 5\end{bmatrix}\in\mathbb{R}^{1\times 2}\]</span>
<span displaypfx="" class="mathjax-container">\[\mathbf{u}\mathbf{v}=\begin{bmatrix}4 &amp; 5\\ 8 &amp; 10\\ 12 &amp; 15\end{bmatrix}\in\mathbb{R}^{3\times 2}\]</span>
<p>这是一个秩一（Rank-1）矩阵：它把“列向量 × 行向量”变成矩阵；在低秩近似、注意力权重构造与二阶统计量里都很常见。</p>
</li>
<li>
<p>点积（Dot Product）：<span displaypfx="inline-" class="mathjax-container">\((1\times m)(m\times 1)=(1\times 1)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{r}=\begin{bmatrix}1 &amp; 2 &amp; 3\end{bmatrix}\in\mathbb{R}^{1\times 3},\quad \mathbf{c}=\begin{bmatrix}4\\ 5\\ 6\end{bmatrix}\in\mathbb{R}^{3\times 1}\]</span>
<span displaypfx="" class="mathjax-container">\[\mathbf{r}\mathbf{c}=\begin{bmatrix}32\end{bmatrix}\in\mathbb{R}^{1\times 1}\equiv 32\]</span>
<p>结果是标量 <span displaypfx="inline-" class="mathjax-container">\(32\)</span>，等价于点积： <span displaypfx="inline-" class="mathjax-container">\(\mathbf{r}\mathbf{c}=\sum_{i=1}^{m} r_i c_i\)</span>。在实现里常写成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top\mathbf{y}\)</span>。</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">仿射变换与仿射子空间</span></div>
<p>仿射（Affine）是线性（Linear）的一个自然扩展。线性变换要求 <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})=A\mathbf{x}\)</span>，必须把原点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{0}\)</span> 映到原点；仿射变换则允许再加一个平移项：</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x})=A\mathbf{x}+\mathbf{b}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 是线性部分，负责旋转、缩放、剪切、投影等形变； <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}\)</span> 是平移项（Translation / Bias），负责把整个空间整体推走一个位移。因此，仿射变换可以理解为“先做线性变换，再做平移”。</p>
<p>几何上，线性变换像在原点固定不动的前提下拉伸或扭转整张网格；仿射变换则像先把网格按 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 变形，再把整张网格连同原点一起平移到别的位置。两者最核心的区别是：<span style="background-color: #c0c0c0;">线性变换保原点，仿射变换不一定保原点</span>。</p>
<p>最简单的一维例子是 <span displaypfx="inline-" class="mathjax-container">\(f(x)=2x\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(g(x)=2x+3\)</span>。前者是线性的，因为 <span displaypfx="inline-" class="mathjax-container">\(f(0)=0\)</span>；后者是仿射的，因为它先把数轴按 2 倍拉伸，再整体平移 3 个单位，所以 <span displaypfx="inline-" class="mathjax-container">\(g(0)=3\)</span>，不再经过原点。二维里也是同样： <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})=\mathbf{x}\)</span> 是恒等线性变换，而 <span displaypfx="inline-" class="mathjax-container">\(g(\mathbf{x})=\mathbf{x}+\begin{bmatrix}1\\2\end{bmatrix}\)</span> 会把整张平面网格整体向右平移 1、向上平移 2。</p>
<p>仿射函数（Affine Function）在优化与机器学习里极其常见。一维情形的 <span displaypfx="inline-" class="mathjax-container">\(f(x)=ax+b\)</span> 就是最简单的仿射函数；高维里则写成 <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})=\mathbf{w}^\top\mathbf{x}+b\)</span>。因此，神经网络里通常口头说“线性层（Linear Layer）”，但如果带偏置 <span displaypfx="inline-" class="mathjax-container">\(b\)</span>，更严格的数学名称其实是仿射层（Affine Layer）：</p>
<span displaypfx="" class="mathjax-container">\[Y=XW+\mathbf{1}b^\top\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 是输入矩阵， <span displaypfx="inline-" class="mathjax-container">\(W\)</span> 是线性变换矩阵， <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是偏置向量， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{1}\)</span> 是把偏置广播到每个样本上的全 1 列向量。若没有偏置，才是严格意义上的线性映射。</p>
<p>仿射还有一个重要性质：它保持仿射组合（Affine Combination）。若 <span displaypfx="inline-" class="mathjax-container">\(\sum_i \alpha_i=1\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[f\!\left(\sum_i \alpha_i \mathbf{x}_i\right)=\sum_i \alpha_i f(\mathbf{x}_i)\]</span>
<p>这意味着直线、平面、平行关系和凸组合结构在仿射变换下会被保留；因此很多几何对象在经过仿射变换后，仍然保持“像线还是线，像平面还是平面”的基本类型，但位置和方向可能改变。</p>
<p>仿射子空间（Affine Subspace）则是“线性子空间整体平移后得到的集合”。若 <span displaypfx="inline-" class="mathjax-container">\(U\)</span> 是一个线性子空间， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_0\)</span> 是空间中的某个固定点，则</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{A}=\mathbf{x}_0+U=\{\mathbf{x}_0+\mathbf{u}:\mathbf{u}\in U\}\]</span>
<p>就是一个仿射子空间。它和线性子空间的差别也在于是否经过原点：线性子空间必须包含原点，仿射子空间不必。例如二维中的直线 <span displaypfx="inline-" class="mathjax-container">\(x+y=1\)</span> 就是一个仿射子空间；它的方向部分与线性子空间 <span displaypfx="inline-" class="mathjax-container">\(x+y=0\)</span> 相同，但整条直线被平移开了，因此不经过原点。</p>
<p>这正是为什么超平面 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}^\top\mathbf{x}+b=0\)</span> 被称为仿射子空间而不是线性子空间：当 <span displaypfx="inline-" class="mathjax-container">\(b\ne 0\)</span> 时，它通常不经过原点，只是由某个线性超平面整体平移而来。优化里的等式约束 <span displaypfx="inline-" class="mathjax-container">\(h_j(x)=0\)</span> 若要求 <span displaypfx="inline-" class="mathjax-container">\(h_j\)</span> 是仿射函数，含义也正是“约束边界仍然保持平直结构，但允许存在偏置和平移”。</p>
<p>更进一步，双仿射（Biaffine）也是同一族概念的延伸。它通常在两个向量之间做一个双线性项 <span displaypfx="inline-" class="mathjax-container">\(h_i^\top U h_j\)</span>，再加上线性项和偏置项，因此既包含“两个变量之间的乘性交互”，也包含仿射偏置修正。理解了仿射，就能把“线性 / 仿射 / 双线性 / 双仿射”看成一条逐步加复杂度的函数族谱。</p>
<div class="blog_h4"><span class="graybg">线性到双仿射的公式族谱</span></div>
<p>把输出视为一个标量时，这条族谱可以写成一组并列的标准形式：</p>
<span displaypfx="" class="mathjax-container">\[f_{\mathrm{linear}}(\mathbf{x})=\mathbf{w}^\top \mathbf{x}\]</span>
<span displaypfx="" class="mathjax-container">\[f_{\mathrm{affine}}(\mathbf{x})=\mathbf{w}^\top \mathbf{x}+b\]</span>
<span displaypfx="" class="mathjax-container">\[s_{\mathrm{bilinear}}(\mathbf{x},\mathbf{z})=\mathbf{x}^\top U\mathbf{z}\]</span>
<span displaypfx="" class="mathjax-container">\[s_{\mathrm{biaffine}}(\mathbf{x},\mathbf{z})=\mathbf{x}^\top U\mathbf{z}+\mathbf{a}^\top\mathbf{x}+\mathbf{c}^\top\mathbf{z}+b\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\in\mathbb{R}^{m},\mathbf{z}\in\mathbb{R}^{n}\)</span> 是两个输入向量， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},\mathbf{a}\in\mathbb{R}^{m}\)</span>， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}\in\mathbb{R}^{n}\)</span>， <span displaypfx="inline-" class="mathjax-container">\(U\in\mathbb{R}^{m\times n}\)</span>， <span displaypfx="inline-" class="mathjax-container">\(b\in\mathbb{R}\)</span>。这里的 <span displaypfx="inline-" class="mathjax-container">\(U\)</span> 是双线性项的参数矩阵（Parameter Matrix）：它不作用于单个向量本身，而是给“ <span displaypfx="inline-" class="mathjax-container">\(x_p\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(z_q\)</span> 同时出现”这类二元交互分配权重。把式子展开后可写成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top U\mathbf{z}=\sum_{p=1}^{m}\sum_{q=1}^{n}x_p\,U_{pq}\,z_q\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(U_{pq}\)</span> 直接控制第 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 个 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 特征与第 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 个 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 特征之间的交互强度。 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a}^\top\mathbf{x}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{c}^\top\mathbf{z}\)</span> 是单边线性项，分别描述各自独立的角色偏好； <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是全局基线偏置。</p>
<p>把这个定义写成一个最小的 2×3 例子，会更容易直接看出“连接强度”来自哪里。令</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}=\begin{bmatrix}x_1\\x_2\end{bmatrix},\quad \mathbf{z}=\begin{bmatrix}z_1\\z_2\\z_3\end{bmatrix},\quad U=\begin{bmatrix}u_{11} &amp; u_{12} &amp; u_{13}\\u_{21} &amp; u_{22} &amp; u_{23}\end{bmatrix}\]</span>
<p>则双线性项为</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top U\mathbf{z}=\begin{bmatrix}x_1 &amp; x_2\end{bmatrix}\begin{bmatrix}u_{11} &amp; u_{12} &amp; u_{13}\\u_{21} &amp; u_{22} &amp; u_{23}\end{bmatrix}\begin{bmatrix}z_1\\z_2\\z_3\end{bmatrix}\]</span>
<span displaypfx="" class="mathjax-container">\[=x_1u_{11}z_1+x_1u_{12}z_2+x_1u_{13}z_3+x_2u_{21}z_1+x_2u_{22}z_2+x_2u_{23}z_3\]</span>
<p>这一步把抽象的矩阵乘法拆成了六条显式连接： <span displaypfx="inline-" class="mathjax-container">\(u_{11}\)</span> 控制 <span displaypfx="inline-" class="mathjax-container">\(x_1\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(z_1\)</span> 的连接强度， <span displaypfx="inline-" class="mathjax-container">\(u_{23}\)</span> 控制 <span displaypfx="inline-" class="mathjax-container">\(x_2\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(z_3\)</span> 的连接强度。若某个 <span displaypfx="inline-" class="mathjax-container">\(u_{pq}\)</span> 很大且为正，只要对应的 <span displaypfx="inline-" class="mathjax-container">\(x_p\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(z_q\)</span> 同时取大值，这一对特征就会显著抬高总分；若某个 <span displaypfx="inline-" class="mathjax-container">\(u_{pq}\)</span> 为负，则说明这对特征的共同出现会压低分数。</p>
<p>在同一个例子里，双仿射只是在双线性项之外再加上单边项与偏置：</p>
<span displaypfx="" class="mathjax-container">\[s_{\mathrm{biaffine}}(\mathbf{x},\mathbf{z})=\mathbf{x}^\top U\mathbf{z}+\mathbf{a}^\top\mathbf{x}+\mathbf{c}^\top\mathbf{z}+b\]</span>
<span displaypfx="" class="mathjax-container">\[=\mathbf{x}^\top U\mathbf{z}+(a_1x_1+a_2x_2)+(c_1z_1+c_2z_2+c_3z_3)+b\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(a_1,a_2\)</span> 描述 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 一侧各特征自身的偏好， <span displaypfx="inline-" class="mathjax-container">\(c_1,c_2,c_3\)</span> 描述 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 一侧各特征自身的偏好， <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 给出无条件基线分数。于是，双线性回答的是“这两组特征配在一起有多合适”，双仿射回答的则是“它们配在一起有多合适，并且各自本身是否已经带有倾向”。</p>
<p>若输出不是一个标量分数，而是 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个关系类别或标签分数，则通常不只使用一个 <span displaypfx="inline-" class="mathjax-container">\(U\)</span>，而是为每个类别准备一张交互矩阵 <span displaypfx="inline-" class="mathjax-container">\(U^{(k)}\)</span>，或等价地把它们堆成三阶张量 <span displaypfx="inline-" class="mathjax-container">\(U\in\mathbb{R}^{K\times m\times n}\)</span>。这时每个类别都拥有自己的一套“特征两两交互”权重。</p>
<p>从函数结构看，线性与仿射的区别在于是否含偏置；双线性与双仿射的区别同样在于是否在交互项之外再加入单边项与偏置。固定 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 时， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top U\mathbf{z}\)</span> 对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 是线性的， <span displaypfx="inline-" class="mathjax-container">\(s_{\text{biaffine}}(\mathbf{x},\mathbf{z})\)</span> 对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 则是仿射的；固定 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 时，对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{z}\)</span> 也是同样的性质。这正是 “biaffine” 这个名字的数学含义。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/affine-biaffine-family.png"><img class="alignnone size-full" src="https://blog.gmem.cc/wp-content/uploads/2026/03/affine-biaffine-family.png" alt="affine-biaffine-family" width="1920" height="1203" /></a></p>
<p>图中上排用一维切片显示“是否经过原点”这一关键差异： <span displaypfx="inline-" class="mathjax-container">\(f(x)=ax\)</span> 必然经过原点，而 <span displaypfx="inline-" class="mathjax-container">\(f(x)=ax+b\)</span> 由于加入偏置项，整条直线沿输出轴发生平移。下排保持与公式族谱一致，仍写成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top U\mathbf{z}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top U\mathbf{z}+\mathbf{a}^\top\mathbf{x}+\mathbf{c}^\top\mathbf{z}+b\)</span>；图像本身展示的是把高维向量关系压到二维后的一个切片。纯双线性项的零等值线体现为对称的交互边界；加入单边项与偏置后，零等值线整体偏移，分数面出现倾斜与平移，表示模型不仅关心“是否匹配”，还关心两个对象各自单独的倾向。</p>
<p>在依存句法（Dependency Parsing）、关系抽取（Relation Extraction）和成对匹配（Pairwise Matching）任务中，这个结构尤其有用。纯双线性项只能表达“组合后是否相容”，双仿射则进一步允许模型学习“某个对象本身就更像 head / dependent”或“某个实体本身就更像某类关系的一端”。因此，双仿射通常既比纯仿射更能表达交互，又比纯双线性更稳定。</p>
<div class="blog_h3"><span class="graybg">转置（Transpose）</span></div>
<p>转置（Transpose）把行列互换：对 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{m\times n}\)</span>，其转置 <span displaypfx="inline-" class="mathjax-container">\(A^\top\in\mathbb{R}^{n\times m}\)</span> 满足 <span displaypfx="inline-" class="mathjax-container">\((A^\top)_{ij}=A_{ji}\)</span>。</p>
<p>它常用于对齐乘法形状、把点积写成矩阵乘法，以及在推导中“移动”矩阵：例如 <span displaypfx="inline-" class="mathjax-container">\((AB)^\top=B^\top A^\top\)</span>。Transformer 注意力中的 <span displaypfx="inline-" class="mathjax-container">\(QK^\top\)</span> 就是典型的“先转置再相乘”。</p>
<div class="blog_h4"><span class="graybg">例：行列互换与形状变化</span></div>
<p>令</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}1 &amp; 2 &amp; 3\\ 4 &amp; 5 &amp; 6\end{bmatrix}\in\mathbb{R}^{2\times 3}\]</span>
<p>则</p>
<span displaypfx="" class="mathjax-container">\[A^\top=\begin{bmatrix}1 &amp; 4\\ 2 &amp; 5\\ 3 &amp; 6\end{bmatrix}\in\mathbb{R}^{3\times 2}\]</span>
<p>可以直接看到：原矩阵的第 1 行变成转置后的第 1 列；因此 <span displaypfx="inline-" class="mathjax-container">\((m\times n)^\top=(n\times m)\)</span>。</p>
<div class="blog_h3"><span class="graybg">Hadamard 乘积（Hadamard Product）</span></div>
<p>Hadamard 乘积（Hadamard Product）是逐元素（Element-wise）相乘：若 <span displaypfx="inline-" class="mathjax-container">\(X,M\)</span> 形状相同，则 <span displaypfx="inline-" class="mathjax-container">\((X\odot M)_{ij}=X_{ij}M_{ij}\)</span>。</p>
<p>典型用途是掩码（Masking）与门控（Gating）。例：令 <span displaypfx="inline-" class="mathjax-container">\(M\in\{0,1\}^{B\times d}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(X\odot M\)</span> 会把被屏蔽的特征位置直接置零；也可以用 <span displaypfx="inline-" class="mathjax-container">\(M\in[0,1]^{B\times d}\)</span> 做连续缩放。</p>
<div class="blog_h3"><span class="graybg">矩阵分解（Factorization / Decomposition）</span></div>
<p>矩阵分解（Decomposition）把矩阵写成更“易处理”的结构乘积，用于求解、降维与稳定计算。常见形式包括：</p>
<ul>
<li>SVD： <span displaypfx="inline-" class="mathjax-container">\(A=U\Sigma V^\top\)</span>，用于 PCA（Principal Component Analysis）、低秩近似与数值稳健求解。</li>
<li>QR： <span displaypfx="inline-" class="mathjax-container">\(A=QR\)</span>（<span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 正交、<span displaypfx="inline-" class="mathjax-container">\(R\)</span> 上三角），常用于最小二乘与正交化。</li>
<li>Cholesky：对对称正定（SPD）矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(A=LL^\top\)</span>，常用于高斯模型与二次优化中的快速求解。</li>
</ul>
<div class="blog_h3"><span class="graybg">迹（Trace）</span></div>
<p>矩阵的迹（Trace）定义为对角线元素之和：对方阵 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{n\times n}\)</span>，</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{tr}(A)=\sum_{i=1}^{n} A_{ii}\]</span>
<p>迹在推导里常用的性质是循环不变性（Cyclic Property）：<span displaypfx="inline-" class="mathjax-container">\(\mathrm{tr}(AB)=\mathrm{tr}(BA)\)</span>（形状匹配时）。一个高频等式是 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{tr}(A^\top A)=\|A\|_F^2\)</span>，它把“平方和”写成迹，便于做矩阵微分与正则化推导。</p>
<div class="blog_h3"><span class="graybg">范数（Matrix Norms）</span></div>
<p>范数（Norm）刻画矩阵的“大小”。最常用的是 Frobenius 范数：</p>
<span displaypfx="" class="mathjax-container">\[\|A\|_F=\sqrt{\sum_{i,j}A_{ij}^2}\]</span>
<p>它等价于把矩阵按元素展平后的 <span displaypfx="inline-" class="mathjax-container">\(\ell_2\)</span> 范数，常用于权重衰减（Weight Decay）/L2 正则。另一个常见的是谱范数（Spectral Norm）<span displaypfx="inline-" class="mathjax-container">\(\|A\|_2\)</span>（最大奇异值），用于控制 Lipschitz 常数与训练稳定性（如 spectral normalization）。</p>
<div class="blog_h3"><span class="graybg">外积与秩一更新（Outer Product / Rank-1 Update）</span></div>
<p>外积（Outer Product）把两个向量映射为矩阵：对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\in\mathbb{R}^{m}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\in\mathbb{R}^{n}\)</span>，</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{u}\mathbf{v}^\top\in\mathbb{R}^{m\times n}\]</span>
<p>它是一个秩一（Rank-1）矩阵。外积在统计与学习中常用于构造二阶量：例如样本协方差的无偏估计可写成中心化向量的外积平均 <span displaypfx="inline-" class="mathjax-container">\(\Sigma\approx \frac{1}{N}\sum_{k=1}^{N}(\mathbf{x}_k-\bar{\mathbf{x}})(\mathbf{x}_k-\bar{\mathbf{x}})^\top\)</span>。</p>
<p>例：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}=\begin{bmatrix}1\\ 2\\ 3\end{bmatrix}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}=\begin{bmatrix}4\\ 5\end{bmatrix}\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{u}\mathbf{v}^\top=\begin{bmatrix}4 &amp; 5\\ 8 &amp; 10\\ 12 &amp; 15\end{bmatrix}\]</span>
<p>直观上，外积得到的矩阵每一列都是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\)</span> 的缩放：第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 列等于 <span displaypfx="inline-" class="mathjax-container">\(v_j\mathbf{u}\)</span>。因此所有列共线，矩阵的秩最多为 1（除非 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\)</span> 为零向量）。</p>
<p>把外积视为线性算子更直接：对任意 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\in\mathbb{R}^{n}\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{u}\mathbf{v}^\top)\mathbf{x}=\mathbf{u}(\mathbf{v}^\top\mathbf{x})\)</span>。这表示先沿 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\)</span> 做一次投影/打分得到标量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}^\top\mathbf{x}\)</span>，再沿 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\)</span> 方向输出。例：取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=\begin{bmatrix}1\\ 1\end{bmatrix}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}^\top\mathbf{x}=9\)</span>，从而 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{u}\mathbf{v}^\top)\mathbf{x}=9\mathbf{u}=\begin{bmatrix}9\\ 18\\ 27\end{bmatrix}\)</span>。</p>
<p>秩一更新（Rank-1 Update）则是把矩阵写成 <span displaypfx="inline-" class="mathjax-container">\(A\leftarrow A+\mathbf{u}\mathbf{v}^\top\)</span>：只引入一个方向上的低秩结构，常用于用较低代价注入统计量/二阶近似，或在保持主结构的前提下做小幅调整。</p>
<div class="blog_h2"><span class="graybg">行列式</span></div>
<p>行列式（Determinant）把一个方阵 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{n\times n}\)</span> 映射为标量 <span displaypfx="inline-" class="mathjax-container">\(\det(A)\)</span>。几何上，它是线性变换对体积的缩放因子（Volume Scaling Factor）：绝对值表示缩放倍数，符号表示是否翻转取向（Orientation Flip）。</p>
<p>二维情形最直观：若</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}a &amp; b\\ c &amp; d\end{bmatrix}\]</span>
<p>则</p>
<span displaypfx="" class="mathjax-container">\[\det(A)=ad-bc\]</span>
<p>例： <span displaypfx="inline-" class="mathjax-container">\(A=\begin{bmatrix}2 &amp; 1\\ 0 &amp; 3\end{bmatrix}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\det(A)=6\)</span>：面积被放大 6 倍。</p>
<p>关键结论：<span style="background-color: #c0c0c0;">方阵可逆（Invertible）当且仅当行列式非零</span>。当 <span displaypfx="inline-" class="mathjax-container">\(\det(A)=0\)</span> 时，变换会把体积压扁到低维（丢失信息），对应列向量线性相关（Linearly Dependent）。</p>
<p>常用性质：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\det(AB)=\det(A)\det(B)\)</span></li>
<li><span displaypfx="inline-" class="mathjax-container">\(\det(A^\top)=\det(A)\)</span></li>
<li><span displaypfx="inline-" class="mathjax-container">\(\det(I)=1\)</span></li>
</ul>
<p>若把特征值（Eigenvalues）记作 <span displaypfx="inline-" class="mathjax-container">\(\{\lambda_i\}_{i=1}^n\)</span>（按代数重数计），则 <span displaypfx="inline-" class="mathjax-container">\(\det(A)=\prod_i \lambda_i\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathrm{tr}(A)=\sum_i \lambda_i\)</span>。</p>
<div class="blog_h2"><span class="graybg">矩阵的秩</span></div>
<p>矩阵的秩（Rank）刻画“列（或行）里最多有多少个线性无关（Linearly Independent）的方向”。对 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{m\times n}\)</span>，秩定义为列空间（Column Space）的维数：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{rank}(A)=\dim(\mathrm{col}(A))\]</span>
<p>这一定义的直接含义是：若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{rank}(A)=r\)</span>，那么 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的所有列向量都只能张成一个 <span displaypfx="inline-" class="mathjax-container">\(r\)</span> 维子空间，超出这个子空间的方向它完全无法表达。反过来，若某一列可以由其他列线性组合得到，它就不提供新的维度，因此不会增加秩。</p>
<p>它也等于行空间（Row Space）的维数（行秩=列秩）。把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 看作线性映射 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}^n\to\mathbb{R}^m\)</span>，秩就是输出子空间的维度：最多能输出多少个自由方向。</p>
<p>满秩（Full Rank）通常指 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{rank}(A)=\min(m,n)\)</span>。对方阵 <span displaypfx="inline-" class="mathjax-container">\(n\times n\)</span> 而言，满秩等价于可逆（也等价于 <span displaypfx="inline-" class="mathjax-container">\(\det(A)\ne 0\)</span>）。</p>
<p>线性方程组（Linear System）<span displaypfx="inline-" class="mathjax-container">\(A\mathbf{x}=\mathbf{b}\)</span> 的解与秩直接相关：设增广矩阵为 <span displaypfx="inline-" class="mathjax-container">\(\left[A\,\middle|\,\mathbf{b}\right]\)</span>，则</p>
<ul>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{rank}(A)\ne \mathrm{rank}\!\left(\left[A\,\middle|\,\mathbf{b}\right]\right)\)</span>，无解。</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{rank}(A)=\mathrm{rank}\!\left(\left[A\,\middle|\,\mathbf{b}\right]\right)=n\)</span>（未知数个数），唯一解。</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{rank}(A)=\mathrm{rank}\!\left(\left[A\,\middle|\,\mathbf{b}\right]\right)<n[/latex]，无穷多解（存在自由变量）。</li>
</ul>
<p>与 SVD 的关系非常实用：<span style="background-color: #c0c0c0;">秩等于非零奇异值（Singular Values）的个数</span>，因此在数值计算里常用“奇异值是否接近 0”判断有效秩（Numerical Rank）。</p>
<div class="blog_h2"><span class="graybg">二次型（Quadratic Form）</span></div>
<p>在高中里，一元二次函数常写成 [latex syntax=inline]ax^2+bx+c\)</span>。把“二次”推广到多元，并且只保留二次项（没有一次项与常数项），就得到二次型（Quadratic Form）。二维里最常见的形式是：</p>
<span displaypfx="" class="mathjax-container">\[q(x,y)=ax^2+bxy+cy^2\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(bxy\)</span> 是交叉项（Cross Term）：它把不同变量“耦合在一起”。在解析几何（Analytic Geometry）里，交叉项常对应二次曲线（Conic Section）的主轴（Principal Axes）不与坐标轴对齐（图形呈旋转/倾斜）。</p>
<p>若再把一次项与常数项加回来，就得到更一般的二次多项式（Quadratic Polynomial）<span displaypfx="inline-" class="mathjax-container">\(ax^2+bxy+cy^2+dx+ey+f\)</span>。这时：交叉项 <span displaypfx="inline-" class="mathjax-container">\(bxy\)</span> 主要反映主轴旋转；一次项 <span displaypfx="inline-" class="mathjax-container">\(dx+ey\)</span> 往往表示图形的中心/顶点从原点平移出去；常数项 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 则改变整体基准值，进而影响图形的截距、大小以及是否与 <span displaypfx="inline-" class="mathjax-container">\(q(x,y)=0\)</span> 相交。只有把一次项和常数项都去掉时，我们讨论的才是纯粹的二次型。</p>
<p>下面两幅图都基于<span style="background-color: #c0c0c0;">等值线（Level Set）</span>生成：先固定常数 <span displaypfx="inline-" class="mathjax-container">\(k\)</span>，在平面上求解 <span displaypfx="inline-" class="mathjax-container">\(q(x,y)=k\)</span> 得到等值线，再把不同 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 的结果叠加，并与 <span displaypfx="inline-" class="mathjax-container">\(z=q(x,y)\)</span> 的三维曲面对应显示。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/quadratic-forms.png"><img class="alignnone size-full wp-image-40671" src="https://blog.gmem.cc/wp-content/uploads/2026/03/quadratic-forms.png" alt="quadratic-forms" width="100%" /></a></p>
<p>第一幅展示无交叉项（<span displaypfx="inline-" class="mathjax-container">\(b=0\)</span>）的典型形态：等值线与坐标轴对齐，曲面主轴方向也与坐标轴一致。</p>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/quadratic-forms-cross.png"><img class="alignnone size-full wp-image-40677" src="https://blog.gmem.cc/wp-content/uploads/2026/03/quadratic-forms-cross.png" alt="quadratic-forms-cross" width="100%" /></a></p>
<p>第二幅展示含交叉项（<span displaypfx="inline-" class="mathjax-container">\(b\ne 0\)</span>）的情形：等值线整体发生旋转/倾斜，三维曲面看起来更“不规则”。</p>
<p>在线性代数里，用矩阵（Matrix）把系数组织起来更方便。令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x_1,\ldots,x_n)^\top\)</span>、<span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{n\times n}\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[q(\mathbf{x})=\mathbf{x}^\top A\mathbf{x}=\sum_{i=1}^{n}\sum_{j=1}^{n}A_{ij}x_i x_j\]</span>
<p>上面这条等式不是“记号游戏”，而是把矩阵乘法按分量（Component）展开后的结果。把它分两步看最清楚：</p>
<ol>
<li>先做一次矩阵-向量乘法：令 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}=A\mathbf{x}\)</span>，则第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量是</li>
</ol>
<span displaypfx="" class="mathjax-container">\[y_i=(A\mathbf{x})_i=\sum_{j=1}^{n}A_{ij}x_j\]</span>
<ol start="2">
<li>再做一次点积： <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top\mathbf{y}=\sum_{i=1}^{n}x_i y_i\)</span>。把第 1 步的 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 代入，就得到</li>
</ol>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=\mathbf{x}^\top(A\mathbf{x})=\sum_{i=1}^{n}x_i\left(\sum_{j=1}^{n}A_{ij}x_j\right)=\sum_{i=1}^{n}\sum_{j=1}^{n}A_{ij}x_i x_j\]</span>
<p>这就是“双重求和（Double Summation）”的含义：每一个矩阵元素 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}\)</span> 都在给二次项 <span displaypfx="inline-" class="mathjax-container">\(x_i x_j\)</span> 分配一个权重；当 <span displaypfx="inline-" class="mathjax-container">\(i=j\)</span> 时就是平方项 <span displaypfx="inline-" class="mathjax-container">\(x_i^2\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(i\ne j\)</span> 时就是交叉项 <span displaypfx="inline-" class="mathjax-container">\(x_i x_j\)</span>。</p>
<p>二维情形最直观：若希望展开后得到 <span displaypfx="inline-" class="mathjax-container">\(ax^2+bxy+cy^2\)</span>，可以取</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}a &amp; \frac{b}{2}\\ \frac{b}{2} &amp; c\end{bmatrix}\]</span>
<p>这里把向量写成 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x,y)^\top\)</span>，按矩阵乘法展开一遍：</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{x}=\begin{bmatrix}a &amp; \frac{b}{2}\\ \frac{b}{2} &amp; c\end{bmatrix}\begin{bmatrix}x\\ y\end{bmatrix}=\begin{bmatrix}ax+\frac{b}{2}y\\ \frac{b}{2}x+cy\end{bmatrix}\]</span>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top(A\mathbf{x})=\begin{bmatrix}x &amp; y\end{bmatrix}\begin{bmatrix}ax+\frac{b}{2}y\\ \frac{b}{2}x+cy\end{bmatrix}=ax^2+\frac{b}{2}xy+\frac{b}{2}yx+cy^2=ax^2+bxy+cy^2\]</span>
<p>可以看到：交叉项 <span displaypfx="inline-" class="mathjax-container">\(xy\)</span> 的系数 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 实际来自两处对称位置 <span displaypfx="inline-" class="mathjax-container">\(A_{12}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(A_{21}\)</span> 的“合力”（各贡献一半）。这也会自然导向下一节的结论：二次型只依赖矩阵的对称部分。</p>
<p>例：取 <span displaypfx="inline-" class="mathjax-container">\(q(x,y)=5x^2-4xy+5y^2\)</span>，对应 <span displaypfx="inline-" class="mathjax-container">\(A=\begin{bmatrix}5 &amp; -2\\ -2 &amp; 5\end{bmatrix}\)</span>。这一类表达式不仅在几何里出现（椭圆（Ellipse）/双曲线（Hyperbola）），在优化与统计里也高频出现（曲率（Curvature）、距离度量（Distance Metric））。</p>
<div class="blog_h3"><span class="graybg">对称化（Symmetrization）</span></div>
<p>对任意方阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，二次型只依赖其对称部分：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=\mathbf{x}^\top\left(\frac{A+A^\top}{2}\right)\mathbf{x}\]</span>
<p>这句话的意思是：无论 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的非对称部分长什么样，只要 <span displaypfx="inline-" class="mathjax-container">\(\frac{A+A^\top}{2}\)</span> 不变，二次型 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}\)</span> 对所有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的取值就完全不变。</p>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 拆开看更直观。定义对称部分（Symmetric Part）与反对称部分（Skew-symmetric Part）：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(S=\frac{A+A^\top}{2}\)</span>，满足 <span displaypfx="inline-" class="mathjax-container">\(S=S^\top\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(K=\frac{A-A^\top}{2}\)</span>，满足 <span displaypfx="inline-" class="mathjax-container">\(K^\top=-K\)</span>。</li>
</ul>
<p>这里“对称部分（Symmetric Part）”是一个<span style="background-color: #c0c0c0;">定义</span>：对任意方阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，把 <span displaypfx="inline-" class="mathjax-container">\(S=\frac{A+A^\top}{2}\)</span> 定义为它的对称部分。它与 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> <span style="background-color: #c0c0c0;">同型（同大小）</span>，并且一定是对称矩阵；它不是 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的某个“子矩阵”。</p>
<p>按元素（Entry-wise）写得更直观：对任意 <span displaypfx="inline-" class="mathjax-container">\(i,j\)</span>，</p>
<span displaypfx="" class="mathjax-container">\[S_{ij}=\frac{A_{ij}+A_{ji}}{2},\quad K_{ij}=\frac{A_{ij}-A_{ji}}{2}\]</span>
<p>也就是说：对称部分就是把每一对对称位置 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\((j,i)\)</span> 的元素取平均；反对称部分则记录它们的“差的一半”。因此 <span displaypfx="inline-" class="mathjax-container">\(A=S+K\)</span> 是把任意矩阵分解成“对称 + 反对称”的标准方式，并且这个分解是唯一的（Unique）。</p>
<p>为什么“取 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(A^\top\)</span> 的平均值”就得到对称部分？因为转置（Transpose）会把非对角元素成对交换： <span displaypfx="inline-" class="mathjax-container">\(A_{ij}\leftrightarrow A_{ji}\)</span>。把它们相加后，非对称性（即 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}-A_{ji}\)</span>）会被抵消，只留下“对称的那一半”（即 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}+A_{ji}\)</span>）。再除以 2，是为了把“加了两份”的量恢复到原始尺度：如果 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 本来就对称（<span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span>），那么 <span displaypfx="inline-" class="mathjax-container">\(\frac{A+A^\top}{2}=A\)</span>，不会把矩阵放大一倍。</p>
<p>于是 <span displaypfx="inline-" class="mathjax-container">\(A=S+K\)</span>，并且</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=\mathbf{x}^\top S\mathbf{x}+\mathbf{x}^\top K\mathbf{x}\]</span>
<p>关键点在于：对任意 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span>，都有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top K\mathbf{x}=0\)</span>。理由很短：它是一个标量，等于它自己的转置，而</p>
<span displaypfx="" class="mathjax-container">\[(\mathbf{x}^\top K\mathbf{x})^\top=\mathbf{x}^\top K^\top \mathbf{x}=\mathbf{x}^\top(-K)\mathbf{x}=-(\mathbf{x}^\top K\mathbf{x})\]</span>
<p>一个数如果等于它的相反数，只能是 0。于是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}=\mathbf{x}^\top S\mathbf{x}\)</span>，二次型确实只由对称部分决定。</p>
<p>二维展开能直接看到“只依赖对称部分”的具体含义。令 <span displaypfx="inline-" class="mathjax-container">\(A=\begin{bmatrix}a &amp; b\\ c &amp; d\end{bmatrix}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x_1,x_2)^\top\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=ax_1^2+(b+c)x_1x_2+dx_2^2\]</span>
<p>交叉项系数只出现 <span displaypfx="inline-" class="mathjax-container">\(b+c\)</span>（也就是 <span displaypfx="inline-" class="mathjax-container">\(A_{12}+A_{21}\)</span>），而差值 <span displaypfx="inline-" class="mathjax-container">\(b-c\)</span>（反对称部分）完全不会出现。</p>
<div class="blog_h4"><span class="graybg">例：两个不同矩阵，二次型完全一样</span></div>
<p>下面给一个“看得见”的数值例子。取</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}2 &amp; 4\\ -2 &amp; 4\end{bmatrix}\]</span>
<p>它显然不是对称矩阵（因为 <span displaypfx="inline-" class="mathjax-container">\(A_{12}=4\ne -2=A_{21}\)</span>）。计算它的对称部分：</p>
<span displaypfx="" class="mathjax-container">\[\frac{A+A^\top}{2}=\frac{1}{2}\left(\begin{bmatrix}2 &amp; 4\\ -2 &amp; 4\end{bmatrix}+\begin{bmatrix}2 &amp; -2\\ 4 &amp; 4\end{bmatrix}\right)=\begin{bmatrix}2 &amp; 1\\ 1 &amp; 4\end{bmatrix}=S\]</span>
<p>现在比较二次型。对任意 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x,y)^\top\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{x}=\begin{bmatrix}2x+4y\\ -2x+4y\end{bmatrix}\Rightarrow \mathbf{x}^\top A\mathbf{x}=x(2x+4y)+y(-2x+4y)=2x^2+2xy+4y^2\]</span>
<span displaypfx="" class="mathjax-container">\[S\mathbf{x}=\begin{bmatrix}2x+y\\ x+4y\end{bmatrix}\Rightarrow \mathbf{x}^\top S\mathbf{x}=x(2x+y)+y(x+4y)=2x^2+2xy+4y^2\]</span>
<p>两者对所有 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 都完全相同；例如取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(1,2)^\top\)</span>，都有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}=\mathbf{x}^\top S\mathbf{x}=22\)</span>。这就直观解释了“二次型只依赖对称部分”的含义：反对称的那一半怎么改，都不会改变 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}\)</span> 的值。</p>
<p>因此讨论二次型时通常可假设 <span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span>。这也解释了为什么二次型与对称矩阵/半正定性（Positive Semi-Definite, PSD）紧密绑定。</p>
<div class="blog_h3"><span class="graybg">二次型的标准型（Standard Form）</span></div>
<div class="blog_h4"><span class="graybg">标准型与对角标准型是什么</span></div>
<p>二次型 <span displaypfx="inline-" class="mathjax-container">\(q(\mathbf{x})=\mathbf{x}^\top A\mathbf{x}\)</span> 本身是一个几何对象；它在不同坐标系（Coordinate System）/基（Basis）下的<span style="background-color: #c0c0c0;">矩阵表示</span>会不同：同一个几何对象，用不同坐标轴/基表示时，系数矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的元素会改变。</p>
<p>这里的标准型指的是：在一类允许的坐标变换（可逆线性变量替换（Invertible Linear Change of Variables））下，把同一个二次型写成某种<span style="background-color: #c0c0c0;">约定的简化代表</span>。不同教材的约定略有差异，但最常用的目标是：把交叉项（Cross Term）消掉，露出每个坐标轴方向上的“纯平方项”。</p>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=T\mathbf{y}\)</span> 理解成换基（Change of Basis）会更不容易出错： <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 是同一几何向量在新基下的坐标，矩阵 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 由新基向量在旧基下的坐标组成。因为 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 可逆，两套坐标是一一对应的，可以互相换回：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{y}=T^{-1}\mathbf{x},\quad \mathbf{x}=T\mathbf{y}\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=T\mathbf{y}\)</span> 代入可得</p>
<span displaypfx="" class="mathjax-container">\[q(\mathbf{x})=\mathbf{x}^\top A\mathbf{x}=\mathbf{y}^\top(T^\top A T)\mathbf{y}\]</span>
<p>这里要求 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 可逆（Invertible），意味着这个变量代换不会把空间压缩到低维（不会丢维度）。因此在新坐标 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 下，二次型对应的系数矩阵变为 <span displaypfx="inline-" class="mathjax-container">\(T^\top A T\)</span>（这叫合同变换（Congruence Transformation））。可以把 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 理解为“旋转/缩放后的新坐标轴”在旧坐标里的表示：同一个二次型在新坐标系里的系数就由 <span displaypfx="inline-" class="mathjax-container">\(T^\top A T\)</span> 给出。标准型的目标就是选取合适的 <span displaypfx="inline-" class="mathjax-container">\(T\)</span>，把 <span displaypfx="inline-" class="mathjax-container">\(T^\top A T\)</span> 化到更简单的结构。</p>
<p>对角标准型（Diagonal Form）指把二次型写成“只有平方项、没有交叉项”的形式（也常称对角规范形（Diagonal Canonical Form））：</p>
<span displaypfx="" class="mathjax-container">\[q(\mathbf{x})=\sum_{i=1}^{n}\lambda_i y_i^2\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i\)</span> 是系数，<span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 是新坐标。对角标准型等价于：在新坐标系下，二次型对应的矩阵是对角矩阵（Diagonal Matrix）；“交叉项” <span displaypfx="inline-" class="mathjax-container">\(y_i y_j\)</span>（<span displaypfx="inline-" class="mathjax-container">\(i\ne j\)</span>）消失。</p>
<p>结论需要明确：标准型与原来的二次型描述的是<span style="background-color: #c0c0c0;">同一个二次型/同一组几何等值集合</span>，只是坐标系不同。给定可逆变换 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=T\mathbf{y}\)</span>，任何关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 的几何描述都可以无损地翻译成关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 的描述，并且可以随时“还原”回去。矩阵层面也一样：若 <span displaypfx="inline-" class="mathjax-container">\(A' = T^\top A T\)</span> 是标准型里的系数矩阵，则 <span displaypfx="inline-" class="mathjax-container">\(A=(T^{-1})^\top A' T^{-1}\)</span> 可把它变回原坐标下的表示。</p>
<p>接下来真正关心的是：<span style="background-color: #c0c0c0;">如何选 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 才能把交叉项消掉</span>。对二次型而言，一个关键简化是：二次型只依赖矩阵的对称部分，因此总可以先把 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 对称化为 <span displaypfx="inline-" class="mathjax-container">\(\frac{A+A^\top}{2}\)</span> 而不改变 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}\)</span> 的值。于是“消交叉项”的核心问题就变成：对实对称矩阵，能否通过一次正交变基（Orthogonal Change of Basis）把它对角化（Diagonalize）。</p>
<div class="blog_h4"><span class="graybg">怎么来的：换到特征向量基消掉交叉项</span></div>
<p>对实对称矩阵，对角化与“消掉交叉项”来自同一个结构事实：它总可以通过一次正交变基（Orthogonal Change of Basis）写成对角形式。数学依据就是谱定理（Spectral Theorem，也常表述为“实对称矩阵可正交对角化（Orthogonal Diagonalization）”）。若 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{n\times n}\)</span> 是实对称矩阵（Real Symmetric Matrix, <span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span>），则存在正交矩阵（Orthogonal Matrix）<span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 与实对角矩阵（Real Diagonal Matrix）<span displaypfx="inline-" class="mathjax-container">\(\Lambda\)</span> 使得</p>
<span displaypfx="" class="mathjax-container">\[A=Q\Lambda Q^\top,\quad \text{等价于}\quad Q^\top A Q=\Lambda\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(Q=[\mathbf{v}_1,\ldots,\mathbf{v}_n]\)</span> 的列向量是一组单位特征向量（Orthonormal Eigenvectors），<span displaypfx="inline-" class="mathjax-container">\(\Lambda=\mathrm{diag}(\lambda_1,\ldots,\lambda_n)\)</span> 的对角元素是对应特征值（Eigenvalues）。该定理同时包含两个常用事实：特征值都是实数；并且可以选出一组两两正交的特征向量作为基。</p>
<p>这就是你熟悉的特征值分解（Eigendecomposition / Eigenvalue Decomposition）的对称矩阵特例。</p>
<p>一般情况下，如果矩阵可对角化（Diagonalizable），可以写成 <span displaypfx="inline-" class="mathjax-container">\(A=V\Lambda V^{-1}\)</span>（或 <span displaypfx="inline-" class="mathjax-container">\(V^{-1}AV=\Lambda\)</span>），其中 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 的列是特征向量；但 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 不一定正交（Orthogonal），甚至矩阵可能不可对角化（Non-diagonalizable）。</p>
<p>对称矩阵的额外好处是：可以把 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 选成正交矩阵 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(V^{-1}=Q^{-1}=Q^\top\)</span>，分解变成数值上更稳定、几何上更直观的 <span displaypfx="inline-" class="mathjax-container">\(A=Q\Lambda Q^\top\)</span>。</p>
<p>两个最小例子能把“<span displaypfx="inline-" class="mathjax-container">\(V\)</span> 不一定正交 / 甚至不可对角化”说得更具体：</p>
<ul>
<li>
<p><span style="background-color: #c0c0c0;">例 1：可对角化，但 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 不正交</span>。取</p>
<span displaypfx="" class="mathjax-container">\[A_1=\begin{bmatrix}2 &amp; 1\\ 0 &amp; 1\end{bmatrix}\]</span>
<p>它的特征值是 <span displaypfx="inline-" class="mathjax-container">\(\lambda_1=2,\lambda_2=1\)</span>（两个不同特征值意味着在二维里一定能找到两条线性无关的特征向量，因此可对角化）。对应一组特征向量可以取</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{v}_1=\begin{bmatrix}1\\ 0\end{bmatrix},\quad \mathbf{v}_2=\begin{bmatrix}1\\ -1\end{bmatrix}\]</span>
<p>它们并不正交，因为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}_1^\top\mathbf{v}_2=1\ne 0\)</span>。把它们按列组成矩阵 <span displaypfx="inline-" class="mathjax-container">\(V=[\mathbf{v}_1\ \mathbf{v}_2]\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[V=\begin{bmatrix}1 &amp; 1\\ 0 &amp; -1\end{bmatrix},\quad \Lambda=\begin{bmatrix}2 &amp; 0\\ 0 &amp; 1\end{bmatrix},\quad V^{-1}=\begin{bmatrix}1 &amp; 1\\ 0 &amp; -1\end{bmatrix}\]</span>
<p>注意：这个例子里 <span displaypfx="inline-" class="mathjax-container">\(V^{-1}\)</span> 恰好等于 <span displaypfx="inline-" class="mathjax-container">\(V\)</span>（只是代数上的巧合），但它仍然不是正交矩阵，因为正交要求 <span displaypfx="inline-" class="mathjax-container">\(V^{-1}=V^\top\)</span>，而这里并不成立。</p>
<p>并且确实有 <span displaypfx="inline-" class="mathjax-container">\(A_1=V\Lambda V^{-1}\)</span>。这个例子说明：一般矩阵即使可对角化，特征向量也未必能选成“互相垂直的方向”。</p>
</li>
<li>
<p><span style="background-color: #c0c0c0;">例 2：不可对角化（特征值重复，但特征向量不够）</span>。取</p>
<span displaypfx="" class="mathjax-container">\[A_2=\begin{bmatrix}1 &amp; 1\\ 0 &amp; 1\end{bmatrix}\]</span>
<p>它的特征值只有 <span displaypfx="inline-" class="mathjax-container">\(\lambda=1\)</span>（在二维里重复出现）。求特征向量需要解 <span displaypfx="inline-" class="mathjax-container">\((A_2-I)\mathbf{v}=\mathbf{0}\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[A_2-I=\begin{bmatrix}0 &amp; 1\\ 0 &amp; 0\end{bmatrix}\Rightarrow (A_2-I)\begin{bmatrix}x\\ y\end{bmatrix}=\begin{bmatrix}y\\ 0\end{bmatrix}=\begin{bmatrix}0\\ 0\end{bmatrix}\Rightarrow y=0\]</span>
<p>因此所有特征向量都形如 <span displaypfx="inline-" class="mathjax-container">\((x,0)^\top\)</span>，只有 1 个线性无关方向。要写成 <span displaypfx="inline-" class="mathjax-container">\(A_2=V\Lambda V^{-1}\)</span>，矩阵 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 必须可逆（Invertible），这要求有足够多（在二维里是 2 个）线性无关特征向量作为列；该矩阵做不到，所以它不可对角化。</p>
</li>
</ul>
<p>把这个定理放回二次型就能立刻看出“交叉项为什么会消失”。令坐标变换 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}=Q^\top\mathbf{x}\)</span>（把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 在特征向量基下的坐标记作 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span>），则</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=\mathbf{x}^\top(Q\Lambda Q^\top)\mathbf{x}=\mathbf{y}^\top\Lambda\mathbf{y}=\sum_{i=1}^{n}\lambda_i y_i^2\]</span>
<p>注意这里同时出现了两种“换坐标”的写法：线性变换里常写 <span displaypfx="inline-" class="mathjax-container">\(Q^{-1}AQ\)</span>（相似变换（Similarity Transformation）），二次型里写 <span displaypfx="inline-" class="mathjax-container">\(Q^\top A Q\)</span>（合同变换（Congruence Transformation））。对正交矩阵而言 <span displaypfx="inline-" class="mathjax-container">\(Q^{-1}=Q^\top\)</span>，所以它们在这里完全一致：同一个正交变换既给出特征值分解，也把二次型化到没有交叉项的对角标准型。</p>
<p>因此：在对称矩阵的情形，“换到特征向量基”与“把二次型旋转到主轴”是同一件事，只是用不同语言描述。</p>
<p>直观上，特征向量（Eigenvector）给出“主轴方向”（把坐标轴转到这些方向后，交叉项会消失），特征值（Eigenvalue）则是标准型里各平方项前的系数。</p>
<p>详细例子：取</p>
<span displaypfx="" class="mathjax-container">\[A=\begin{bmatrix}5 &amp; -2\\ -2 &amp; 5\end{bmatrix}\]</span>
<p>它是对称矩阵，因此可正交对角化。其特征值与一组单位特征向量可以取为：</p>
<span displaypfx="" class="mathjax-container">\[\lambda_1=3,\ \mathbf{v}_1=\frac{1}{\sqrt{2}}\begin{bmatrix}1\\ 1\end{bmatrix};\quad \lambda_2=7,\ \mathbf{v}_2=\frac{1}{\sqrt{2}}\begin{bmatrix}1\\ -1\end{bmatrix}\]</span>
<p>把它们组成正交矩阵与对角矩阵：</p>
<span displaypfx="" class="mathjax-container">\[Q=[\mathbf{v}_1\ \mathbf{v}_2]=\frac{1}{\sqrt{2}}\begin{bmatrix}1 &amp; 1\\ 1 &amp; -1\end{bmatrix},\quad \Lambda=\begin{bmatrix}3 &amp; 0\\ 0 &amp; 7\end{bmatrix}\]</span>
<p>则 <span displaypfx="inline-" class="mathjax-container">\(A=Q\Lambda Q^\top\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 不是任意新变量，而是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 在特征向量基 <span displaypfx="inline-" class="mathjax-container">\(\{\mathbf{v}_1,\mathbf{v}_2\}\)</span> 下的坐标： <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=y_1\mathbf{v}_1+y_2\mathbf{v}_2=Q\mathbf{y}\)</span>。</p>
<p>由于 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 是正交矩阵（<span displaypfx="inline-" class="mathjax-container">\(Q^\top Q=I\)</span>），左乘 <span displaypfx="inline-" class="mathjax-container">\(Q^\top\)</span> 可得 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}=Q^\top\mathbf{x}\)</span>。这一步就是换基（Change of Basis）：把向量从标准坐标系表达改写为主轴坐标系表达。</p>
<p>因此可显式写出两组坐标关系：</p>
<span displaypfx="" class="mathjax-container">\[y_1=\frac{x_1+x_2}{\sqrt{2}},\quad y_2=\frac{x_1-x_2}{\sqrt{2}}\]</span>
<span displaypfx="" class="mathjax-container">\[x_1=\frac{y_1+y_2}{\sqrt{2}},\quad x_2=\frac{y_1-y_2}{\sqrt{2}}\]</span>
<p>代入标准型：</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=\mathbf{y}^\top\Lambda\mathbf{y}=3y_1^2+7y_2^2\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 用 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 展开，可直接验证“交叉项被旋转消掉”：</p>
<span displaypfx="" class="mathjax-container">\[3y_1^2+7y_2^2=\frac{3}{2}(x_1+x_2)^2+\frac{7}{2}(x_1-x_2)^2=5x_1^2-4x_1x_2+5x_2^2\]</span>
<p>数值校验：取 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(1,2)^\top\)</span>，原式为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}=17\)</span>；而 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}=Q^\top\mathbf{x}=\left(\frac{3}{\sqrt{2}},-\frac{1}{\sqrt{2}}\right)^\top\)</span>，代入 <span displaypfx="inline-" class="mathjax-container">\(3y_1^2+7y_2^2\)</span> 同样得到 17。</p>
<p>几何解释：正交矩阵 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 表示旋转/换基，把坐标轴对齐到“主轴方向”（特征向量）；对角矩阵 <span displaypfx="inline-" class="mathjax-container">\(\Lambda\)</span> 表示沿主轴的逐轴缩放（由特征值控制）。因此等值线在 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 坐标系里与轴对齐，形状由 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i\)</span> 决定。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(\Lambda\)</span> 中既有正特征值也有负特征值，则二次型是不定的（Indefinite）：沿某些方向 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 增大，沿另一些方向 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 减小。二维里它的等值线（Level Set）典型呈双曲线（Hyperbola）形状，优化里对应鞍点（Saddle Point）结构。例：令 <span displaypfx="inline-" class="mathjax-container">\(A=\begin{bmatrix}1 &amp; 2\\ 2 &amp; 1\end{bmatrix}\)</span>，其特征值为 3 与 -1，在某个正交坐标 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 下有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^\top A\mathbf{x}=3y_1^2-y_2^2\)</span>，可取正也可取负。</p>
<p>进一步允许一般可逆线性变换（不要求是旋转）时，可把对角项缩放为 <span displaypfx="inline-" class="mathjax-container">\(+1,-1,0\)</span>（Sylvester 惯性定理（Law of Inertia））：二次型被分解为若干正平方项、负平方项与零方向，其中正/负/零项的个数在合同变换下保持不变（换言之，“正方向有几个、负方向有几个、平坦方向有几个”是坐标变换改不掉的性质）；正/负项的个数也称为签名（Signature）。在优化里，它们分别对应局部最小、鞍点与平坦方向。</p>
<div class="blog_h3"><span class="graybg">二次型与机器学习（Quadratic Form in Machine Learning）</span></div>
<p>下面这些场景看起来不同，但核心都在计算“某个方向上的能量/代价”：给一个向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\)</span>，二次型 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}^\top A\mathbf{v}\)</span> 会告诉你它在矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 定义的几何里有多大、代价有多高。</p>
<ul>
<li>平方范数（Squared <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> Norm）：<span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_2^2=\mathbf{x}^\top I\mathbf{x}\)</span>。直白地说，它就是“向量长度的平方”，在训练里常作为最基础的“大小惩罚”。例如权重衰减（Weight Decay）把过大的参数拉回去，本质是在最小化 <span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_2^2\)</span> 这种二次型。</li>
<li>最小二乘与二次损失（Least Squares / MSE）：线性回归目标 <span displaypfx="inline-" class="mathjax-container">\(\|X\mathbf{w}-\mathbf{y}\|_2^2\)</span> 展开后是关于 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 的二次型。通俗理解：模型每偏一点，代价按“平方”增长，所以大误差会被更重惩罚。它的闭式解来自正规方程（Normal Equations）<span displaypfx="inline-" class="mathjax-container">\(X^\top X\mathbf{w}=X^\top\mathbf{y}\)</span>。</li>
<li>PCA（Principal Component Analysis）：对中心化（Centering）数据，方向 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\)</span> 上的方差是 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}^\top\Sigma\mathbf{u}\)</span>。这句话的直觉是：“把数据投影到某个方向后，能展开多宽”。PCA 就是在所有单位方向里找让这个二次型最大的方向（主成分），因此主成分就是“信息最密集”的方向。</li>
<li>马氏距离（Mahalanobis Distance）与高斯负对数似然（Gaussian NLL）：核心项是 <span displaypfx="inline-" class="mathjax-container">\((\mathbf{x}-\boldsymbol{\mu})^\top\Sigma^{-1}(\mathbf{x}-\boldsymbol{\mu})\)</span>。可以把它理解成“先按数据真实尺度做校正，再测距离”：方差大的方向偏离一点不算太异常，方差小的方向偏离同样大小则更异常。异常检测（Anomaly Detection）和高斯判别模型都依赖这个量。</li>
<li>二阶近似与优化曲率（Second-order Approximation / Curvature）：在参数点附近，损失变化可写成 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\Delta^\top H\Delta\)</span>。它告诉你“往哪个方向走会涨得快/慢”：特征值大表示该方向很陡，特征值小表示平坦，正负混合则是鞍点（Saddle）。这也是为什么牛顿法、预条件（Preconditioning）和学习率调度都在关心 Hessian 的谱结构。</li>
</ul>
<p>在机器学习的实现层面，二次型也常先通过可逆变量替换 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=T\mathbf{y}\)</span> 化到更易计算的表示： <span displaypfx="inline-" class="mathjax-container">\(q(\mathbf{x})=\mathbf{x}^\top A\mathbf{x}=\mathbf{y}^\top(T^\top A T)\mathbf{y}\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span>，原坐标有 <span displaypfx="inline-" class="mathjax-container">\(\nabla_{\mathbf{x}}q=2A\mathbf{x}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\nabla^2_{\mathbf{x}}q=2A\)</span>；在新坐标下 <span displaypfx="inline-" class="mathjax-container">\(\nabla_{\mathbf{y}}q=2A'\mathbf{y}\)</span>（<span displaypfx="inline-" class="mathjax-container">\(A'=T^\top A T\)</span>）。若进一步化到对角标准型 <span displaypfx="inline-" class="mathjax-container">\(A'=\Lambda\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial q}{\partial y_i}=2\lambda_i y_i\)</span>，逐坐标解耦，推导与实现都会更直接。</p>
<p>这里要区分“性质不变”和“数值不变”：在可逆变量替换下，正定/半正定/不定性质与正负零方向个数（惯性（Inertia））保持不变，因此局部最小/鞍点等优化结构不变；但一般合同变换 <span displaypfx="inline-" class="mathjax-container">\(T^\top A T\)</span> 不要求逐个保留特征值数值，只有正交相似变换 <span displaypfx="inline-" class="mathjax-container">\(Q^\top A Q\)</span> 才逐个保留特征值。</p>
<div class="blog_h2"><span class="graybg">对角矩阵（Diagonal Matrix）</span></div>
<p>对角矩阵（Diagonal Matrix）是只有对角线元素可能非零的方阵。写作 <span displaypfx="inline-" class="mathjax-container">\(D=\mathrm{diag}(d_1,\ldots,d_n)\)</span>，其非对角元素全为 0：</p>
<span displaypfx="" class="mathjax-container">\[D=\begin{bmatrix}d_1 &amp; 0 &amp; \cdots &amp; 0\\ 0 &amp; d_2 &amp; \cdots &amp; 0\\ \vdots &amp; \vdots &amp; \ddots &amp; \vdots\\ 0 &amp; 0 &amp; \cdots &amp; d_n\end{bmatrix}\]</span>
<p>对角矩阵乘以向量等价于“逐维缩放”：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x_1,\ldots,x_n)^\top\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(D\mathbf{x}=(d_1x_1,\ldots,d_nx_n)^\top\)</span>。例：令 <span displaypfx="inline-" class="mathjax-container">\(D=\mathrm{diag}(2,0.5)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(3,4)^\top\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(D\mathbf{x}=(6,2)^\top\)</span>。</p>
<p>在 AI 里，对角矩阵最常见的用途是把“逐元素缩放”写成线性算子：例如 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\odot \mathbf{s}=\mathrm{diag}(\mathbf{s})\mathbf{x}\)</span>。很多优化器的自适应学习率也可以视为对角预条件（Diagonal Preconditioner）：例如 Adam/Adagrad 里的 <span displaypfx="inline-" class="mathjax-container">\(1/\sqrt{v+\epsilon}\)</span> 本质上是按参数维度缩放梯度。</p>
<div class="blog_h2"><span class="graybg">单位矩阵（Identity Matrix）</span></div>
<p>单位矩阵（Identity Matrix）<span displaypfx="inline-" class="mathjax-container">\(I_n\)</span> 是对角线上全为 1、其他元素为 0 的方阵：</p>
<span displaypfx="" class="mathjax-container">\[I_n=\begin{bmatrix}1 &amp; 0 &amp; \cdots &amp; 0\\ 0 &amp; 1 &amp; \cdots &amp; 0\\ \vdots &amp; \vdots &amp; \ddots &amp; \vdots\\ 0 &amp; 0 &amp; \cdots &amp; 1\end{bmatrix}\]</span>
<p>它是矩阵乘法的单位元：对任意形状匹配的矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(AI=IA=A\)</span>；对任意向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(I\mathbf{x}=\mathbf{x}\)</span>。例：若 <span displaypfx="inline-" class="mathjax-container">\(I_2=\begin{bmatrix}1 &amp; 0\\ 0 &amp; 1\end{bmatrix}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(I_2(3,4)^\top=(3,4)^\top\)</span>。</p>
<p>在 AI/数值计算里，<span displaypfx="inline-" class="mathjax-container">\(A+\lambda I\)</span>（<span displaypfx="inline-" class="mathjax-container">\(\lambda>0\)</span>）用于改善条件数、提高可逆性与数值稳定性：例如岭回归（Ridge Regression）把 <span displaypfx="inline-" class="mathjax-container">\(X^\top X\)</span> 替换为 <span displaypfx="inline-" class="mathjax-container">\(X^\top X+\lambda I\)</span>；在高斯模型与协方差估计里常见 <span displaypfx="inline-" class="mathjax-container">\(\Sigma+\epsilon I\)</span> 来保证 Cholesky 分解可用。</p>
<div class="blog_h2"><span class="graybg">对称矩阵（Symmetric Matrix）</span></div>
<p>对称矩阵（Symmetric Matrix）是满足 <span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span> 的实方阵，即 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}=A_{ji}\)</span>。直观上，它的上三角与下三角互为镜像。</p>
<p>例： <span displaypfx="inline-" class="mathjax-container">\(\begin{bmatrix}2 &amp; 1\\ 1 &amp; 3\end{bmatrix}\)</span> 是对称矩阵；而 <span displaypfx="inline-" class="mathjax-container">\(\begin{bmatrix}2 &amp; 1\\ 0 &amp; 3\end{bmatrix}\)</span> 不是，因为非对角元素不成对相等。</p>
<p>对称矩阵拥有更“干净”的谱结构：所有特征值都是实数，并且可正交对角化（Spectral Theorem）：<span displaypfx="inline-" class="mathjax-container">\(A=Q\Lambda Q^\top\)</span>（<span displaypfx="inline-" class="mathjax-container">\(Q^\top Q=I\)</span>）。这使得很多推导都可以在旋转后的坐标系里逐维分析二次型、曲率与能量。</p>
<p>在 AI 里，对称矩阵高频出现于：</p>
<ul>
<li>协方差矩阵（Covariance Matrix）<span displaypfx="inline-" class="mathjax-container">\(\Sigma\)</span>：例如高斯模型与特征白化（Whitening）里，要求 <span displaypfx="inline-" class="mathjax-container">\(\Sigma\succeq 0\)</span>，并常用 <span displaypfx="inline-" class="mathjax-container">\(\Sigma+\epsilon I\)</span> 保证数值稳定。</li>
<li>Gram 矩阵（Gram Matrix）<span displaypfx="inline-" class="mathjax-container">\(X^\top X\)</span> 与核矩阵（Kernel Matrix）<span displaypfx="inline-" class="mathjax-container">\(K\)</span>：它们天然对称/半正定，是最小二乘、岭回归与核方法的核心对象。</li>
<li>海森矩阵（Hessian）：当目标函数二阶连续可导时，Hessian 对称；其特征值决定局部曲率，从而决定“极小/极大/鞍点”的类型与优化难度。</li>
</ul>
<div class="blog_h2"><span class="graybg">厄米矩阵（Hermitian Matrix）</span></div>
<p>厄米矩阵（Hermitian Matrix）是复数域上与对称矩阵对应的概念。若复矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{C}^{n\times n}\)</span> 满足</p>
<span displaypfx="" class="mathjax-container">\[A=A^\ast\]</span>
<p>则称 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 为厄米矩阵，其中 <span displaypfx="inline-" class="mathjax-container">\(A^\ast\)</span> 表示共轭转置（Conjugate Transpose）：先转置，再对每个元素取复共轭。因此，厄米矩阵满足按元素关系 <span displaypfx="inline-" class="mathjax-container">\(A_{ij}=\overline{A_{ji}}\)</span>。可以把它直接理解为<span style="background-color: #c0c0c0;">复数版本的对称矩阵</span>。</p>
<p>例： <span displaypfx="inline-" class="mathjax-container">\(\begin{bmatrix}1 &amp; 2+i\\ 2-i &amp; 3\end{bmatrix}\)</span> 是厄米矩阵，因为非对角元素互为复共轭，而对角线元素必须是实数。厄米矩阵保留了实对称矩阵最重要的好性质：特征值全为实数，并且可以被酉矩阵（Unitary Matrix）对角化。因此在复数信号处理、量子力学、复数优化与某些频域分析里，它扮演的角色与实对称矩阵在实数域中的角色完全对应。</p>
<div class="blog_h2"><span class="graybg">可逆矩阵与奇异矩阵（Invertible vs Singular）</span></div>
<p>可逆矩阵（Invertible Matrix）是存在逆矩阵的方阵：对 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{n\times n}\)</span>，若存在 <span displaypfx="inline-" class="mathjax-container">\(A^{-1}\)</span> 使得 <span displaypfx="inline-" class="mathjax-container">\(AA^{-1}=A^{-1}A=I\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 可逆；否则称 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 为奇异矩阵（Singular Matrix）。</p>
<p>等价判据（常用）：<span displaypfx="inline-" class="mathjax-container">\(A\)</span> 可逆 <span displaypfx="inline-" class="mathjax-container">\(\Leftrightarrow \det(A)\ne 0 \Leftrightarrow \mathrm{rank}(A)=n\)</span>（列向量线性无关）。</p>
<p>例（可逆）：令 <span displaypfx="inline-" class="mathjax-container">\(A=\begin{bmatrix}2 &amp; 1\\ 1 &amp; 1\end{bmatrix}\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\det(A)=1\)</span>，并且 <span displaypfx="inline-" class="mathjax-container">\(A^{-1}=\begin{bmatrix}1 &amp; -1\\ -1 &amp; 2\end{bmatrix}\)</span>。</p>
<p>例（奇异）：令 <span displaypfx="inline-" class="mathjax-container">\(B=\begin{bmatrix}1 &amp; 2\\ 2 &amp; 4\end{bmatrix}\)</span>，第二行是第一行的 2 倍，因此秩为 1、行列式为 0，无法求逆。对应线性方程组 <span displaypfx="inline-" class="mathjax-container">\(B\mathbf{x}=\mathbf{b}\)</span> 可能无解（例如 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}=(3,5)^\top\)</span>），也可能有无穷多解（例如 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{b}=(3,6)^\top\)</span>）。</p>
<p>在 AI 里，奇异性最常出现在最小二乘与协方差：当特征共线、维度远大于样本数（<span displaypfx="inline-" class="mathjax-container">\(d\gg N\)</span>）时，<span displaypfx="inline-" class="mathjax-container">\(X^\top X\)</span> 往往奇异或病态（Ill-conditioned）。常见处理是正则化（<span displaypfx="inline-" class="mathjax-container">\(X^\top X+\lambda I\)</span>）或用 SVD/QR 求解并使用伪逆（Pseudoinverse）<span displaypfx="inline-" class="mathjax-container">\(A^+\)</span>。</p>
<p>实现上通常避免显式求 <span displaypfx="inline-" class="mathjax-container">\(A^{-1}\)</span>：更稳定的做法是直接求解 <span displaypfx="inline-" class="mathjax-container">\(A\mathbf{x}=\mathbf{b}\)</span>（Solve），或用分解（Cholesky / QR / SVD）替代。</p>
<div class="blog_h2"><span class="graybg">正交矩阵（Orthogonal Matrix）</span></div>
<p>正交矩阵（Orthogonal Matrix）是满足 <span displaypfx="inline-" class="mathjax-container">\(Q^\top Q=QQ^\top=I\)</span> 的实方阵。它的列向量（或行向量）构成一组正交标准基（Orthonormal Basis），因此保持长度与点积：对任意向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(\|Q\mathbf{x}\|_2=\|\mathbf{x}\|_2\)</span>，对任意向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{a},\mathbf{b}\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\((Q\mathbf{a})^\top(Q\mathbf{b})=\mathbf{a}^\top\mathbf{b}\)</span>。</p>
<p>正交矩阵的列向量不需要“沿着坐标轴方向”。要求只有一个：列向量两两正交且都是单位向量，也就是构成一组正交标准基。标准基（<span displaypfx="inline-" class="mathjax-container">\(\mathbf{e}_1,\mathbf{e}_2,\ldots\)</span>）只是其中最常用的一组。</p>
<p>例（旋转 45°）：令</p>
<span displaypfx="" class="mathjax-container">\[Q=\frac{1}{\sqrt{2}}\begin{bmatrix}1 &amp; -1\\ 1 &amp; 1\end{bmatrix}\]</span>
<p>它的两列分别是 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{\sqrt{2}}(1,1)^\top\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{\sqrt{2}}(-1,1)^\top\)</span>，都不与坐标轴对齐，但它们正交且单位长度，因此</p>
<span displaypfx="" class="mathjax-container">\[Q^\top Q=\frac{1}{2}\begin{bmatrix}1 &amp; 1\\ -1 &amp; 1\end{bmatrix}\begin{bmatrix}1 &amp; -1\\ 1 &amp; 1\end{bmatrix}=\begin{bmatrix}1 &amp; 0\\ 0 &amp; 1\end{bmatrix}=I\]</span>
<p>例（二维旋转 90°）：令</p>
<span displaypfx="" class="mathjax-container">\[R=\begin{bmatrix}0 &amp; -1\\ 1 &amp; 0\end{bmatrix},\quad R^\top=\begin{bmatrix}0 &amp; 1\\ -1 &amp; 0\end{bmatrix}\]</span>
<p>则</p>
<span displaypfx="" class="mathjax-container">\[R^\top R=RR^\top=\begin{bmatrix}1 &amp; 0\\ 0 &amp; 1\end{bmatrix}=I\]</span>
<p>在 AI 里，正交矩阵常用于正交初始化（Orthogonal Initialization）、QR 分解与正交约束参数化；核心目的是把谱范数控制在 1 附近，改善深层网络与 RNN 的数值稳定性。</p>
<div class="blog_h2"><span class="graybg">酉矩阵（Unitary Matrix）</span></div>
<p>酉矩阵（Unitary Matrix）是复数域上的“长度保持”线性变换。对复矩阵 <span displaypfx="inline-" class="mathjax-container">\(U\in\mathbb{C}^{n\times n}\)</span>，若满足</p>
<span displaypfx="" class="mathjax-container">\[U^\ast U=UU^\ast=I\]</span>
<p>则称 <span displaypfx="inline-" class="mathjax-container">\(U\)</span> 为酉矩阵，其中 <span displaypfx="inline-" class="mathjax-container">\(U^\ast\)</span> 是共轭转置（Conjugate Transpose）。酉矩阵的列向量构成一组正交归一基（Orthonormal Basis），因此对任意向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 都有 <span displaypfx="inline-" class="mathjax-container">\(\|U\mathbf{x}\|_2=\|\mathbf{x}\|_2\)</span>。</p>
<p>实数域特例：当矩阵元素为实数时，酉矩阵退化为正交矩阵（Orthogonal Matrix），即满足 <span displaypfx="inline-" class="mathjax-container">\(Q^\top Q=QQ^\top=I\)</span>。</p>
<p>例（复数域）：令</p>
<span displaypfx="" class="mathjax-container">\[U=\begin{bmatrix}1 &amp; 0\\ 0 &amp; i\end{bmatrix},\quad U^\ast=\begin{bmatrix}1 &amp; 0\\ 0 &amp; -i\end{bmatrix}\]</span>
<p>则可直接计算：</p>
<span displaypfx="" class="mathjax-container">\[U^\ast U=\begin{bmatrix}1 &amp; 0\\ 0 &amp; -i\end{bmatrix}\begin{bmatrix}1 &amp; 0\\ 0 &amp; i\end{bmatrix}=\begin{bmatrix}1 &amp; 0\\ 0 &amp; 1\end{bmatrix}=I,\quad UU^\ast=\begin{bmatrix}1 &amp; 0\\ 0 &amp; i\end{bmatrix}\begin{bmatrix}1 &amp; 0\\ 0 &amp; -i\end{bmatrix}=I\]</span>
<p>在 AI 里，正交/酉矩阵常用于控制数值稳定性：例如正交初始化（Orthogonal Initialization）与正交/酉参数化可把谱范数压在 1 附近，缓解深层网络与 RNN 中的梯度爆炸/消失；一些长序列建模会使用 unitary/orthogonal RNN 来更好地传播长程信息。</p>
<div class="blog_h2"><span class="graybg">正定矩阵</span></div>
<p>正定矩阵（Positive Definite Matrix）把“二次型总是正”形式化。对对称矩阵（Symmetric Matrix）<span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span>，若对任意非零向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\ne\mathbf{0}\)</span> 都有</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x} > 0\]</span>
<p>则称 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 正定，记作 <span displaypfx="inline-" class="mathjax-container">\(A\succ 0\)</span>。若是 <span displaypfx="inline-" class="mathjax-container">\(\ge 0\)</span> 则为半正定（Positive Semi-Definite, PSD），记作 <span displaypfx="inline-" class="mathjax-container">\(A\succeq 0\)</span>。</p>
<p>几何上，把 <span displaypfx="inline-" class="mathjax-container">\(q(x,y)=\mathbf{x}^\top A\mathbf{x}\)</span> 画成 <span displaypfx="inline-" class="mathjax-container">\(z=q(x,y)\)</span> 的三维曲面时，正定对应“向上开口的碗”（椭圆抛物面（Elliptic Paraboloid））：原点是唯一最低点，任意非零方向都往上抬升。若没有交叉项（<span displaypfx="inline-" class="mathjax-container">\(bxy\)</span> 项为 0），等值线与坐标轴对齐；若有交叉项（<span displaypfx="inline-" class="mathjax-container">\(b\ne 0\)</span>），碗的主轴会旋转，但“向上碗”的本质不变。半正定则可能出现平坦方向（Flat Direction），典型形状是槽（Trough）而非严格碗底。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/matrix-pd-psd.png"><img class="alignnone size-full wp-image-40695" src="https://blog.gmem.cc/wp-content/uploads/2026/03/matrix-pd-psd.png" alt="matrix-pd-psd" width="100%" /></a></p>
<p>从特征值（Eigenvalues）角度看，对称矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的二次型类型可直接由特征值符号判别（下述以二维 <span displaypfx="inline-" class="mathjax-container">\(\lambda_1,\lambda_2\)</span> 为例）：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda_1>0,\lambda_2>0\)</span>：正定（Positive Definite, PD），向上开口碗。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda_1<0,\lambda_2<0[/latex]：负定（Negative Definite, ND），向下开口碗。</li>
<li>一个为 0、另一个大于 0：半正定（Positive Semi-Definite, PSD），出现平坦方向，形状更像槽。</li>
<li>一个为 0、另一个小于 0：半负定（Negative Semi-Definite, NSD），对应“倒槽”。</li>
<li>一正一负：不定（Indefinite），对应鞍面（Saddle Surface）。</li>
</ul>
<p>因此图里“有交叉项”并不改变正负定类型；它主要改变主轴方向（旋转等值线），而“是不是碗/槽/鞍”由特征值符号决定。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/matrix-nd-nsd.png"><img class="alignnone size-full wp-image-40703" src="https://blog.gmem.cc/wp-content/uploads/2026/03/matrix-nd-nsd.png" alt="matrix-nd-nsd" width="100%" /></a></p>
<p>例：令</p>
[latex]A=\begin{bmatrix}2 &amp; 1\\ 1 &amp; 2\end{bmatrix}\)</span>
<p>对任意 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x_1,x_2)^\top\)</span>，有</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{x}^\top A\mathbf{x}=2x_1^2+2x_1x_2+2x_2^2=(x_1+x_2)^2+x_1^2+x_2^2>0\quad (\mathbf{x}\ne\mathbf{0})\]</span>
<p>因此 <span displaypfx="inline-" class="mathjax-container">\(A\succ 0\)</span>。同时它的特征值为 3 与 1（均为正），并且存在 Cholesky 分解：</p>
<span displaypfx="" class="mathjax-container">\[A=LL^\top,\quad L=\begin{bmatrix}\sqrt{2} &amp; 0\\ \frac{1}{\sqrt{2}} &amp; \sqrt{\frac{3}{2}}\end{bmatrix}\]</span>
<p>等价刻画（常用）：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(A\succ 0\)</span> 当且仅当所有特征值 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i>0\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(A\succ 0\)</span> 当且仅当存在 Cholesky 分解 <span displaypfx="inline-" class="mathjax-container">\(A=LL^\top\)</span>（<span displaypfx="inline-" class="mathjax-container">\(L\)</span> 下三角且对角为正）。</li>
</ul>
<p>它在优化里非常关键：若函数的海森矩阵（Hessian）在某点正定，则该点是严格局部极小；若 Hessian 半正定，则函数局部凸（Locally Convex）。</p>
<div class="blog_h2"><span class="graybg">特征值与特征向量</span></div>
<p>特征值（Eigenvalue）与特征向量（Eigenvector）描述线性变换的“固有方向”：若存在非零向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\ne\mathbf{0}\)</span> 与标量 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 使得</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{v}=\lambda \mathbf{v}\]</span>
<p>则 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\)</span> 是特征向量，<span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是对应特征值。几何上，沿特征向量方向的向量经过变换后方向不变，只被缩放（若 <span displaypfx="inline-" class="mathjax-container">\(\lambda<0[/latex] 还会翻转）。</p>
<p>当矩阵可对角化（Diagonalizable）时，可写为 [latex syntax=inline]A=V\Lambda V^{-1}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\Lambda\)</span> 是特征值对角矩阵，列向量 <span displaypfx="inline-" class="mathjax-container">\(V=[\mathbf{v}_1,\dots,\mathbf{v}_n]\)</span> 是特征向量。</p>
<p>对称矩阵（Symmetric Matrix）是最重要的特例：它的特征向量可取为一组正交归一基（Orthonormal Basis），因此</p>
<span displaypfx="" class="mathjax-container">\[A=Q\Lambda Q^\top,\quad Q^\top Q=I\]</span>
<p>这正是 PCA（Principal Component Analysis）等方法背后的谱分解（Spectral Decomposition）基础。</p>
<div class="blog_h2"><span class="graybg">谱、谱定理与谱范数</span></div>
<div class="blog_h3"><span class="graybg">谱（Spectrum）</span></div>
<p>矩阵的谱（Spectrum）指的是矩阵全部特征值构成的集合。对方阵 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{C}^{n\times n}\)</span>，常记为 <span displaypfx="inline-" class="mathjax-container">\(\sigma(A)\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\sigma(A)=\{\lambda\in\mathbb{C}\mid \det(A-\lambda I)=0\}\]</span>
<p>这里之所以会把行列式（Determinant）扯进来，是因为特征值定义本身就会自然导向这个条件。若 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是特征值，按定义必须存在某个非零向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\ne \mathbf{0}\)</span> 使得</p>
<span displaypfx="" class="mathjax-container">\[A\mathbf{v}=\lambda \mathbf{v}\]</span>
<p>把右边移到左边，就是</p>
<span displaypfx="" class="mathjax-container">\[(A-\lambda I)\mathbf{v}=\mathbf{0}\]</span>
<p>这说明：矩阵 <span displaypfx="inline-" class="mathjax-container">\(A-\lambda I\)</span> 对应的齐次线性方程组必须有<span style="background-color: #c0c0c0;">非零解</span>。而线性代数里，一个方阵齐次方程组有非零解，当且仅当该矩阵不可逆；对方阵而言，不可逆又等价于行列式为 0。因此就得到</p>
<span displaypfx="" class="mathjax-container">\[\det(A-\lambda I)=0\]</span>
<p>也就是说，行列式在这里不是额外硬塞进来的工具，而是“特征向量存在非零解”这一要求的等价写法。解这个方程得到的多项式根，就是矩阵的全部特征值。</p>
<p>这个定义的关键不是“列出所有特征值”本身，而是把矩阵最核心的线性作用浓缩成一组标量。许多稳定性、可逆性、收敛性与几何性质，最终都可以回到谱上来判断。例：若 <span displaypfx="inline-" class="mathjax-container">\(0\in \sigma(A)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 不可逆；若所有特征值都为正，且 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 对称，则它是正定矩阵。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/spectrum.png"><img class="alignnone size-full wp-image-42063" src="https://blog.gmem.cc/wp-content/uploads/2026/03/spectrum.png" alt="spectrum" width="1024" height="1024" /></a></p>
<p>与谱紧密相关、但不能混为一谈的另一个概念是谱半径（Spectral Radius）：</p>
<span displaypfx="" class="mathjax-container">\[\rho(A)=\max_{\lambda\in\sigma(A)}|\lambda|\]</span>
<p>它只取特征值模长中的最大者。谱是整个特征值集合，谱半径只是其中的最大模长摘要。在线性迭代、RNN 稳定性与幂法（Power Method）里，谱半径尤其常见。</p>
<div class="blog_h3"><span class="graybg">谱定理（Spectral Theorem）</span></div>
<p>谱定理（Spectral Theorem）是线性代数里最重要的结构定理之一。对实对称矩阵 <span displaypfx="inline-" class="mathjax-container">\(A=A^\top\)</span>，存在正交矩阵 <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 与实对角矩阵 <span displaypfx="inline-" class="mathjax-container">\(\Lambda\)</span>，使得</p>
<span displaypfx="" class="mathjax-container">\[A=Q\Lambda Q^\top\]</span>
<p>等价地说， <span displaypfx="inline-" class="mathjax-container">\(Q\)</span> 的列向量是一组两两正交的单位特征向量， <span displaypfx="inline-" class="mathjax-container">\(\Lambda\)</span> 的对角线上放着对应特征值。这条定理的含义非常深：对称矩阵不仅“有特征值”，而且它的全部作用都可以被解释为<span style="background-color: #c0c0c0;">先换到特征向量基，再按各坐标轴独立缩放</span>。这也是为什么二次型、协方差矩阵、Hessian、图 Laplacian 等对象一旦对称，分析就会变得异常清爽。</p>
<p>在复数域上，对应版本是厄米矩阵（Hermitian Matrix）<span displaypfx="inline-" class="mathjax-container">\(A=A^\ast\)</span> 可被酉矩阵（Unitary Matrix）对角化：</p>
<span displaypfx="" class="mathjax-container">\[A=U\Lambda U^\ast\]</span>
<p>这类“可由一组正交 / 酉基完全分解”的矩阵，在数值上更稳定、几何上更透明，也正因此成为机器学习里最重要的一类矩阵对象。</p>
<div class="blog_h3"><span class="graybg">谱范数（Spectral Norm）</span></div>
<p>谱范数（Spectral Norm）记作 <span displaypfx="inline-" class="mathjax-container">\(\|A\|_2\)</span>，定义为矩阵对单位向量能造成的最大放大倍数：</p>
<span displaypfx="" class="mathjax-container">\[\|A\|_2=\max_{\|\mathbf{x}\|_2=1}\|A\mathbf{x}\|_2\]</span>
<p>这一定义直接揭示了它的几何意义：找出所有长度为 1 的输入向量，看矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 最多能把哪一个方向拉得最长。对任意矩阵，都有</p>
<span displaypfx="" class="mathjax-container">\[\|A\|_2=\sigma_{\max}(A)=\sqrt{\lambda_{\max}(A^\top A)}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\sigma_{\max}(A)\)</span> 是最大奇异值。若 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 还是实对称矩阵，则奇异值等于特征值绝对值，于是谱范数进一步化为</p>
<span displaypfx="" class="mathjax-container">\[\|A\|_2=\max_i |\lambda_i(A)|\]</span>
<p>因此，对一般矩阵要通过奇异值来理解谱范数；对对称矩阵，则可以直接通过特征值来理解。要特别区分：谱范数与谱半径在对称 / 正规矩阵上常常一致，但对一般非正规矩阵并不总相同。</p>
<p>在 AI 中，谱范数最常用于表达“局部放大能力”与 Lipschitz 常数控制。若某层线性映射的谱范数过大，它会把某些方向的扰动显著放大，进而加重训练不稳定、梯度爆炸或对抗脆弱性；这也是谱归一化（Spectral Normalization）、正交初始化与某些鲁棒优化方法反复关注它的原因。</p>
<div class="blog_h2"><span class="graybg">奇异值分解（SVD）</span></div>
<p>奇异值分解（SVD, Singular Value Decomposition）对任意矩阵 <span displaypfx="inline-" class="mathjax-container">\(A\in\mathbb{R}^{m\times n}\)</span> 都成立：</p>
<span displaypfx="" class="mathjax-container">\[A=U\Sigma V^\top\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(U\in\mathbb{R}^{m\times m}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(V\in\mathbb{R}^{n\times n}\)</span> 是正交矩阵（Orthogonal Matrix），<span displaypfx="inline-" class="mathjax-container">\(\Sigma\in\mathbb{R}^{m\times n}\)</span> 是对角（更准确说是“对角形”）矩阵，其对角线元素 <span displaypfx="inline-" class="mathjax-container">\(\sigma_1\ge\sigma_2\ge\cdots\ge 0\)</span> 为奇异值（Singular Values）。</p>
<p>几何解释非常直接：<span style="background-color: #c0c0c0;">先用 <span displaypfx="inline-" class="mathjax-container">\(V^\top\)</span> 旋转/换基，再用 <span displaypfx="inline-" class="mathjax-container">\(\Sigma\)</span> 沿坐标轴缩放，最后用 <span displaypfx="inline-" class="mathjax-container">\(U\)</span> 再旋转</span>。因此 SVD 是“旋转-缩放-旋转”的标准分解。</p>
<p>SVD 与特征值的关系要分左右两侧一起看：</p>
<ul>
<li>右奇异向量（Right Singular Vectors）是 <span displaypfx="inline-" class="mathjax-container">\(A^\top A\)</span> 的特征向量，即 <span displaypfx="inline-" class="mathjax-container">\(A^\top A\mathbf{v}_i=\sigma_i^2\mathbf{v}_i\)</span>。</li>
<li>左奇异向量（Left Singular Vectors）是 <span displaypfx="inline-" class="mathjax-container">\(AA^\top\)</span> 的特征向量，即 <span displaypfx="inline-" class="mathjax-container">\(AA^\top\mathbf{u}_i=\sigma_i^2\mathbf{u}_i\)</span>。</li>
</ul>
<p>其中<span displaypfx="inline-" class="mathjax-container">\(\sigma_i^2\)</span> 是 <span displaypfx="inline-" class="mathjax-container">\(A^\top A\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(AA^\top\)</span> 的特征值；<span displaypfx="inline-" class="mathjax-container">\(\sigma_i\)</span> 为奇异值（即这些非零特征值的平方根）。</p>
<p>矩阵形式分别是 <span displaypfx="inline-" class="mathjax-container">\(A^\top A=V\Sigma^\top\Sigma V^\top\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(AA^\top=U\Sigma\Sigma^\top U^\top\)</span>，因此非零奇异值满足 <span displaypfx="inline-" class="mathjax-container">\(\sigma_i=\sqrt{\lambda_i(A^\top A)}=\sqrt{\lambda_i(AA^\top)}\)</span>。这也解释了为什么奇异值总是非负，而一般矩阵的特征值可以为负甚至为复数。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(\sigma_i>0\)</span>，左右奇异向量还可互相对应： <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}_i=\frac{A\mathbf{v}_i}{\sigma_i}\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}_i=\frac{A^\top\mathbf{u}_i}{\sigma_i}\)</span>。</p>
<p>特殊地，若 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 是对称矩阵，则它可正交对角化，此时奇异值等于特征值的绝对值：<span displaypfx="inline-" class="mathjax-container">\(\sigma_i=|\lambda_i(A)|\)</span>；若 <span displaypfx="inline-" class="mathjax-container">\(A\succeq 0\)</span>（半正定），则 <span displaypfx="inline-" class="mathjax-container">\(\sigma_i=\lambda_i(A)\)</span>。</p>
<p>为什么 SVD 可用于压缩与降维（Compression &amp; Dimensionality Reduction）？因为很多数据/权重矩阵在有效意义下是低秩（Low-rank）的：只有前几个 <span displaypfx="inline-" class="mathjax-container">\(\sigma_i\)</span> 很大，后面的奇异值接近 0。保留前 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 项得到秩 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 近似：</p>
<span displaypfx="" class="mathjax-container">\[A_k=U_{(:,1:k)}\Sigma_{(1:k,1:k)}V_{(:,1:k)}^\top\]</span>
<p>它在 Frobenius 范数（Frobenius Norm）与谱范数（Spectral Norm）意义下都是最优的秩 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 近似（Eckart–Young 定理）：用最少的信息保留最大的能量（由奇异值平方决定）。</p>
<p>在 PCA 中，对中心化数据矩阵 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 做 SVD： <span displaypfx="inline-" class="mathjax-container">\(X=U\Sigma V^\top\)</span>，右奇异向量 <span displaypfx="inline-" class="mathjax-container">\(V\)</span> 给出主方向（Principal Directions），奇异值刻画各方向的方差贡献（Variance Explained）。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/svd.png"><img class="alignnone size-full wp-image-42053" src="https://blog.gmem.cc/wp-content/uploads/2026/03/svd.png" alt="svd" width="1024" height="1024" /></a></p>
<div class="blog_h2"><span class="graybg">范数（L0 / L1 / L2 / L∞）</span></div>
<p>范数（Norm）刻画“向量大小”。在 AI 中，它最常出现在三类地方：距离度量（Distance Metric）、正则化（Regularization）与鲁棒性约束（Robustness Constraint）。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\in\mathbb{R}^d\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(p\ge 1\)</span> 时 <span displaypfx="inline-" class="mathjax-container">\(L_p\)</span> 范数定义为</p>
<span displaypfx="" class="mathjax-container">\[\|\mathbf{x}\|_p=\left(\sum_{i=1}^{d}|x_i|^p\right)^{1/p},\quad p\ge 1\]</span>
<p>并且 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_\infty=\max_i |x_i|=\lim_{p\to\infty}\|\mathbf{x}\|_p\)</span>。</p>
<div class="blog_h3"><span class="graybg">L0 “范数”（L0 “norm”）</span></div>
<p><span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_0\)</span> 定义为非零分量的个数：</p>
<span displaypfx="" class="mathjax-container">\[\|\mathbf{x}\|_0=\#\{i\mid x_i\ne 0\}\]</span>
<p>严格来说它不是范数：例如对任意非零标量 <span displaypfx="inline-" class="mathjax-container">\(\alpha\ne 0\)</span>，有 <span displaypfx="inline-" class="mathjax-container">\(\|\alpha\mathbf{x}\|_0=\|\mathbf{x}\|_0\)</span>，不满足齐次性（Homogeneity）<span displaypfx="inline-" class="mathjax-container">\(\|\alpha\mathbf{x}\|=|\alpha|\|\mathbf{x}\|\)</span>。</p>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(3,0,-1,0)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_0=2\)</span>。</p>
<p>在 AI 里，<span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_0\)</span> 用来表达稀疏性（Sparsity）：特征选择（Feature Selection）、压缩感知（Compressed Sensing）与网络剪枝（Pruning）常以“非零个数最少”为目标。但直接优化 <span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_0\)</span> 一般是组合优化，常用 <span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_1\)</span> 或其他可优化的替代目标近似。</p>
<div class="blog_h3"><span class="graybg">L1 范数（L1 Norm）</span></div>
<span displaypfx="" class="mathjax-container">\[\|\mathbf{x}\|_1=\sum_{i=1}^{d}|x_i|\]</span>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(3,4)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_1=7\)</span>。几何上，二维 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 等值线是菱形；与 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 的圆相比，它更容易在坐标轴上产生“尖角”，对应优化时更容易把部分坐标推到 0。</p>
<p>在 AI 里，<span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 正则化是稀疏学习（Sparse Learning）的标准工具：在凸模型中会得到稀疏解（例如 Lasso）；在深度模型中也常用于诱导稀疏权重/稀疏特征、做轻量化。</p>
<div class="blog_h3"><span class="graybg">L2 范数（L2 Norm）</span></div>
<span displaypfx="" class="mathjax-container">\[\|\mathbf{x}\|_2=\sqrt{\sum_{i=1}^{d}x_i^2}\]</span>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(3,4)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_2=5\)</span>，对应欧氏距离（Euclidean Distance）。在连续优化中，<span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_2^2\)</span> 具有良好的光滑性（Smoothness），使得许多推导与数值计算更稳定。</p>
<p>在 AI 里，<span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则化（权重衰减（Weight Decay））会惩罚大权重、改善泛化并缓解病态问题；在二次目标里它也对应“加 <span displaypfx="inline-" class="mathjax-container">\(\lambda I\)</span>”的稳定化（例如岭回归）。</p>
<div class="blog_h3"><span class="graybg">L∞ 范数（L-infinity Norm）</span></div>
<span displaypfx="" class="mathjax-container">\[\|\mathbf{x}\|_\infty=\max_{i}|x_i|\]</span>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(3,0,-1,0)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{x}\|_\infty=3\)</span>。它度量的是“最大坐标幅度”。</p>
<p>在 AI 里，<span displaypfx="inline-" class="mathjax-container">\(L_\infty\)</span> 最常与鲁棒性相关：对抗样本（Adversarial Examples）中的 <span displaypfx="inline-" class="mathjax-container">\(L_\infty\)</span> 约束表示“每个像素的改动幅度不超过 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span>”；一些鲁棒优化与最坏情况界也会用 <span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_\infty\)</span> 表达最大误差。</p>
<div class="blog_h3"><span class="graybg">距离与正则化</span></div>
<p>由范数诱导的距离（Norm-induced Distance）写作 <span displaypfx="inline-" class="mathjax-container">\(d(\mathbf{x},\mathbf{y})=\|\mathbf{x}-\mathbf{y}\|\)</span>。常见对应关系：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(L_2\)</span>：欧氏距离（Euclidean Distance），<span displaypfx="inline-" class="mathjax-container">\(d_2(\mathbf{x},\mathbf{y})=\|\mathbf{x}-\mathbf{y}\|_2=\sqrt{\sum_{i=1}^{d}(x_i-y_i)^2}\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(L_1\)</span>：曼哈顿距离（Manhattan Distance），<span displaypfx="inline-" class="mathjax-container">\(d_1(\mathbf{x},\mathbf{y})=\|\mathbf{x}-\mathbf{y}\|_1=\sum_{i=1}^{d}|x_i-y_i|\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(L_\infty\)</span>：切比雪夫距离（Chebyshev Distance），<span displaypfx="inline-" class="mathjax-container">\(d_\infty(\mathbf{x},\mathbf{y})=\|\mathbf{x}-\mathbf{y}\|_\infty=\max_{i}|x_i-y_i|\)</span>。它度量的是“最坏维度”的偏差：例如 <span displaypfx="inline-" class="mathjax-container">\(d_\infty(\mathbf{x},\mathbf{y})\le \epsilon \Leftrightarrow \forall i,\ |x_i-y_i|\le \epsilon\)</span>，即每一维的误差都被同一个上界约束。直觉上，如果一次操作允许同时修改所有坐标、且每步每个坐标最多改 1，那么从 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 变到 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 的最少步数就是 <span displaypfx="inline-" class="mathjax-container">\(\max_i|x_i-y_i|\)</span>（更一般地，每步上限为 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 时步数为 <span displaypfx="inline-" class="mathjax-container">\(d_\infty(\mathbf{x},\mathbf{y})/\epsilon\)</span> 的向上取整）。</li>
</ul>
<p>关于 <span displaypfx="inline-" class="mathjax-container">\(L_0\)</span>：常见写法是 <span displaypfx="inline-" class="mathjax-container">\(d_0(\mathbf{x},\mathbf{y})=\|\mathbf{x}-\mathbf{y}\|_0=\#\{i\mid x_i\ne y_i\}\)</span>，它统计两向量在多少个坐标上不相等。本质上这是逐坐标“相等/不相等”的计数度量；当取值来自离散集合时，它对应哈明距离（Hamming Distance）。但 <span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|_0\)</span> 严格来说不是范数，因此 <span displaypfx="inline-" class="mathjax-container">\(d_0\)</span> 不属于“由范数诱导”的距离家族。</p>
<p>在学习目标中，正则化通常写成：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\theta}\ \frac{1}{m}\sum_{i=1}^{m}\ell\!\left(f_{\theta}(x^{(i)}),y^{(i)}\right)+\lambda\Omega(\theta)\]</span>
<p>式中各成分含义如下：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>：模型参数（Parameters），优化的对象。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(m\)</span>：训练样本数（Number of Samples）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\((x^{(i)},y^{(i)})\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本与标签。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f_{\theta}(x^{(i)})\)</span>：模型对第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个样本的预测。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\ell(\cdot,\cdot)\)</span>：单样本损失函数（Per-sample Loss），衡量预测与标签偏差。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{m}\sum_{i=1}^{m}\ell(\cdot)\)</span>：经验风险（Empirical Risk），即平均训练误差。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)\)</span>：正则项（Regularizer），约束参数复杂度。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span>：正则化系数（Regularization Strength），平衡“拟合训练数据”与“控制模型复杂度”。</li>
</ul>
<p>常见的 <span displaypfx="inline-" class="mathjax-container">\(\Omega(\theta)\)</span> 包括 <span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_1\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_2^2\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_0\)</span>（稀疏性目标）与 <span displaypfx="inline-" class="mathjax-container">\(\|\theta\|_\infty\)</span>（最大幅度约束）。同一个思想也可写成“约束形式”（Constraint Form）：<span displaypfx="inline-" class="mathjax-container">\(\min_\theta \frac{1}{m}\sum_i \ell(\cdot)\ \text{s.t.}\ \Omega(\theta)\le c\)</span>。惩罚系数 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 与约束半径 <span displaypfx="inline-" class="mathjax-container">\(c\)</span> 在凸优化（Convex Optimization）里可通过对偶（Duality）联系起来。</p>
<div class="blog_h1"><span class="graybg">微积分</span></div>
<div class="blog_h2"><span class="graybg">极限与连续</span></div>
<div class="blog_h3"><span class="graybg">极限的定义</span></div>
<p>极限（Limit）回答的问题是：当输入“逼近”某个值时，函数输出“逼近”什么值。它关心的是趋势而不是是否刚好取到该点。</p>
<span displaypfx="" class="mathjax-container">\[\lim_{x\to a}f(x)=L\]</span>
<p>严格定义（<span displaypfx="inline-" class="mathjax-container">\(\varepsilon-\delta\)</span> 定义）可写为：</p>
<span displaypfx="" class="mathjax-container">\[\forall \varepsilon \gt 0,\ \exists \delta \gt 0,\ \text{s.t.}\ 0 \lt |x-a| \lt \delta \Rightarrow |f(x)-L| \lt \varepsilon\]</span>
<p>工程上可把它理解为：把输入控制得足够近，输出误差就能被压到任意小。</p>
<p>在 AI 里，极限直觉用于理解“收敛（Convergence）”：例如训练步长变小后，参数更新是否趋于稳定；以及损失函数在某点附近是否可被低阶展开近似。</p>
<div class="blog_h3"><span class="graybg">连续性</span></div>
<p>连续（Continuity）可理解为“函数图像没有跳断”。在点 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 处连续的三个条件是：</p>
<ol>
<li><span displaypfx="inline-" class="mathjax-container">\(f(a)\)</span> 有定义；</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lim_{x\to a}f(x)\)</span> 存在；</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lim_{x\to a}f(x)=f(a)\)</span>。</li>
</ol>
<p>连续是可导（Differentiable）的前提之一（但连续不必然可导）。例如 <span displaypfx="inline-" class="mathjax-container">\(|x|\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(x=0\)</span> 连续但不可导。</p>
<p>在优化里，连续性保证“小步更新不会导致目标突变”，这也是学习率（Learning Rate）可调与训练可控的基础假设之一。</p>
<div class="blog_h3"><span class="graybg">无穷小与无穷大</span></div>
<p>无穷小（Infinitesimal）描述“趋近于 0 的量”，无穷大（Infinity）描述“无界增长”。在推导里常通过渐近记号（Asymptotic Notation）表达量级关系：</p>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(o\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(O\)</span> 不是变量，而是两种记号：小 <span displaypfx="inline-" class="mathjax-container">\(o\)</span>（little-o）表示“严格更小一个量级”，大 <span displaypfx="inline-" class="mathjax-container">\(O\)</span>（big-O）表示“至多同量级的上界”。</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(f(x)=o(g(x))\)</span>：当 <span displaypfx="inline-" class="mathjax-container">\(x\to a\)</span> 时 <span displaypfx="inline-" class="mathjax-container">\(f/g\to 0\)</span>（高阶小量，增长/衰减速度严格慢于 <span displaypfx="inline-" class="mathjax-container">\(g\)</span>）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(x)=O(g(x))\)</span>：存在常数 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 使 <span displaypfx="inline-" class="mathjax-container">\(|f(x)|\le C|g(x)|\)</span>（同阶或更小的上界）。</li>
</ul>
<p>例如一阶 Taylor 展开里的 <span displaypfx="inline-" class="mathjax-container">\(o(\Delta x)\)</span> 表示“比 <span displaypfx="inline-" class="mathjax-container">\(\Delta x\)</span> 更小得多”的误差项。算法分析中的时间复杂度 <span displaypfx="inline-" class="mathjax-container">\(O(Nd)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(O(N^2)\)</span> 也属于同一套量级语言。</p>
<div class="blog_h2"><span class="graybg">常见求导法则与公式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">法则</td>
<td style="text-align: center;">公式</td>
<td style="text-align: center;">条件</td>
</tr>
</thead>
<tbody>
<tr>
<td>常数倍法则</td>
<td><span displaypfx="inline-" class="mathjax-container">\((Cu)' = C u'\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(C\)</span> 为常数</td>
</tr>
<tr>
<td>和差法则</td>
<td><span displaypfx="inline-" class="mathjax-container">\((u \pm v)' = u' \pm v'\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(u,v\)</span> 可导</td>
</tr>
<tr>
<td>乘法法则</td>
<td><span displaypfx="inline-" class="mathjax-container">\((uv)' = u'v + uv'\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(u,v\)</span> 可导</td>
</tr>
<tr>
<td>除法法则</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\left(\frac{u}{v}\right)'=\frac{u'v-uv'}{v^2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(u,v\)</span> 可导，且 <span displaypfx="inline-" class="mathjax-container">\(v \ne 0\)</span></td>
</tr>
<tr>
<td>链式法则</td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{d}{dx}f(g(x)) = f'(g(x))g'(x)\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(f,g\)</span> 可导</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">常见函数导数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">函数 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span></td>
<td style="text-align: center;">导数 <span displaypfx="inline-" class="mathjax-container">\(f'(x)\)</span></td>
<td style="text-align: center;">备注/条件</td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(c\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(0\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(c\)</span> 为常数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x^n\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(n x^{n-1}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(n\)</span> 为常数；非整数 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 时需注意实数域定义域</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\sqrt{x}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2\sqrt{x}}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x>0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(e^x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^x\)</span></td>
<td>自然指数（Natural Exponential）</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(a^x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a^x\ln a\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(a>0,a\ne 1\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{x}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x>0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\log_a x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{x\ln a}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x>0,\ a>0,\ a\ne 1\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\cos x\)</span></td>
<td> </td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\cos x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(-\sin x\)</span></td>
<td> </td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\tan x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sec^2 x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\cos x\ne 0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(|x|\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\mathrm{sign}(x)\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x>0\)</span> 时导数为 <span displaypfx="inline-" class="mathjax-container">\(1\)</span></p>
<p><span displaypfx="inline-" class="mathjax-container">\(x<0[/latex] 时导数为 [latex syntax=inline]-1[/latex]</p>
<p>在 [latex syntax=inline]x=0\)</span> 不可导（优化中常用次梯度 <span displaypfx="inline-" class="mathjax-container">\(g\in[-1,1]\)</span>）</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">导数（Derivative）</span></div>
<p>导数是点 <span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 处的瞬时变化率（Instantaneous Rate of Change）：</p>
<span displaypfx="" class="mathjax-container">\[f'(x_0)=\lim_{\Delta x\to 0}\frac{f(x_0+\Delta x)-f(x_0)}{\Delta x}\]</span>
<p><span displaypfx="inline-" class="mathjax-container">\(x_0\)</span> 不是“很小的数”，而是定义域中的固定位置；趋近于 0 的小量是 <span displaypfx="inline-" class="mathjax-container">\(\Delta x\)</span>（或记作 <span displaypfx="inline-" class="mathjax-container">\(dx\)</span>）。</p>
<p>一阶展开把导数解释为“线性项的系数”：当 <span displaypfx="inline-" class="mathjax-container">\(\Delta x\to 0\)</span> 时，</p>
<span displaypfx="" class="mathjax-container">\[f(x_0+\Delta x)=f(x_0)+f'(x_0)\Delta x+o(\Delta x)\]</span>
<p>可按“等于三项相加”理解：左边 <span displaypfx="inline-" class="mathjax-container">\(f(x_0+\Delta x)\)</span> 是扰动后的真实函数值；右边第一项 <span displaypfx="inline-" class="mathjax-container">\(f(x_0)\)</span> 是基点函数值，第二项 <span displaypfx="inline-" class="mathjax-container">\(f'(x_0)\Delta x\)</span> 是一阶线性近似，第三项 <span displaypfx="inline-" class="mathjax-container">\(o(\Delta x)\)</span> 是比 <span displaypfx="inline-" class="mathjax-container">\(\Delta x\)</span> 更小的高阶余项（High-order Remainder）。</p>
<div class="blog_h2"><span class="graybg">中值定理（Mean Value Theorems）</span></div>
<p>中值定理（Mean Value Theorems）是一组把“区间上的平均变化”与“某一点的瞬时变化率”连接起来的核心定理。它们在形式上不同，但共同结构是：在闭区间连续、在开区间可导，进而保证存在某个中间点 <span displaypfx="inline-" class="mathjax-container">\(c\in(a,b)\)</span> 使得斜率关系成立。</p>
<div class="blog_h3"><span class="graybg">罗尔中值定理</span></div>
<p>设 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\([a,b]\)</span> 上连续、在 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 上可导，且 <span displaypfx="inline-" class="mathjax-container">\(f(a)=f(b)\)</span>。则存在 <span displaypfx="inline-" class="mathjax-container">\(c\in(a,b)\)</span> 使</p>
<span displaypfx="" class="mathjax-container">\[f'(c)=0\]</span>
<p>几何直觉：若曲线两端高度相同，那么中间至少有一点的切线是水平的。</p>
<div class="blog_h3"><span class="graybg">拉格朗日中值定理</span></div>
<p>设 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\([a,b]\)</span> 上连续、在 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 上可导。则存在 <span displaypfx="inline-" class="mathjax-container">\(c\in(a,b)\)</span> 使</p>
<span displaypfx="" class="mathjax-container">\[f'(c)=\frac{f(b)-f(a)}{b-a}\]</span>
<p>几何直觉：区间割线斜率（平均变化率）等于某个点的切线斜率（瞬时变化率）。</p>
<div class="blog_h3"><span class="graybg">柯西中值定理</span></div>
<p>设 <span displaypfx="inline-" class="mathjax-container">\(f,g\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\([a,b]\)</span> 上连续、在 <span displaypfx="inline-" class="mathjax-container">\((a,b)\)</span> 上可导。则存在 <span displaypfx="inline-" class="mathjax-container">\(c\in(a,b)\)</span> 使</p>
<span displaypfx="" class="mathjax-container">\[(f(b)-f(a))g'(c)=(g(b)-g(a))f'(c)\]</span>
<p>当分母不为 0 时，可写成比值形式：</p>
<span displaypfx="" class="mathjax-container">\[\frac{f'(c)}{g'(c)}=\frac{f(b)-f(a)}{g(b)-g(a)}\]</span>
<p>它把“单函数斜率比较”推广为“两个函数变化率的比较”，是洛必达法则（L'Hospital's Rule）证明链中的关键步骤。</p>
<div class="blog_h3"><span class="graybg">三类中值定理的区别</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">定理</td>
<td style="text-align: center;">涉及函数</td>
<td style="text-align: center;">额外条件</td>
<td style="text-align: center; width: 40%;">结论形式</td>
<td style="text-align: center;">关系</td>
</tr>
</thead>
<tbody>
<tr>
<td>罗尔中值定理</td>
<td>1 个函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(f(a)=f(b)\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(f'(c)=0\)</span></td>
<td>拉格朗日定理的特殊情形</td>
</tr>
<tr>
<td>拉格朗日中值定理</td>
<td>1 个函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span></td>
<td>无额外端点相等条件</td>
<td><span displaypfx="inline-" class="mathjax-container">\(f'(c)=\frac{f(b)-f(a)}{b-a}\)</span></td>
<td>柯西定理取 <span displaypfx="inline-" class="mathjax-container">\(g(x)=x\)</span> 的特例</td>
</tr>
<tr>
<td>柯西中值定理</td>
<td>2 个函数 <span displaypfx="inline-" class="mathjax-container">\(f,g\)</span></td>
<td>两个函数都满足连续+可导</td>
<td><span displaypfx="inline-" class="mathjax-container">\((f(b)-f(a))g'(c)=(g(b)-g(a))f'(c)\)</span></td>
<td>三者中最一般形式</td>
</tr>
</tbody>
</table>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/mean-value-theorems.png"><img class="alignnone size-full wp-image-40785" src="https://blog.gmem.cc/wp-content/uploads/2026/03/mean-value-theorems.png" alt="mean-value-theorems" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">中值定理和AI</span></div>
<p>在 AI 系统中，中值定理主要作为理论工具出现：工程代码很少直接“调用定理”，但大量优化、稳定性与误差分析都在使用它的核心变形，即把“两个点的函数值差”转写为“某个中间点的导数（梯度）信息”。</p>
<ul>
<li>优化收敛分析（Optimization Convergence）：训练中常关心一步更新后损失变化 <span displaypfx="inline-" class="mathjax-container">\(L(\theta+\Delta)-L(\theta)\)</span>。中值定理把它连接到中间点的梯度，进而用于学习率条件与下降性证明。</li>
<li>Lipschitz 界与鲁棒性（Lipschitz Bound &amp; Robustness）：中值定理给出“输入小扰动导致输出变化”的上界形式。若梯度有界，则输出变化可控；这正是对抗鲁棒性分析和梯度惩罚（Gradient Penalty）类方法的数学基础之一。</li>
<li>有限差分与梯度估计（Finite Difference / Gradient Estimation）：割线斜率与切线斜率之间的关系来自拉格朗日中值定理，是数值优化里差分近似、线搜索（Line Search）和误差估计的核心依据。</li>
</ul>
<p>从“纯数学”到“AI 实践”的桥梁可以一句话概括：<span style="background-color: #c0c0c0;">中值定理把宏观变化量（函数值差）变成可优化的微观信息（导数/梯度）</span>。</p>
<div class="blog_h2"><span class="graybg">偏导数（Partial Derivative）</span></div>
<p>对多元函数 <span displaypfx="inline-" class="mathjax-container">\(f:\mathbb{R}^n\to\mathbb{R}\)</span>，偏导数把“只沿某一坐标轴方向的导数”形式化：固定其余变量，只让第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个变量变化。先写成最直接的极限定义：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial f}{\partial x_i}(x_0)=\lim_{h\to 0}\frac{f(x_{0,1},\dots,x_{0,i-1},x_{0,i}+h,x_{0,i+1},\dots,x_{0,n})-f(x_0)}{h}\]</span>
<p>这里的 <span displaypfx="inline-" class="mathjax-container">\(h\)</span> 是“第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个变量的微小增量”（Increment）：只加在第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个坐标上，其他坐标保持不变。</p>
<p>在线性代数记号下，上式等价写成（更紧凑）：令 <span displaypfx="inline-" class="mathjax-container">\(e_i\)</span> 为第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个标准基向量（Standard Basis Vector），则</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial f}{\partial x_i}(x_0)=\lim_{h\to 0}\frac{f(x_0+h e_i)-f(x_0)}{h}\]</span>
<p>更常用的导数视角（计算视角）是：<span style="background-color: #c0c0c0;">把其余变量暂时当常数，把目标变量当自变量，直接套一元求导法则</span>。也就是说，极限定义负责“定义正确性”，日常计算通常用求导规则完成。</p>
<p>例如 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=x^2y\)</span>：对 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 求偏导时把 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 视作常数，得 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial f}{\partial x}=2xy\)</span>；对 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 求偏导时把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 视作常数，得 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial f}{\partial y}=x^2\)</span>。</p>
<p>切换为极限视角，令 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=x^2y\)</span>，在点 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial f}{\partial x}(1,2)=\lim_{h\to 0}\frac{f(1+h,2)-f(1,2)}{h}\]</span>
<span displaypfx="" class="mathjax-container">\[=\lim_{h\to 0}\frac{2(1+h)^2-2}{h}\]</span>
<span displaypfx="" class="mathjax-container">\[=\lim_{h\to 0}(4+2h)=4\]</span>
<span displaypfx="" class="mathjax-container">\[\frac{\partial f}{\partial y}(1,2)=\lim_{h\to 0}\frac{f(1,2+h)-f(1,2)}{h}\]</span>
<span displaypfx="" class="mathjax-container">\[=\lim_{h\to 0}\frac{(2+h)-2}{h}=1\]</span>
<p>可以看到：求 <span displaypfx="inline-" class="mathjax-container">\(\partial f/\partial x\)</span> 时把 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 当常数；求 <span displaypfx="inline-" class="mathjax-container">\(\partial f/\partial y\)</span> 时把 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 当常数。这正是“其余变量固定”的具体计算含义。</p>
<p>梯度（Gradient）就是把所有偏导数按坐标收集成向量： <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x)=(\partial f/\partial x_1,\dots,\partial f/\partial x_n)^\top\)</span>。</p>
<div class="blog_h2"><span class="graybg">微分（Differential）</span></div>
<p>在一元情形中，微分是“把函数增量线性化”的记号：给定一个小增量 <span displaypfx="inline-" class="mathjax-container">\(dx\)</span>，定义</p>
<span displaypfx="" class="mathjax-container">\[df\big|_{x_0}=f'(x_0)\,dx\]</span>
<p>此时真实增量满足 <span displaypfx="inline-" class="mathjax-container">\(\Delta f = df + o(dx)\)</span>。给定 <span displaypfx="inline-" class="mathjax-container">\(dx\)</span> 后， <span displaypfx="inline-" class="mathjax-container">\(df\)</span> 才对应一个确定数值。</p>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(f(x)=x^2\)</span>，在 <span displaypfx="inline-" class="mathjax-container">\(x_0=3\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(dx=0.1\)</span> 时， <span displaypfx="inline-" class="mathjax-container">\(df=2x_0dx=0.6\)</span>，而 <span displaypfx="inline-" class="mathjax-container">\(\Delta f=f(3.1)-f(3)=0.61\)</span>。微分给出一阶近似，误差来自高阶项。</p>
<div class="blog_h2"><span class="graybg">全微分（Total Differential）</span></div>
<p>对多元函数 <span displaypfx="inline-" class="mathjax-container">\(f(x_1,\dots,x_n)\)</span>，全微分把“多方向小扰动下的一阶线性响应”写成：</p>
<span displaypfx="" class="mathjax-container">\[df=\sum_{i=1}^n \frac{\partial f}{\partial x_i}dx_i\]</span>
<p>令 <span displaypfx="inline-" class="mathjax-container">\(d\mathbf{x}=(dx_1,\dots,dx_n)^\top\)</span>，则向量形式是：</p>
<span displaypfx="" class="mathjax-container">\[df=(\nabla f(x))^\top d\mathbf{x}\]</span>
<p>若进一步把扰动分解为“方向 + 步长”： <span displaypfx="inline-" class="mathjax-container">\(d\mathbf{x}=\mathbf{u}\,ds\)</span>（<span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{u}\|_2=1\)</span>），则</p>
<span displaypfx="" class="mathjax-container">\[\frac{df}{ds}=\nabla f(x)\cdot \mathbf{u}\]</span>
<p>右侧就是方向导数（Directional Derivative）：全微分给出任意小位移的一阶近似，方向导数则把位移约束在单位方向并除去步长。</p>
<p>例：令 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=x^2y\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial f}{\partial x}=2xy\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\frac{\partial f}{\partial y}=x^2\)</span>。在点 <span displaypfx="inline-" class="mathjax-container">\((x_0,y_0)=(1,2)\)</span>，若 <span displaypfx="inline-" class="mathjax-container">\(dx=0.1\)</span>、<span displaypfx="inline-" class="mathjax-container">\(dy=-0.05\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(df=2xy\,dx+x^2dy=2\cdot1\cdot2\cdot0.1+1\cdot(-0.05)=0.35\)</span>，给出 <span displaypfx="inline-" class="mathjax-container">\(\Delta f\)</span> 的一阶近似。</p>
<div class="blog_h2"><span class="graybg">向量微积分</span></div>
<p>Nabla 算子（Nabla Operator）记作 <span displaypfx="inline-" class="mathjax-container">\(\nabla\)</span>，本质上是把偏导算子按坐标排列成的“向量形式”：</p>
<span displaypfx="" class="mathjax-container">\[\nabla=\left(\frac{\partial}{\partial x_1},\dots,\frac{\partial}{\partial x_n}\right)^\top\]</span>
<p>它本身不是一个数，而是一个算子（Operator）。作用在标量场上得到梯度（Gradient）；与向量场点乘得到散度（Divergence）；与向量场叉乘得到旋度（Curl）；对标量场再取散度得到拉普拉斯（Laplacian）。</p>
<p>符号 <span displaypfx="inline-" class="mathjax-container">\(\|\cdot\|\)</span>（双竖线）表示范数（Norm）。因此 <span displaypfx="inline-" class="mathjax-container">\(\|\nabla f(x)\|\)</span> 是梯度向量的长度；而 <span displaypfx="inline-" class="mathjax-container">\(\|\nabla\|\)</span> 作为“算子范数”在工程推导中很少直接使用，通常需要明确它作用的函数空间与范数定义。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/nabla.jpg"><img class="alignnone size-full wp-image-40609" src="https://blog.gmem.cc/wp-content/uploads/2026/03/nabla.jpg" alt="nabla" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">梯度</span></div>
<p>梯度（Gradient）把标量函数 <span displaypfx="inline-" class="mathjax-container">\(f:\mathbb{R}^n\to\mathbb{R}\)</span> 的局部变化率组织成向量：</p>
<span displaypfx="" class="mathjax-container">\[\nabla f(x)=\left(\frac{\partial f}{\partial x_1},\dots,\frac{\partial f}{\partial x_n}\right)^\top\]</span>
<p>它指向函数增长最快的方向（Steepest Ascent Direction），模长刻画该方向上的最大斜率。在优化里，梯度下降（Gradient Descent）沿 <span displaypfx="inline-" class="mathjax-container">\(-\nabla f\)</span> 走，是因为这是一阶近似下最快下降方向。</p>
<p>例： <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=x^2+2y^2\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\nabla f=(2x,4y)\)</span>，在点 <span displaypfx="inline-" class="mathjax-container">\((1,1)\)</span> 处梯度为 <span displaypfx="inline-" class="mathjax-container">\((2,4)\)</span>，表示沿 y 方向的增长更陡。</p>
<p>方向导数（Directional Derivative）把“沿某个方向的变化率”写成点积：若 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\)</span> 是单位方向向量，则</p>
<span displaypfx="" class="mathjax-container">\[D_{\mathbf{u}}f(x)=\nabla f(x)\cdot \mathbf{u}\]</span>
<p>符号先约定： <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是当前点， <span displaypfx="inline-" class="mathjax-container">\(\mathbf{u}\)</span> 是单位方向（<span displaypfx="inline-" class="mathjax-container">\(\|\mathbf{u}\|_2=1\)</span>）， <span displaypfx="inline-" class="mathjax-container">\(\varepsilon\)</span> 是沿该方向的步长（可正可负的小实数）。方向导数定义为</p>
<span displaypfx="" class="mathjax-container">\[D_{\mathbf{u}}f(x)=\lim_{\varepsilon\to 0}\frac{f(x+\varepsilon\mathbf{u})-f(x)}{\varepsilon}\]</span>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(f(x+\varepsilon\mathbf{u})\)</span> 做一阶 Taylor 展开：</p>
<span displaypfx="" class="mathjax-container">\[f(x+\varepsilon\mathbf{u})=f(x)+\varepsilon\,\nabla f(x)\cdot\mathbf{u}+o(\varepsilon)\]</span>
<p>代回定义式：</p>
<span displaypfx="" class="mathjax-container">\[\frac{f(x+\varepsilon\mathbf{u})-f(x)}{\varepsilon}\]</span>
<span displaypfx="" class="mathjax-container">\[=\nabla f(x)\cdot\mathbf{u}+\frac{o(\varepsilon)}{\varepsilon}\]</span>
<p>令 <span displaypfx="inline-" class="mathjax-container">\(\varepsilon\to 0\)</span>，由于 <span displaypfx="inline-" class="mathjax-container">\(o(\varepsilon)/\varepsilon\to 0\)</span>，得到 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathbf{u}}f(x)=\nabla f(x)\cdot\mathbf{u}\)</span>。</p>
<div class="blog_h3"><span class="graybg">散度</span></div>
<p>场（Field）是“把空间中每个点映射到一个量”的函数。标量场（Scalar Field）<span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 在每个点输出一个标量；向量场（Vector Field）<span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}(x)\)</span> 在每个点输出一个向量。梯度作用在标量场上，散度/旋度作用在向量场上。</p>
<p>散度（Divergence）作用在向量场 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}=(F_1,\dots,F_n)^\top:\mathbb{R}^n\to\mathbb{R}^n\)</span> 上。这里先把两个术语说清楚：</p>
<span displaypfx="" class="mathjax-container">\[J_{\mathbf{F}}(x)=\left[\frac{\partial F_i}{\partial x_j}\right]_{i,j=1}^n\]</span>
<p>上式是雅可比矩阵（Jacobian）：第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 行第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 列表示 <span displaypfx="inline-" class="mathjax-container">\(F_i\)</span> 对 <span displaypfx="inline-" class="mathjax-container">\(x_j\)</span> 的偏导。迹（Trace）是矩阵对角线元素之和，因此</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{tr}(J_{\mathbf{F}})=\sum_{i=1}^n \frac{\partial F_i}{\partial x_i}\]</span>
<p>而散度按定义正是这条和式，所以“散度 = Jacobian 的迹”：</p>
<span displaypfx="" class="mathjax-container">\[\nabla\cdot \mathbf{F}(x)=\mathrm{tr}(J_{\mathbf{F}}(x))=\sum_{i=1}^n \frac{\partial F_i}{\partial x_i}\]</span>
<p>几何/物理直觉：把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}\)</span> 想成“流体速度场（Velocity Field）”，散度衡量一个点附近是否像“源（Source）/汇（Sink）”——单位体积的净流出率。散度为正表示净流出，为负表示净流入。</p>
<p>例：二维场 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}(x,y)=(x,y)\)</span> 的 Jacobian 是 <span displaypfx="inline-" class="mathjax-container">\(\begin{bmatrix}1&amp;0\\0&amp;1\end{bmatrix}\)</span>，其迹为 <span displaypfx="inline-" class="mathjax-container">\(1+1=2\)</span>，所以散度也等于 2，在任意点都表现为均匀“发散”。</p>
<div class="blog_h3"><span class="graybg">旋度</span></div>
<p>旋度（Curl）衡量向量场的局部旋转（Local Rotation）。在三维中：</p>
<span displaypfx="" class="mathjax-container">\[\nabla\times \mathbf{F}=\left(\frac{\partial F_3}{\partial y}-\frac{\partial F_2}{\partial z},\ \frac{\partial F_1}{\partial z}-\frac{\partial F_3}{\partial x},\ \frac{\partial F_2}{\partial x}-\frac{\partial F_1}{\partial y}\right)\]</span>
<p>直觉上，可把它理解为“放一个很小的桨轮（Paddle Wheel）在该点附近，桨轮是否会被带着转”。保守场（Conservative Field）满足 <span displaypfx="inline-" class="mathjax-container">\(\nabla\times\mathbf{F}=\mathbf{0}\)</span>，这与“路径无关”/“存在势函数”是等价刻画。</p>
<p>例：二维旋转场 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}(x,y)=(-y,x,0)\)</span> 的旋度为 <span displaypfx="inline-" class="mathjax-container">\((0,0,2)\)</span>，表示绕 z 轴的均匀旋转趋势。</p>
<p>拉普拉斯算子（Laplacian）作用在标量场上，定义为梯度的散度：</p>
<span displaypfx="" class="mathjax-container">\[\nabla^2 f=\nabla\cdot(\nabla f)=\sum_{i=1}^n \frac{\partial^2 f}{\partial x_i^2}\]</span>
<p>它常出现在扩散（Diffusion）与平滑（Smoothing）问题中，也常被用作“曲率/粗糙度”的度量。例： <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=x^2+y^2\)</span> 则 <span displaypfx="inline-" class="mathjax-container">\(\nabla^2 f=2+2=4\)</span>。</p>
<div class="blog_h2"><span class="graybg">Jacobian 矩阵</span></div>
<p>当函数的输出不再是一个标量，而是一个向量时，梯度这个概念就不够用了。此时需要把“每个输出分量对每个输入变量的一阶变化率”统一收集起来，这个对象就是 Jacobian 矩阵（Jacobian Matrix）。它是一阶导数在向量值函数上的自然推广，也是 Hessian 的直接前置概念。</p>
<p>设向量值函数</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{F}:\mathbb{R}^n\to\mathbb{R}^m,\qquad \mathbf{F}(\mathbf{x})=\begin{bmatrix}F_1(\mathbf{x})\\ \vdots\\ F_m(\mathbf{x})\end{bmatrix}\]</span>
<p>其中输入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x_1,\dots,x_n)^\top\)</span>，输出 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}(\mathbf{x})\)</span> 有 <span displaypfx="inline-" class="mathjax-container">\(m\)</span> 个分量。Jacobian 矩阵定义为：</p>
<span displaypfx="" class="mathjax-container">\[J_{\mathbf{F}}(\mathbf{x})=\left[\frac{\partial F_i}{\partial x_j}(\mathbf{x})\right]_{i=1,\dots,m;\,j=1,\dots,n}\]</span>
<p>按矩阵展开，就是一个 <span displaypfx="inline-" class="mathjax-container">\(m\times n\)</span> 矩阵：</p>
<span displaypfx="" class="mathjax-container">\[J_{\mathbf{F}}(\mathbf{x})= \begin{bmatrix} \frac{\partial F_1}{\partial x_1} &amp; \frac{\partial F_1}{\partial x_2} &amp; \cdots &amp; \frac{\partial F_1}{\partial x_n}\\ \frac{\partial F_2}{\partial x_1} &amp; \frac{\partial F_2}{\partial x_2} &amp; \cdots &amp; \frac{\partial F_2}{\partial x_n}\\ \vdots &amp; \vdots &amp; \ddots &amp; \vdots\\ \frac{\partial F_m}{\partial x_1} &amp; \frac{\partial F_m}{\partial x_2} &amp; \cdots &amp; \frac{\partial F_m}{\partial x_n} \end{bmatrix}\]</span>
<p>这里第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 行第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 列的元素 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial F_i}{\partial x_j}\)</span> 表示：第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个输入变量发生微小变化时，第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个输出分量会怎样一阶变化。按矩阵读法，<span style="background-color: #c0c0c0;">行对应输出分量，列对应输入变量</span>，因此 Jacobian 本质上是一张局部灵敏度表（Local Sensitivity Table）。</p>
<p>它描述的不是全局非线性结构，而是函数在某个点附近的局部线性映射：输入空间里的一个小扰动向量 <span displaypfx="inline-" class="mathjax-container">\(d\mathbf{x}\)</span> 经过 Jacobian 作用后，变成输出空间中的一阶近似扰动 <span displaypfx="inline-" class="mathjax-container">\(d\mathbf{F}\)</span>。因此，Jacobian 可以理解为“该点附近最能代表原函数的一阶线性算子”。</p>
<p>它与全微分的关系最直接。若在点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 附近加入一个小扰动 <span displaypfx="inline-" class="mathjax-container">\(d\mathbf{x}\)</span>，则输出的一阶变化满足：</p>
<span displaypfx="" class="mathjax-container">\[d\mathbf{F}\approx J_{\mathbf{F}}(\mathbf{x})\,d\mathbf{x}\]</span>
<p>这条式子就是向量值函数的一阶线性化。也就是说，Jacobian 在局部扮演的角色，类似于一元函数里的导数：它给出“最好的线性近似”。若输出是一维，即 <span displaypfx="inline-" class="mathjax-container">\(m=1\)</span>，Jacobian 就退化成一个 <span displaypfx="inline-" class="mathjax-container">\(1\times n\)</span> 的行向量；它与梯度本质上包含同一组偏导数，只是排布约定可能不同。</p>
<p>一个二维到二维的例子最容易看清。设</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{F}(x,y)=\begin{bmatrix}x^2y\\ x+y^2\end{bmatrix}\]</span>
<p>则第一行来自分量函数 <span displaypfx="inline-" class="mathjax-container">\(F_1(x,y)=x^2y\)</span>，第二行来自 <span displaypfx="inline-" class="mathjax-container">\(F_2(x,y)=x+y^2\)</span>。逐项求偏导可得：</p>
<span displaypfx="" class="mathjax-container">\[J_{\mathbf{F}}(x,y)= \begin{bmatrix} 2xy &amp; x^2\\ 1 &amp; 2y \end{bmatrix}\]</span>
<p>例如在点 <span displaypfx="inline-" class="mathjax-container">\((1,2)\)</span>，Jacobian 为</p>
<span displaypfx="" class="mathjax-container">\[J_{\mathbf{F}}(1,2)= \begin{bmatrix} 4 &amp; 1\\ 1 &amp; 4 \end{bmatrix}\]</span>
<p>这表示：在该点附近，若输入发生微小变化 <span displaypfx="inline-" class="mathjax-container">\((dx,dy)^\top\)</span>，则输出变化近似为</p>
<span displaypfx="" class="mathjax-container">\[d\mathbf{F}\approx \begin{bmatrix} 4 &amp; 1\\ 1 &amp; 4 \end{bmatrix} \begin{bmatrix} dx\\ dy \end{bmatrix}\]</span>
<p>第一行说明输出第一分量大致按 <span displaypfx="inline-" class="mathjax-container">\(4dx+dy\)</span> 变化，第二行说明输出第二分量大致按 <span displaypfx="inline-" class="mathjax-container">\(dx+4dy\)</span> 变化。Jacobian 因而可以被理解为局部灵敏度矩阵（Sensitivity Matrix）。</p>
<p>对标量函数 <span displaypfx="inline-" class="mathjax-container">\(f:\mathbb{R}^n\to\mathbb{R}\)</span>，梯度 <span displaypfx="inline-" class="mathjax-container">\(\nabla f:\mathbb{R}^n\to\mathbb{R}^n\)</span> 本身就是一个向量值函数，因此 Hessian 可以直接看成梯度映射的 Jacobian：</p>
<span displaypfx="" class="mathjax-container">\[\nabla^2 f(\mathbf{x})=J_{\nabla f}(\mathbf{x})\]</span>
<p>这就是把 Jacobian 放在 Hessian 前面的原因：先理解“向量值函数的一阶导数如何组织成矩阵”，再看“标量函数的梯度再求一次导”，Hessian 的结构就不会显得突兀。</p>
<p>在机器学习中，Jacobian 最常出现在三类地方：</p>
<ol>
<li>反向传播（Backpropagation）：本质上是在链式法则（Chain Rule）下逐层组合这些局部 Jacobian。前一层输出的微小变化如何传到后一层，正是由对应层的 Jacobian 决定。</li>
<li>向量场分析：散度（Divergence）等于 Jacobian 的迹（Trace）。</li>
<li>生成模型、正则化与鲁棒性分析：常关心 Jacobian 的谱范数（Spectral Norm）或 Frobenius 范数，以度量局部放大效应。</li>
</ol>
<p>若输出维度和输入维度都很大，显式构造完整 Jacobian 也会很贵，因此工程上常直接计算 Jacobian-Vector Product 或 Vector-Jacobian Product，而不是把整块矩阵完全展开。</p>
<p>在神经网络语境里，Jacobian 到底“针对激活值还是针对权重”，取决于谁被当作自变量。若研究层与层之间的信号传播，常写成 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial h^{(l+1)}}{\partial h^{(l)}}\)</span>，这时它针对的是激活值（Activation）或层表示；若研究模型输出对输入的敏感度，可写成 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial y}{\partial x}\)</span>；若研究输出对参数的敏感度，也可以写成 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial y}{\partial \theta}\)</span>。因此，Jacobian 的本质是“一个向量怎样对另一个向量发生一阶变化”，并不预先限定变量一定是激活值还是权重。</p>
<div class="blog_h2"><span class="graybg">Hessian 矩阵</span></div>
<p>梯度（Gradient）回答的是“函数沿各坐标方向的一阶变化率”；Hessian 矩阵（Hessian Matrix）进一步回答的是“这些一阶变化率本身又在怎样变化”。这一点可以直接从“梯度是一个向量值函数”展开出来。对标量函数 <span displaypfx="inline-" class="mathjax-container">\(f:\mathbb{R}^n\to\mathbb{R}\)</span>，梯度写成</p>
<span displaypfx="" class="mathjax-container">\[\nabla f(\mathbf{x})=\begin{bmatrix}\frac{\partial f}{\partial x_1}\\ \frac{\partial f}{\partial x_2}\\ \vdots\\ \frac{\partial f}{\partial x_n}\end{bmatrix}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\nabla f\)</span> 已经不是标量，而是把输入向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 映射为另一个向量的函数。因此可以像对任何向量值函数那样，对它再求一次 Jacobian：</p>
<span displaypfx="" class="mathjax-container">\[J_{\nabla f}(\mathbf{x})=\frac{\partial (\nabla f)}{\partial \mathbf{x}}\]</span>
<p>把梯度各分量逐项代进去，Jacobian 的第 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 个元素就是“梯度第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个分量对输入第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个变量的偏导数”：</p>
<span displaypfx="" class="mathjax-container">\[\big[J_{\nabla f}(\mathbf{x})\big]_{ij}=\frac{\partial}{\partial x_j}\left(\frac{\partial f}{\partial x_i}\right)=\frac{\partial^2 f}{\partial x_j\partial x_i}\]</span>
<p>于是整块矩阵就是</p>
<span displaypfx="" class="mathjax-container">\[J_{\nabla f}(\mathbf{x})=\begin{bmatrix}\frac{\partial^2 f}{\partial x_1\partial x_1} &amp; \frac{\partial^2 f}{\partial x_2\partial x_1} &amp; \cdots &amp; \frac{\partial^2 f}{\partial x_n\partial x_1}\\ \frac{\partial^2 f}{\partial x_1\partial x_2} &amp; \frac{\partial^2 f}{\partial x_2\partial x_2} &amp; \cdots &amp; \frac{\partial^2 f}{\partial x_n\partial x_2}\\ \vdots &amp; \vdots &amp; \ddots &amp; \vdots\\ \frac{\partial^2 f}{\partial x_1\partial x_n} &amp; \frac{\partial^2 f}{\partial x_2\partial x_n} &amp; \cdots &amp; \frac{\partial^2 f}{\partial x_n\partial x_n}\end{bmatrix}\]</span>
<p>这正是 Hessian：</p>
<span displaypfx="" class="mathjax-container">\[\nabla^2 f(\mathbf{x})=J_{\nabla f}(\mathbf{x})\]</span>
<p>这个等式的含义很直接：Hessian 不是凭空引入的另一种矩阵，而就是“梯度这个向量场对输入的 Jacobian”。</p>
<p>直接从二阶导角度看，Hessian 是多元函数的二阶导数对象，用来描述局部曲率（Curvature）、凹凸性以及临界点附近的二阶形状。</p>
<p>设标量函数 <span displaypfx="inline-" class="mathjax-container">\(f:\mathbb{R}^n\to\mathbb{R}\)</span>，输入向量为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}=(x_1,\dots,x_n)^\top\)</span>。Hessian 矩阵记作 <span displaypfx="inline-" class="mathjax-container">\(\nabla^2 f(\mathbf{x})\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(H_f(\mathbf{x})\)</span>，定义为：</p>
<span displaypfx="" class="mathjax-container">\[\nabla^2 f(\mathbf{x})=\left[\frac{\partial^2 f}{\partial x_i\partial x_j}(\mathbf{x})\right]_{i,j=1}^{n}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 都从 <span displaypfx="inline-" class="mathjax-container">\(1\)</span> 遍历到 <span displaypfx="inline-" class="mathjax-container">\(n\)</span>，因此 Hessian 会收集所有变量对 <span displaypfx="inline-" class="mathjax-container">\((x_i,x_j)\)</span> 的二阶偏导。之所以自然形成一个 <span displaypfx="inline-" class="mathjax-container">\(n\times n\)</span> 的“全组合”矩阵，是因为一阶导数 <span displaypfx="inline-" class="mathjax-container">\(\nabla f\)</span> 本身已经有 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个分量；再对这 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个分量分别对 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 个输入变量各求一次导，就得到 <span displaypfx="inline-" class="mathjax-container">\(n\times n\)</span> 个二阶偏导项。前面展开的 <span displaypfx="inline-" class="mathjax-container">\(J_{\nabla f}(\mathbf{x})\)</span> 就是这块矩阵，因此这里不再重复写一遍。</p>
<p>把梯度记为 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(\mathbf{x})=(g_1,\dots,g_n)^\top\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(g_i=\frac{\partial f}{\partial x_i}\)</span>，则 Hessian 的第 <span displaypfx="inline-" class="mathjax-container">\((i,j)\)</span> 个元素可以直接写成 <span displaypfx="inline-" class="mathjax-container">\(H_{ij}=\frac{\partial g_i}{\partial x_j}\)</span>。这个记号把它的意义说得很清楚：它衡量“第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个方向上的斜率”会不会随着第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个变量的变化而改变。</p>
<p>对角线元素 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial^2 f}{\partial x_i^2}\)</span> 描述沿单一坐标方向的纯二阶曲率，也就是该方向本身的弯曲强弱；非对角元素 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial^2 f}{\partial x_i\partial x_j}\)</span> 描述不同方向之间的局部耦合（Local Coupling）。当 <span displaypfx="inline-" class="mathjax-container">\(H_{ij}=0\)</span> 时，意味着在当前点附近，变量 <span displaypfx="inline-" class="mathjax-container">\(x_j\)</span> 的微小变化不会改变第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个方向上的一阶斜率；当 <span displaypfx="inline-" class="mathjax-container">\(H_{ij}\neq 0\)</span> 时，两个方向在局部二阶结构上发生耦合。</p>
<p>这种耦合会直接反映在等高线（Contour）形状上：若非对角项接近零，局部二次近似更接近与坐标轴对齐的“正椭圆碗”；若非对角项明显非零，等高线会发生倾斜，说明改变一个变量会连带改变另一个方向上的坡度。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 足够光滑，满足二阶混合偏导连续，则由 Clairaut / Schwarz 定理可得</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial^2 f}{\partial x_i\partial x_j}=\frac{\partial^2 f}{\partial x_j\partial x_i}\]</span>
<p>因此 Hessian 矩阵是对称矩阵（Symmetric Matrix）。这点非常重要，因为一旦 Hessian 对称，就可以用特征值（Eigenvalue）来判断局部曲率：正特征值对应向上弯，负特征值对应向下弯，正负并存则意味着不同方向上的弯曲趋势相反。</p>
<p>Hessian 的几何意义可以通过二阶 Taylor 展开看得最清楚。对点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}\)</span> 附近的微小扰动 <span displaypfx="inline-" class="mathjax-container">\(\Delta \mathbf{x}\)</span>，有：</p>
<span displaypfx="" class="mathjax-container">\[f(\mathbf{x}+\Delta\mathbf{x})\approx f(\mathbf{x})+\nabla f(\mathbf{x})^\top \Delta\mathbf{x}+\frac{1}{2}\Delta\mathbf{x}^\top \nabla^2 f(\mathbf{x})\Delta\mathbf{x}\]</span>
<p>这里第一项 <span displaypfx="inline-" class="mathjax-container">\(f(\mathbf{x})\)</span> 是当前函数值，第二项 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(\mathbf{x})^\top \Delta\mathbf{x}\)</span> 是一阶线性变化，第三项 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2}\Delta\mathbf{x}^\top \nabla^2 f(\mathbf{x})\Delta\mathbf{x}\)</span> 就是二阶曲率修正项。也就是说，梯度告诉你“朝哪边走函数会立刻增减”，Hessian 则告诉你“地形本身是碗状、山丘状，还是马鞍状”。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/hessian-locality-preview.png"><img class="alignnone size-full" src="https://blog.gmem.cc/wp-content/uploads/2026/03/hessian-locality.png" alt="hessian-locality" width="2307" height="1169" /></a></p>
<p>二维情形最直观。设</p>
<span displaypfx="" class="mathjax-container">\[f(x_1,x_2)=x_1^2+3x_1x_2+2x_2^2\]</span>
<p>先求梯度：</p>
<span displaypfx="" class="mathjax-container">\[\nabla f(x_1,x_2)=\begin{bmatrix}2x_1+3x_2\\3x_1+4x_2\end{bmatrix}\]</span>
<p>再求二阶偏导，可得：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial^2 f}{\partial x_1^2}=2,\qquad \frac{\partial^2 f}{\partial x_1\partial x_2}=3,\qquad \frac{\partial^2 f}{\partial x_2^2}=4\]</span>
<p>因此 Hessian 为：</p>
<span displaypfx="" class="mathjax-container">\[\nabla^2 f(x_1,x_2)=\begin{bmatrix}2 &amp; 3\\3 &amp; 4\end{bmatrix}\]</span>
<p>这个例子还有一个关键特征：Hessian 与 <span displaypfx="inline-" class="mathjax-container">\((x_1,x_2)\)</span> 无关，因此它说明该函数在整个空间里都是同一种二次曲面（Quadratic Surface）。对二次函数而言，Hessian 足以完整决定其曲率结构。</p>
<p>在极值判别里，Hessian 主要出现在临界点（Critical Point）附近。若某点 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}^*\)</span> 满足 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(\mathbf{x}^*)=\mathbf{0}\)</span>，则：</p>
<ul>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\nabla^2 f(\mathbf{x}^*)\)</span> 正定（Positive Definite），即对任意非零向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\)</span> 都有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}^\top \nabla^2 f(\mathbf{x}^*)\mathbf{v}>0\)</span>，则该点是局部极小值（Local Minimum）。</li>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(\nabla^2 f(\mathbf{x}^*)\)</span> 负定（Negative Definite），即对任意非零向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}\)</span> 都有 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{v}^\top \nabla^2 f(\mathbf{x}^*)\mathbf{v}<0[/latex]，则该点是局部极大值（Local Maximum）。</li>
<li>若 Hessian 不定（Indefinite），即存在方向使二次型为正，也存在方向使其为负，则该点是鞍点（Saddle Point）。</li>
<li>若 Hessian 只有半正定、半负定，或出现零特征值，则二阶信息不足以单独判定，还需要更高阶分析或结合函数结构进一步判断。</li>
</ul>
<p>这套判据和一维情形完全一致：一维里 [latex syntax=inline]f''(x^*)>0\)</span> 表示“碗底”， <span displaypfx="inline-" class="mathjax-container">\(f''(x^*)<0[/latex] 表示“山顶”；Hessian 只是把这个二阶导概念推广到了多维空间。</p>
<p>在优化中，Hessian 的意义更直接。牛顿法（Newton's Method）用它近似局部曲面，并据此选择更合理的更新方向：</p>
[latex]\mathbf{x}_{t+1}=\mathbf{x}_t-\big(\nabla^2 f(\mathbf{x}_t)\big)^{-1}\nabla f(\mathbf{x}_t)\)</span>
<p>与只看一阶斜率的梯度下降不同，牛顿法还利用了局部曲率信息：若某个方向很陡但也弯得很厉害，步长就应更谨慎；若某个方向很平缓，更新可以相对更大。这也是为什么 Hessian 会出现在牛顿法、拟牛顿法（BFGS / L-BFGS）以及很多二阶优化分析中。</p>
<p>在机器学习里，Hessian 还常用来讨论损失面的尖锐度（Sharpness）、参数可辨识性以及训练稳定性。但深度学习模型维度极高，完整 Hessian 的存储与求逆代价非常大：若参数维度为 <span displaypfx="inline-" class="mathjax-container">\(d\)</span>，Hessian 大小就是 <span displaypfx="inline-" class="mathjax-container">\(d\times d\)</span>。因此工程上更常用 Hessian 向量积（Hessian-Vector Product）、对角近似、低秩近似，或拟牛顿方法来间接利用二阶信息，而不是显式构造整块矩阵。</p>
<p>在神经网络里，单独说 Hessian 时，默认通常指损失函数对参数向量 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的 Hessian，即 <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta^2 L(\theta)\)</span>。原因很直接：优化真正要更新的是权重，因此人们最关心的是“损失面对参数空间的局部曲率”。当然，Hessian 也可以对输入或中间激活值来求，例如 <span displaypfx="inline-" class="mathjax-container">\(\nabla_x^2 f(x)\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\nabla_h^2 f(h)\)</span>，它们分别描述输入空间或表示空间中的局部弯曲；只是从工程语境看，这类用法通常不是默认含义。</p>
<div class="blog_h2"><span class="graybg">积分</span></div>
<div class="blog_h3"><span class="graybg">不定积分</span></div>
<p>不定积分（Indefinite Integral）也叫反导数、原函数。是“由变化率反推原函数（Antiderivative）”：已知 <span displaypfx="inline-" class="mathjax-container">\(f\)</span>，寻找所有满足 <span displaypfx="inline-" class="mathjax-container">\(F'(x)=f(x)\)</span> 的函数 <span displaypfx="inline-" class="mathjax-container">\(F\)</span>。</p>
<p><span style="background-color: #c0c0c0;">一句话强调：不定积分就是“已知导函数（导数），求原函数”的过程。</span></p>
<span displaypfx="" class="mathjax-container">\[\int f(x)\,dx=F(x)+C\]</span>
<div class="blog_h4"><span class="graybg">从变化率反推状态</span></div>
<p>把导数看作“瞬时变化率”最容易理解积分的方向。若 <span displaypfx="inline-" class="mathjax-container">\(s(t)\)</span> 是位置、<span displaypfx="inline-" class="mathjax-container">\(v(t)\)</span> 是速度，则 <span displaypfx="inline-" class="mathjax-container">\(s'(t)=v(t)\)</span>。当只知道 <span displaypfx="inline-" class="mathjax-container">\(v(t)\)</span> 时，要恢复 <span displaypfx="inline-" class="mathjax-container">\(s(t)\)</span> 就是在做积分。</p>
<span displaypfx="" class="mathjax-container">\[s'(t)=v(t)\ \Longrightarrow\ s(t)=\int v(t)\,dt\]</span>
<div class="blog_h4"><span class="graybg">为什么必须有 +C</span></div>
<p>因为微分会“抹掉常数项”。不同函数只要相差一个常数，导数就完全一样：</p>
<span displaypfx="" class="mathjax-container">\[\frac{d}{dx}(x^2)=\frac{d}{dx}(x^2+5)=\frac{d}{dx}(x^2-100)=2x\]</span>
<p>所以当你从 <span displaypfx="inline-" class="mathjax-container">\(f(x)=2x\)</span> 反推原函数时，只能确定主体是 <span displaypfx="inline-" class="mathjax-container">\(x^2\)</span>，无法确定常数偏移。这就是积分常数（Constant of Integration）<span displaypfx="inline-" class="mathjax-container">\(C\)</span> 的来源。若再给一个初值条件（Initial Condition），例如 <span displaypfx="inline-" class="mathjax-container">\(F(x_0)=y_0\)</span>，<span displaypfx="inline-" class="mathjax-container">\(C\)</span> 才会被唯一确定。</p>
<div class="blog_h4"><span class="graybg">几何图像：不是一条曲线，而是一族</span></div>
<p><span displaypfx="inline-" class="mathjax-container">\(\int f(x)\,dx\)</span> 的结果对应一族函数 <span displaypfx="inline-" class="mathjax-container">\(\{F(x)+C\mid C\in\mathbb{R}\}\)</span>。它们形状完全相同，只是沿 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 轴上下平移；在同一个 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 处，这族曲线的切线斜率都等于 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span>。</p>
<div class="blog_h4"><span class="graybg">一个最小可算例</span></div>
<span displaypfx="" class="mathjax-container">\[\int 2x\,dx=x^2+C\]</span>
<p>验算： <span displaypfx="inline-" class="mathjax-container">\(\frac{d}{dx}(x^2+C)=2x\)</span>。这一步强调的是“求导与积分互为逆过程”，但逆过程会保留一个常数不确定性。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/indefinite-integral1.png"><img class="alignnone size-full wp-image-40773" src="https://blog.gmem.cc/wp-content/uploads/2026/03/indefinite-integral1.png" alt="indefinite-integral" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">微积分基本定理（Fundamental Theorem of Calculus）</span></div>
<p>不定积分与定积分的连接点是微积分基本定理（Fundamental Theorem of Calculus）。它包含两条互补结论：</p>
<ol>
<li>若 <span displaypfx="inline-" class="mathjax-container">\(F'(x)=f(x)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\int_a^b f(x)\,dx=F(b)-F(a)\)</span>。这告诉我们：定积分可以通过任意一个原函数在端点处相减来计算。</li>
<li>定义 <span displaypfx="inline-" class="mathjax-container">\(G(x)=\int_a^x f(t)\,dt\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(G'(x)=f(x)\)</span>。这告诉我们：把函数从 <span displaypfx="inline-" class="mathjax-container">\(a\)</span> 到 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 累积起来，得到的累积函数本身就是一个原函数。</li>
</ol>
<p><span style="background-color: #c0c0c0;">一句话强调：定积分就是原函数（不定积分）在区间两端的函数值“收尾相减”</span>，即 <span displaypfx="inline-" class="mathjax-container">\(\int_a^b f(x)\,dx=F(b)-F(a)\)</span>。</p>
<p>例： <span displaypfx="inline-" class="mathjax-container">\(f(x)=2x\)</span>，取原函数 <span displaypfx="inline-" class="mathjax-container">\(F(x)=x^2\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\int_1^3 2x\,dx=F(3)-F(1)=9-1=8\]</span>
<div class="blog_h3"><span class="graybg">定积分</span></div>
<p>定积分（Definite Integral）可看成“带符号面积（Signed Area）”或“连续求和”。Riemann 和定义为：</p>
<span displaypfx="" class="mathjax-container">\[\int_a^b f(x)\,dx=\lim_{n\to\infty}\sum_{k=1}^{n}f(\xi_k)\Delta x_k\]</span>
<p>若上下界中出现 <span displaypfx="inline-" class="mathjax-container">\(\infty\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(-\infty\)</span>，它仍然属于定积分范畴，只是更准确地叫广义积分（Improper Integral）或广义定积分：本质上仍是在一个区间上求累积量，只是需要先把无穷区间截断，再用极限恢复。例如</p>
<span displaypfx="" class="mathjax-container">\[\int_a^{\infty} f(x)\,dx:=\lim_{R\to\infty}\int_a^R f(x)\,dx\]</span>
<p>因此“上下界无穷”不是不定积分，而是<span style="background-color: #c0c0c0;">定积分的特殊形式</span>。</p>
<p>在 AI 里，连续概率密度函数（Probability Density Function, PDF）的概率计算 <span displaypfx="inline-" class="mathjax-container">\(P(a\le X\le b)=\int_a^b p(x)\,dx\)</span> 本质上就是定积分。</p>
<div class="blog_h3"><span class="graybg">常用积分公式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">被积函数</td>
<td style="text-align: center;">积分结果</td>
<td style="text-align: center;">备注</td>
</tr>
</thead>
<tbody>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(x^n\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{x^{n+1}}{n+1}+C\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(n\ne -1\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{x}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\ln|x|+C\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(x\ne 0\)</span></td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(e^x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(e^x+C\)</span></td>
<td>指数函数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(-\cos x+C\)</span></td>
<td>三角函数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\cos x\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\sin x+C\)</span></td>
<td>三角函数</td>
</tr>
<tr>
<td><span displaypfx="inline-" class="mathjax-container">\(\frac{1}{1+x^2}\)</span></td>
<td><span displaypfx="inline-" class="mathjax-container">\(\arctan x+C\)</span></td>
<td>反三角函数</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">多重积分</span></div>
<p>多重积分（Multiple Integrals）是把一维积分的“连续求和”推广到更高维区域上的积分运算。它积分的对象不再只是线段上的小长度，而是平面区域上的小面积、空间区域中的小体积，或者曲面上的小面片。因此，多重积分处理的是“在二维、三维或更一般区域上，把局部量连续累加成整体量”的问题。</p>
<p>它的阅读难点通常不在计算本身，而在于符号会很快遮住几何直觉。理解的关键仍然是把“积分”看成连续求和，只不过求和对象已经从一维区间扩展到了更高维区域。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/multiple-integral.jpg"><img class="alignnone size-full wp-image-40789" src="https://blog.gmem.cc/wp-content/uploads/2026/03/multiple-integral.jpg" alt="multiple-integral" width="100%" /></a></p>
<div class="blog_h4"><span class="graybg">从一维面积到三维体积</span></div>
<p>一重积分 <span displaypfx="inline-" class="mathjax-container">\(\int f(x)\,dx\)</span> 常被理解为曲线下的面积：在 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 轴上切出很多极窄的小条，每条小条的面积近似是“高度 <span displaypfx="inline-" class="mathjax-container">\(\times\)</span> 宽度”，也就是 <span displaypfx="inline-" class="mathjax-container">\(f(x)\,dx\)</span>，再把它们全部加起来。</p>
<p>双重积分把这个想法从“一条线”推广到“一块区域”。设 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 是平面上的一块区域，曲面高度由 <span displaypfx="inline-" class="mathjax-container">\(z=f(x,y)\)</span> 给出，那么</p>
<span displaypfx="" class="mathjax-container">\[\iint_D f(x,y)\,dx\,dy\]</span>
<p>就可以理解为：在区域 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 的每个小块 <span displaypfx="inline-" class="mathjax-container">\(dx\,dy\)</span> 上立起一根很细的小柱子，柱高是 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)\)</span>。这些小柱子拼起来，形成曲面下方的一整块立体体积。于是双重积分最直观的几何意义就是：<span style="background-color: #c0c0c0;">把区域上方由高度函数堆出来的整块三维体积连续累加起来</span>。</p>
<p>再往上推广，三重积分</p>
<span displaypfx="" class="mathjax-container">\[\iiint_\Omega f(x,y,z)\,dV\]</span>
<p>表示在空间区域 <span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span> 内，对每个微小体积元 <span displaypfx="inline-" class="mathjax-container">\(dV\)</span> 的函数值做累加。此时它不一定是在“求一个更高维体积”，更准确地说，是在整个空间体内部对某个量做总汇总，例如总质量、总能量、总代价或总概率权重。可以把它和一重积分放在同一条理解链上来看：速度函数在时间上的累积给出位移，密度函数在空间上的累积给出质量，能量密度（Energy Density）在区域或体积上的累积给出总能量。若 <span displaypfx="inline-" class="mathjax-container">\(\rho(x,y)\)</span> 是面密度，则 <span displaypfx="inline-" class="mathjax-container">\(\iint_D \rho(x,y)\,dA\)</span> 表示区域 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 上的总质量；若 <span displaypfx="inline-" class="mathjax-container">\(\rho(x,y,z)\)</span> 是体密度，则 <span displaypfx="inline-" class="mathjax-container">\(\iiint_\Omega \rho(x,y,z)\,dV\)</span> 表示空间区域 <span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span> 内的总质量。积分符号的重数，本质上对应的是累积区域的维度。</p>
<div class="blog_h4"><span class="graybg">高度函数到底表示什么</span></div>
<p>双重积分里最需要读懂的是 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)\)</span>。它并不总是“真实高度”，而是一个随位置变化的量。你可以把它看成“每个点上放了多少东西”。积分的过程，就是把整块区域上的这些局部贡献全部收集起来。</p>
<ul>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=1\)</span> 时，每个位置的高度都恒为 1，所以积分值在数值上就等于区域 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 的面积。此时双重积分退化为“面积公式”。</li>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)\)</span> 是密度函数（Density Function）时，积分得到的是总质量、总电荷或总概率。也就是说，函数值越大，说明该位置“堆得越多”，对总量贡献越大。</li>
<li>当 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)\)</span> 表示代价、风险、热量、响应强度等场量时，积分得到的是这类量在整个区域上的累积结果。</li>
</ul>
<p>因此，多重积分做的是一件很具体的事：把无穷多个微小局部量累加成一个整体量。</p>
<div class="blog_h4"><span class="graybg">带圈的积分号是什么意思</span></div>
<p>有时会看到带圈的双积分号 ∯。这个圈表示积分域是闭合的（Closed）：不是一块敞开的平面区域，而是一个完整包起来的曲面，例如球壳表面、气泡表面或任意封闭外壳。为了把这个“带圈”符号直接看出来，下面就直接写成在闭合曲面 <span displaypfx="inline-" class="mathjax-container">\(S\)</span> 上的积分。</p>
<p style="text-align: center; font-size: 1.15em;"><span style="text-align: center; font-size: 2em;">∯</span><sub>S</sub> <span displaypfx="inline-" class="mathjax-container">\(\mathbf{F}\cdot d\mathbf{S}\)</span></p>
<p>这里的含义通常不是“求面积”，而是计算向量场（Vector Field）穿过闭合曲面的总通量（Flux）。直观地说，就是看有多少“流”从这个封闭外壳内部穿出，或者从外部流入。它和高斯散度定理（Gauss's Divergence Theorem）直接相关，因此在偏微分方程（PDE）、连续介质建模、计算流体以及物理信息神经网络（Physics-Informed Neural Networks, PINNs）中十分重要。</p>
<div class="blog_h4"><span class="graybg">多重积分和AI</span></div>
<p>在 AI 中，多重积分最常见的身份是<span style="background-color: #c0c0c0;">概率分布上的总量计算器</span>。如果 <span displaypfx="inline-" class="mathjax-container">\(p(x,y)\)</span> 是二维随机变量的概率密度，那么</p>
<span displaypfx="" class="mathjax-container">\[\iint p(x,y)\,dx\,dy=1\]</span>
<p>表示整张概率曲面下方的总体积必须等于 1。这句话的本质是：所有可能情况加起来，概率总和必须是 100%。</p>
<p>进一步，期望（Expectation）就是在这个概率地形上做加权平均。以一维为例，</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[X]=\int x\,p(x)\,dx\]</span>
<p>表示每个位置 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 都按其概率权重 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span> 参与平均；多维情形下则自然推广为多重积分。用几何语言说，期望就是这堆“概率质量”在各坐标方向上的重心位置。</p>
<p>这也是为什么高斯分布归一化、边缘分布（Marginal Distribution）、条件分布、变分推断（Variational Inference）、连续潜变量模型以及能量模型（Energy-Based Models）都离不开多重积分。很多时候模型真正要做的事，并不是求一个公式值，而是在高维概率空间里累计、归一化、消去变量或计算期望。</p>
<div class="blog_h2"><span class="graybg">卷积（Convolution）</span></div>
<p>卷积（Convolution）把两个函数/序列组合成第三个函数/序列，最常见的语义是：<span style="background-color: #c0c0c0;">“一个信号在另一个信号的权重/响应下做累积叠加”</span>。它同时是信号处理（Signal Processing）、系统理论（Systems Theory）与 CNN 的基础运算。</p>
<div class="blog_h3"><span class="graybg">连续卷积（Continuous Convolution）</span></div>
<p>对连续函数 <span displaypfx="inline-" class="mathjax-container">\(f(t)\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(g(t)\)</span>，卷积定义为：</p>
<span displaypfx="" class="mathjax-container">\[(f*g)(t)=\int_{-\infty}^{+\infty} f(\tau)\,g(t-\tau)\,d\tau\]</span>
<p>经典记忆法：<span style="background-color: #c0c0c0;">翻转（Flip）→ 平移（Shift）→ 相乘（Multiply）→ 积分（Integrate）</span>。其中 <span displaypfx="inline-" class="mathjax-container">\(g(t-\tau)\)</span> 这一项等价于把 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 先时间反转再按 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 平移。</p>
<div class="blog_h3"><span class="graybg">离散卷积（Discrete Convolution）</span></div>
<p>对离散序列 <span displaypfx="inline-" class="mathjax-container">\(x[n]\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(h[n]\)</span>，卷积定义为：</p>
<span displaypfx="" class="mathjax-container">\[(x*h)[n]=\sum_{k=-\infty}^{+\infty} x[k]\,h[n-k]\]</span>
<p>当序列有限长度时，上式求和区间会自然收缩为有限项。二维卷积（2D Convolution）/图像滤波可以视为把求和从一维扩展到二维索引。</p>
<div class="blog_h3"><span class="graybg">因果性与单侧卷积（Causality）</span></div>
<p>工程系统常满足因果性（Causality）：系统在时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 的输出只依赖过去与当前输入，不依赖未来输入。若输入 <span displaypfx="inline-" class="mathjax-container">\(f(t)\)</span> 与系统的冲激响应（Impulse Response）<span displaypfx="inline-" class="mathjax-container">\(g(t)\)</span> 都是因果的（<span displaypfx="inline-" class="mathjax-container">\(t<0[/latex] 时为 0），则卷积积分可写成单侧形式：</p>
[latex](f*g)(t)=\int_{0}^{t} f(\tau)\,g(t-\tau)\,d\tau\)</span>
<p>这个形式更符合直觉：时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 的结果由 <span displaypfx="inline-" class="mathjax-container">\([0,t]\)</span> 区间内“所有过去输入”的贡献叠加而来。</p>
<div class="blog_h3"><span class="graybg">直觉例子：打点滴的累积药效</span></div>
<p>把卷积理解成“累积药效”通常更直观。设 <span displaypfx="inline-" class="mathjax-container">\(r(t)\)</span> 是单位时间的滴注速率（Infusion Rate），<span displaypfx="inline-" class="mathjax-container">\(h(t)\)</span> 是“单位剂量在体内的药效/浓度随时间衰减曲线”（可以把它理解为冲激响应）。在时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 的总药效/浓度 <span displaypfx="inline-" class="mathjax-container">\(c(t)\)</span> 可以写成：</p>
<span displaypfx="" class="mathjax-container">\[c(t)=(r*h)(t)=\int_{0}^{t} r(\tau)\,h(t-\tau)\,d\tau\]</span>
<p>解释：在过去每个时刻 <span displaypfx="inline-" class="mathjax-container">\(\tau\)</span> 滴入的一小部分药量 <span displaypfx="inline-" class="mathjax-container">\(r(\tau)\,d\tau\)</span>，到现在时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 已经“经过了” <span displaypfx="inline-" class="mathjax-container">\(t-\tau\)</span> 的代谢时间，因此它的残留贡献按 <span displaypfx="inline-" class="mathjax-container">\(h(t-\tau)\)</span> 衰减；把所有过去贡献叠加，就得到当前总效果。这正是卷积“用一个响应核去累积历史”的核心语义。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/convolution.jpg"><img class="alignnone size-full wp-image-40805" src="https://blog.gmem.cc/wp-content/uploads/2026/03/convolution.jpg" alt="convolution" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">卷积与互相关（Cross-Correlation）</span></div>
<p>互相关（Cross-Correlation）与卷积的形式非常接近，但没有“核翻转”（Kernel Flip）：一维离散互相关常写成 <span displaypfx="inline-" class="mathjax-container">\(\sum_k x[k]\,w[n+k]\)</span>（不同资料索引略有差异）。深度学习框架里多数“卷积层”实现的是互相关，而不是严格数学卷积；由于卷积核参数是可学习的，翻不翻转并不会改变可表达的函数族，但在信号处理里二者语义不同，需注意约定。</p>
<div class="blog_h2"><span class="graybg">链式法则</span></div>
<p>链式法则（Chain Rule）是反向传播（Backpropagation）的核心：当计算图（Computation Graph）由一系列函数复合组成时，总导数等于沿路径的局部导数相乘。</p>
<p>在微分语言下更紧凑：若 <span displaypfx="inline-" class="mathjax-container">\(y=g(x)\)</span>、<span displaypfx="inline-" class="mathjax-container">\(z=f(y)\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(dz=f'(y)\,dy\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(dy=g'(x)\,dx\)</span>，合并得 <span displaypfx="inline-" class="mathjax-container">\(dz=f'(g(x))g'(x)\,dx\)</span>。反向传播做的就是把这些局部“系数”从输出一路乘回输入。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/rule-of-chains.jpg"><img class="alignnone size-full wp-image-40809" src="https://blog.gmem.cc/wp-content/uploads/2026/03/rule-of-chains.jpg" alt="rule-of-chains" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">最小二乘法</span></div>
<p>最小二乘法（Least Squares）解决“拟合误差最小”的问题。给定样本矩阵 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 与目标 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span>，线性模型 <span displaypfx="inline-" class="mathjax-container">\(\hat{\mathbf{y}}=X\mathbf{w}\)</span> 的目标是：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w}}\ \|X\mathbf{w}-\mathbf{y}\|_2^2\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(X^\top X\)</span> 可逆时，闭式解满足正规方程（Normal Equations）：</p>
<span displaypfx="" class="mathjax-container">\[X^\top X\mathbf{w}=X^\top\mathbf{y},\quad \mathbf{w}^*=(X^\top X)^{-1}X^\top\mathbf{y}\]</span>
<p>几何直觉： <span displaypfx="inline-" class="mathjax-container">\(X\mathbf{w}\)</span> 只能落在 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 的列空间（Column Space）中，最小二乘解就是把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{y}\)</span> 正交投影（Orthogonal Projection）到这个空间上，剩余误差向量与列空间正交。</p>
<p>在 AI/统计实践中，若 <span displaypfx="inline-" class="mathjax-container">\(X^\top X\)</span> 病态或奇异，常用岭回归（Ridge Regression）：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w}}\ \|X\mathbf{w}-\mathbf{y}\|_2^2+\lambda\|\mathbf{w}\|_2^2,\quad (X^\top X+\lambda I)\mathbf{w}=X^\top\mathbf{y}\]</span>
<p>它本质上是把二次目标做稳定化，降低方差并提升泛化。</p>
<div class="blog_h2"><span class="graybg">凸函数与凸优化</span></div>
<p>凸函数（Convex Function）是“碗状”的函数：任意两点连线上的函数值不超过端点函数值的线性插值。形式化定义：</p>
<span displaypfx="" class="mathjax-container">\[f(\lambda x_1+(1-\lambda)x_2)\le \lambda f(x_1)+(1-\lambda)f(x_2),\quad \lambda\in[0,1]\]</span>
<p>凸优化（Convex Optimization）之所以重要，是因为凸目标在凸可行域上没有“坏局部最小值”：任一局部最小值都是全局最小值。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 二阶可导，则 Hessian 提供了最直接的局部判据：在一维中 <span displaypfx="inline-" class="mathjax-container">\(f''(x)\ge 0\)</span> 对应凸；在多维中，若对所有 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 都有 <span displaypfx="inline-" class="mathjax-container">\(\nabla^2 f(x)\succeq 0\)</span>（半正定，Positive Semidefinite），则 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 是凸函数。若处处 <span displaypfx="inline-" class="mathjax-container">\(\nabla^2 f(x)\succ 0\)</span>（正定，Positive Definite），则函数通常具有更强的严格凸性（Strict Convexity）。</p>
<p>损失函数的形状可以非常多样。需要区分两层含义：</p>
<ul>
<li>损失形式本身：例如 BCE/CE（对概率或对线性模型的 logits）是凸的，MSE 也是凸的。</li>
<li>对参数的整体目标：当把损失与深度网络的非线性参数化组合后，目标函数通常变成非凸（Non-convex），这不是“交叉熵不凸”，而是“网络映射让问题不凸”。</li>
</ul>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/loss-fn-convex.jpg"><img class="alignnone size-full wp-image-40813" src="https://blog.gmem.cc/wp-content/uploads/2026/03/loss-fn-convex.jpg" alt="loss-fn-convex" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">拉格朗日乘数法</span></div>
<p>拉格朗日乘数法（Lagrange Multiplier Method）处理约束优化（Constrained Optimization）：在满足约束的前提下最小化/最大化目标函数。常用写法是把约束写成函数形式：等式约束（Equality Constraints）<span displaypfx="inline-" class="mathjax-container">\(g_i(x)=0\)</span>，不等式约束（Inequality Constraints）<span displaypfx="inline-" class="mathjax-container">\(g_i(x)\le 0\)</span>。</p>
<p>对等式约束，构造拉格朗日函数（Lagrangian）：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(x,\lambda)=f(x)+\sum_{i=1}^{m}\lambda_i g_i(x)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 是目标函数（Objective），<span displaypfx="inline-" class="mathjax-container">\(g_i(x)\)</span> 是约束函数（Constraint Function），<span displaypfx="inline-" class="mathjax-container">\(\lambda_i\)</span> 是拉格朗日乘子（Lagrange Multiplier）。新增部分 <span displaypfx="inline-" class="mathjax-container">\(\sum_i \lambda_i g_i(x)\)</span> 称为拉格朗日项（Lagrange Term），单个约束对应的项是 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i g_i(x)\)</span>。</p>
<p>在不同应用里乘子的记号可能不同：在优化理论里常写 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i\)</span>，在支持向量机（SVM）里常写 <span displaypfx="inline-" class="mathjax-container">\(\alpha_i\)</span>（每个样本约束对应一个乘子）。</p>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}(x,\lambda)\)</span> 看作关于原变量 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 与乘子 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 的二元函数后，鞍点（Saddle Point）结构是约束被编码进目标函数后的直接结果。 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的角色是压低总代价， <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 的角色是放大任何尚未满足的约束，因此两者天然形成极小极大（Min-Max）结构。</p>
<p>先看等式约束 <span displaypfx="inline-" class="mathjax-container">\(g(x)=0\)</span>。这里必须先把“为什么内层是最大化”说清楚：这不是记号习惯，而是为了把约束编码成一个<span style="background-color: #c0c0c0;">可行点保留原目标、不可行点直接罚到 <span displaypfx="inline-" class="mathjax-container">\(+\infty\)</span></span> 的机制。因此，固定某个 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 后，内层写成 <span displaypfx="inline-" class="mathjax-container">\(\max_\lambda \, (f(x)+\lambda g(x))\)</span>。如果 <span displaypfx="inline-" class="mathjax-container">\(g(x)\neq 0\)</span>，由于 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 不受符号限制，最大化者总能把 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 取到与 <span displaypfx="inline-" class="mathjax-container">\(g(x)\)</span> 同号且绝对值任意大，使 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}(x,\lambda)\to\infty\)</span>；只有当 <span displaypfx="inline-" class="mathjax-container">\(g(x)=0\)</span> 时，拉格朗日项才消失，内层最大值才退化为 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(\max_\lambda \mathcal{L}(x,\lambda)\)</span> 的作用，是把所有不满足等式约束的点直接排除掉。</p>
<p>不等式约束更容易看出这种“过滤”机制。若约束是 <span displaypfx="inline-" class="mathjax-container">\(g(x)\le 0\)</span> 且乘子满足 <span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>，那么当 <span displaypfx="inline-" class="mathjax-container">\(g(x)>0\)</span> 时，最大化者会把 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 推大，使 <span displaypfx="inline-" class="mathjax-container">\(\lambda g(x)\to\infty\)</span>；当 <span displaypfx="inline-" class="mathjax-container">\(g(x)\le 0\)</span> 时，继续增大 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 只会让值变小，因此最优选择是 <span displaypfx="inline-" class="mathjax-container">\(\lambda=0\)</span>。于是 <span displaypfx="inline-" class="mathjax-container">\(\max_{\lambda\ge 0}\mathcal{L}(x,\lambda)\)</span> 对可行点返回原始代价 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span>，对不可行点返回“无限罚款”。外层再对 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 做最小化，就等价于只在可行域内最小化 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span>。</p>
<span displaypfx="" class="mathjax-container">\[\min_x\ \max_\lambda\ \mathcal{L}(x,\lambda)\]</span>
<p>几何上，这正对应马鞍形曲面：沿 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 方向切开， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span> 像一个向上开的“碗”，因为这里在做最小化；沿 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 方向切开， <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span> 像一个向下开的“拱”，因为这里在做最大化。鞍点 <span displaypfx="inline-" class="mathjax-container">\((x^*,\lambda^*)\)</span> 的含义是：固定 <span displaypfx="inline-" class="mathjax-container">\(\lambda=\lambda^*\)</span> 时， <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 已经不能再把值压低；固定 <span displaypfx="inline-" class="mathjax-container">\(x=x^*\)</span> 时， <span displaypfx="inline-" class="mathjax-container">\(\lambda^*\)</span> 也已经不能再把值抬高。原问题的最优解与约束的恰当作用强度，就在这个交点同时确定。</p>
<p>记号约定：上标星号（Asterisk）<span displaypfx="inline-" class="mathjax-container">\(^*\)</span> 通常表示“最优/最优点处的取值”，例如 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 是最优解， <span displaypfx="inline-" class="mathjax-container">\(\lambda^*\)</span> 是与之对应的最优乘子。</p>
<p>在鞍点 <span displaypfx="inline-" class="mathjax-container">\((x^*,\lambda^*)\)</span>，固定 <span displaypfx="inline-" class="mathjax-container">\(\lambda=\lambda^*\)</span> 时 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 使 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span> 最小；固定 <span displaypfx="inline-" class="mathjax-container">\(x=x^*\)</span> 时 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*\)</span> 使 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span> 最大，可用不等式写成：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(x^*,\lambda)\le \mathcal{L}(x^*,\lambda^*)\le \mathcal{L}(x,\lambda^*)\]</span>
<p>上面的鞍点不等式给出的是几何刻画；真正求解时，还需要把“这个点已经不能再降、也不能再升”的直观条件改写成可计算的方程。做法是检查拉格朗日函数（Lagrangian）<span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span> 对各个变量的一阶变化：若在候选点附近，沿某个允许方向还能继续把值压低（对 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>）或继续把值抬高（对 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span>），那么该点就不可能是鞍点。</p>
<p>这就是一阶必要条件（First-Order Necessary Condition）的来源：若 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 是最优解，则一阶（线性）变化项必须已经消失，否则还存在继续改进的方向。这里“必要”不等于“充分”：满足一阶条件只说明它有可能是最优点，不说明它一定最优；要得到充分结论，还需要二阶条件、凸性（Convexity）或其他结构。</p>
<p>无约束情形最直接。在一维中，若 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 是可微且不在边界上的局部最小点，则向左或向右移动都不能让函数更小，因此斜率必须满足 <span displaypfx="inline-" class="mathjax-container">\(f'(x^*)=0\)</span>；多维里把“斜率”推广为梯度（Gradient），于是条件变为 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x^*)=0\)</span>。</p>
<p>把同样的思路移到等式约束上，求解目标就从“找原函数 <span displaypfx="inline-" class="mathjax-container">\(f\)</span> 的极小点”变成“找拉格朗日函数 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}(x,\lambda)\)</span> 的鞍点”。对可导问题，一阶条件写成：</p>
<span displaypfx="" class="mathjax-container">\[\nabla_x \mathcal{L}(x^*,\lambda^*)=0,\quad g_i(x^*)=0\]</span>
<p>第一式表示：固定 <span displaypfx="inline-" class="mathjax-container">\(\lambda=\lambda^*\)</span> 后，已经找不到能继续降低 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span> 的 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 方向；第二式就是可行性（Feasibility）本身。又因为 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial \mathcal{L}}{\partial \lambda_i}=g_i(x)\)</span>，所以“对乘子求偏导为 0”本质上只是把约束 <span displaypfx="inline-" class="mathjax-container">\(g_i(x)=0\)</span> 原样写回。</p>
<p>不等式约束时，乘子必须满足 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i\ge 0\)</span>，因此鞍点结构相应写成：</p>
<span displaypfx="" class="mathjax-container">\[\min_x\ \max_{\lambda\ge 0}\ \mathcal{L}(x,\lambda)\]</span>
<p>此时除了驻点条件，还必须同时满足 KKT 条件（Karush–Kuhn–Tucker Conditions）。其中最关键的是互补松弛（Complementary Slackness）<span displaypfx="inline-" class="mathjax-container">\(\lambda_i g_i(x^*)=0\)</span>：每个约束在最优点只有两种状态——要么它是紧的（Active），即 <span displaypfx="inline-" class="mathjax-container">\(g_i(x^*)=0\)</span>；要么它不紧，此时对应乘子必须为 0，表示该约束在最优点处没有实际作用。</p>
<p>例（等式约束）：最小化 <span displaypfx="inline-" class="mathjax-container">\(f(x,y)=x^2+y^2\)</span>，约束 <span displaypfx="inline-" class="mathjax-container">\(x+y=1\)</span>。拉格朗日函数：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(x,y,\lambda)=x^2+y^2+\lambda(x+y-1)\]</span>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-lagrange.png"><img class="alignnone size-full wp-image-40821" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-lagrange.png" alt="plot-lagrange" width="100%" /></a></p>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\lambda(x+y-1)\)</span> 是拉格朗日项（Lagrange Term）：它把等式约束 <span displaypfx="inline-" class="mathjax-container">\(x+y-1=0\)</span> 以乘子加权的形式并入目标函数。</p>
<p>令偏导为零： <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial \mathcal{L}}{\partial x}=2x+\lambda=0\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\frac{\partial \mathcal{L}}{\partial y}=2y+\lambda=0\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\frac{\partial \mathcal{L}}{\partial \lambda}=x+y-1=0\)</span>。注意 <span displaypfx="inline-" class="mathjax-container">\(\frac{\partial \mathcal{L}}{\partial \lambda}=0\)</span> 就是把约束 <span displaypfx="inline-" class="mathjax-container">\(x+y=1\)</span> 写回来；联立可得 <span displaypfx="inline-" class="mathjax-container">\(x=y=\frac{1}{2}\)</span>。几何上，这意味着最优点处目标函数等高线与约束曲线相切。</p>
<p>例（不等式约束）：最小化 <span displaypfx="inline-" class="mathjax-container">\(f(x)=x^2\)</span>，约束 <span displaypfx="inline-" class="mathjax-container">\(x\ge 1\)</span>。直觉上，无约束最小值在 <span displaypfx="inline-" class="mathjax-container">\(x=0\)</span>，但它不满足约束，所以最优点只能落在边界 <span displaypfx="inline-" class="mathjax-container">\(x=1\)</span>。</p>
<ol>
<li>把约束改写成标准形式：令 <span displaypfx="inline-" class="mathjax-container">\(g(x)=1-x\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(x\ge 1\Leftrightarrow g(x)\le 0\)</span>。</li>
<li>构造拉格朗日函数： <span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}(x,\lambda)=f(x)+\lambda g(x)=x^2+\lambda(1-x)\)</span>。其中 <span displaypfx="inline-" class="mathjax-container">\(\lambda(1-x)\)</span> 是拉格朗日项（Lagrange Term），不等式情形要求 <span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>。</li>
<li>写出 KKT 的核心条件：可行性（Feasibility）<span displaypfx="inline-" class="mathjax-container">\(1-x\le 0\)</span>；乘子非负 <span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>；驻点（Stationarity）<span displaypfx="inline-" class="mathjax-container">\(\frac{d\mathcal{L}}{dx}=2x-\lambda=0\)</span>；互补松弛（Complementary Slackness）<span displaypfx="inline-" class="mathjax-container">\(\lambda(1-x)=0\)</span>。</li>
<li>解：由 <span displaypfx="inline-" class="mathjax-container">\(2x-\lambda=0\)</span> 得 <span displaypfx="inline-" class="mathjax-container">\(\lambda=2x\)</span>。互补松弛要求 <span displaypfx="inline-" class="mathjax-container">\(\lambda=0\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(x=1\)</span>。若 <span displaypfx="inline-" class="mathjax-container">\(\lambda=0\)</span> 则 <span displaypfx="inline-" class="mathjax-container">\(x=0\)</span>，但违反 <span displaypfx="inline-" class="mathjax-container">\(x\ge 1\)</span>；因此 <span displaypfx="inline-" class="mathjax-container">\(x^*=1\)</span>，进而 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*=2\)</span>。</li>
<li>解释：在最优点处 <span displaypfx="inline-" class="mathjax-container">\(1-x^*=0\)</span>，该约束是“紧的”（Active），互补松弛允许乘子 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*>0\)</span>，它刻画了该约束在最优点处对解的影响强度。如果最优点落在可行域（Feasible Set）内部（约束不紧， <span displaypfx="inline-" class="mathjax-container">\(1-x^*<0[/latex]），互补松弛会强制 [latex syntax=inline]\lambda^*=0[/latex]。</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-lagrange-2.png"><img class="alignnone size-full wp-image-40825" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-lagrange-2.png" alt="plot-lagrange-2" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">对偶问题</span></div>
<p>对偶问题（Dual Problem）从原始问题（Primal Problem）导出下界函数，用于分析可行性、间隙与最优性。标准形式：</p>
[latex]\min_x\ f(x)\ \text{s.t.}\ g_i(x)\le 0,\ h_j(x)=0\)</span>
<span displaypfx="" class="mathjax-container">\[g(\lambda,\nu)=\inf_x\Big(f(x)+\sum_i \lambda_i g_i(x)+\sum_j \nu_j h_j(x)\Big),\ \lambda_i\ge 0\]</span>
<span displaypfx="" class="mathjax-container">\[\max_{\lambda,\nu}\ g(\lambda,\nu)\ \text{s.t.}\ \lambda\ge 0\]</span>
<p>把括号中的表达式记为拉格朗日函数（Lagrangian）<span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}(x,\lambda,\nu)=f(x)+\sum_i \lambda_i g_i(x)+\sum_j \nu_j h_j(x)\)</span>，则对偶函数（Dual Function）就是 <span displaypfx="inline-" class="mathjax-container">\(g(\lambda,\nu)=\inf_x \mathcal{L}(x,\lambda,\nu)\)</span>：固定乘子后先对原变量求下确界，把它“消去”，再对乘子最大化这个下界。</p>
<div class="blog_h4"><span class="graybg">例：SVM 对偶函数是怎么把变量“消掉”的</span></div>
<p>SVM 的对偶函数（Dual Function）来自一个标准过程：先写出原始问题（Primal Problem）的拉格朗日函数（Lagrangian），再对原变量取下确界（Infimum），把优化问题改写为只依赖乘子变量的形式。在线性支持向量机（Support Vector Machine, SVM）里，这个过程最清楚，因为 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\alpha}\)</span> 的出现既解释了“为什么叫对偶”，也把“把原变量消掉”变成了可逐步计算的代换。</p>
<p>考虑硬间隔（Hard-margin）线性 SVM。原始问题是：</p>
<span displaypfx="" class="mathjax-container">\[\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|_2^2\quad \text{s.t.}\quad y_i(\mathbf{w}^\top \mathbf{x}_i+b)-1\ge 0,\ i=1,\dots,n\]</span>
<p>把约束改写成标准不等式形式：</p>
<span displaypfx="" class="mathjax-container">\[g_i(\mathbf{w},b)=1-y_i(\mathbf{w}^\top \mathbf{x}_i+b)\le 0\]</span>
<p>于是拉格朗日函数为：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(\mathbf{w},b,\boldsymbol{\alpha})=\frac{1}{2}\|\mathbf{w}\|_2^2+\sum_{i=1}^{n}\alpha_i\big(1-y_i(\mathbf{w}^\top \mathbf{x}_i+b)\big),\quad \alpha_i\ge 0\]</span>
<p>对偶函数（Dual Function）的定义是：固定乘子 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\alpha}\)</span>，对原变量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 取下确界：</p>
<span displaypfx="" class="mathjax-container">\[g(\boldsymbol{\alpha})=\inf_{\mathbf{w},b}\mathcal{L}(\mathbf{w},b,\boldsymbol{\alpha})\]</span>
<p>这里的关键结构已经出现了：原始问题直接优化 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span>，而对偶函数先把 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\alpha}\)</span> 固定住，把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 当成待消去变量。随后得到的最大化问题，只剩乘子变量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\alpha}\)</span>。这就是“对偶”的来源：<span style="background-color: #c0c0c0;">同一个最优化问题，被改写成了另一组变量上的伴随优化问题</span>。</p>
<p>现在把 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span> 完整消掉。先求驻点条件（Stationarity Conditions）。对 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 求偏导：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial \mathbf{w}}=\mathbf{w}-\sum_{i=1}^{n}\alpha_i y_i \mathbf{x}_i=\mathbf{0}\]</span>
<p>因此</p>
<span displaypfx="" class="mathjax-container">\[\mathbf{w}=\sum_{i=1}^{n}\alpha_i y_i \mathbf{x}_i\]</span>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 求偏导：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\partial \mathcal{L}}{\partial b}=-\sum_{i=1}^{n}\alpha_i y_i=0\]</span>
<p>因此</p>
<span displaypfx="" class="mathjax-container">\[\sum_{i=1}^{n}\alpha_i y_i=0\]</span>
<p>把这些结果代回拉格朗日函数之前，先把它展开成便于逐项代换的形式：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(\mathbf{w},b,\boldsymbol{\alpha})=\frac{1}{2}\mathbf{w}^\top\mathbf{w}+\sum_{i=1}^{n}\alpha_i-\sum_{i=1}^{n}\alpha_i y_i\,\mathbf{w}^\top\mathbf{x}_i-b\sum_{i=1}^{n}\alpha_i y_i\]</span>
<p>第一项代入 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}=\sum_{i=1}^{n}\alpha_i y_i \mathbf{x}_i\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[\frac{1}{2}\mathbf{w}^\top\mathbf{w}=\frac{1}{2}\Big(\sum_{i=1}^{n}\alpha_i y_i \mathbf{x}_i\Big)^\top\Big(\sum_{j=1}^{n}\alpha_j y_j \mathbf{x}_j\Big)\]</span>
<span displaypfx="" class="mathjax-container">\[=\frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<p>第二项保持不变：</p>
<span displaypfx="" class="mathjax-container">\[\sum_{i=1}^{n}\alpha_i\]</span>
<p>第三项代入同一个 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w}\)</span> 表达式：</p>
<span displaypfx="" class="mathjax-container">\[-\sum_{i=1}^{n}\alpha_i y_i\,\mathbf{w}^\top\mathbf{x}_i=-\sum_{i=1}^{n}\alpha_i y_i\Big(\sum_{j=1}^{n}\alpha_j y_j \mathbf{x}_j\Big)^\top\mathbf{x}_i\]</span>
<span displaypfx="" class="mathjax-container">\[=-\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_j^\top\mathbf{x}_i\]</span>
<span displaypfx="" class="mathjax-container">\[=-\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<p>最后一项利用 <span displaypfx="inline-" class="mathjax-container">\(\sum_{i=1}^{n}\alpha_i y_i=0\)</span> 直接消失：</p>
<span displaypfx="" class="mathjax-container">\[-b\sum_{i=1}^{n}\alpha_i y_i=0\]</span>
<p>因此</p>
<span displaypfx="" class="mathjax-container">\[g(\boldsymbol{\alpha})=\frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j+\sum_{i=1}^{n}\alpha_i-\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<span displaypfx="" class="mathjax-container">\[=\sum_{i=1}^{n}\alpha_i-\frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\alpha_i\alpha_j y_i y_j\,\mathbf{x}_i^\top\mathbf{x}_j\]</span>
<p>于是对偶问题（Dual Problem）就是：</p>
<span displaypfx="" class="mathjax-container">\[\max_{\boldsymbol{\alpha}}\ g(\boldsymbol{\alpha})\quad \text{s.t.}\quad \alpha_i\ge 0,\ \sum_{i=1}^{n}\alpha_i y_i=0\]</span>
<p>此时“对偶”二字的含义就精确了：原始问题在参数空间里直接求 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{w},b\)</span>，对偶问题则在乘子空间里求 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{\alpha}\)</span>；两者来自同一个拉格朗日结构，描述的是同一个最优解的两种表示。对 SVM 而言，更重要的结构变化是：数据 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i\)</span> 不再单独出现，而只通过内积（Inner Product）<span displaypfx="inline-" class="mathjax-container">\(\mathbf{x}_i^\top\mathbf{x}_j\)</span> 进入目标函数，这正是核技巧（Kernel Trick）的入口。</p>
<p>若问题是凸的且满足 Slater 条件，则强对偶（Strong Duality）成立，可以用鞍点形式表达“先最小化再最大化”与“先最大化再最小化”等价：</p>
<span displaypfx="" class="mathjax-container">\[\min_x\max_{\lambda\ge 0,\nu}\mathcal{L}(x,\lambda,\nu)=\max_{\lambda\ge 0,\nu}\min_x\mathcal{L}(x,\lambda,\nu)\]</span>
<p>对偶函数之所以是“下界”，来自一个简单但关键的不等式：对任意可行解 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>（满足所有约束）与任意 <span displaypfx="inline-" class="mathjax-container">\(\lambda\ge 0\)</span>，都有</p>
<span displaypfx="" class="mathjax-container">\[f(x)\ge f(x)+\sum_i \lambda_i g_i(x)+\sum_j \nu_j h_j(x)\ge \inf_{x'}\Big(f(x')+\sum_i \lambda_i g_i(x')+\sum_j \nu_j h_j(x')\Big)=g(\lambda,\nu)\]</span>
<p>因此对任何 <span displaypfx="inline-" class="mathjax-container">\((\lambda,\nu)\)</span>，都有 <span displaypfx="inline-" class="mathjax-container">\(g(\lambda,\nu)\le p^*\)</span>（原始最优值），这就是弱对偶（Weak Duality）。</p>
<p>弱对偶（Weak Duality）总成立；也就是说，对偶问题给出的值永远不会高于原始问题的最优值。但弱对偶只保证“对偶值是下界”，并不保证这个下界恰好贴住原始最优值。若两者之间还差一截，这个差距就叫对偶间隙（Duality Gap）。</p>
<p>强对偶（Strong Duality）则更进一步：它要求原始最优值与对偶最优值完全相等，也就是对偶问题不只是给出一个保守下界，而是精确刻画了原问题的最优值。对很多凸优化问题而言，Slater 条件正是让这种“下界刚好贴住最优值”的关键保证。</p>
<p>从直观上看，Slater 条件要求可行域内部真的存在一个严格可行点。这意味着约束系统不是被边界死死卡住的退化结构，而是有真实的内部空间。可行域一旦有内部，拉格朗日乘子就更容易稳定地描述“目标函数下降趋势”和“约束反作用力”之间的平衡，因此原始问题与对偶问题之间更不容易出现缝隙。在凸优化里，这就是为什么 Slater 条件常被看作强对偶成立的重要通行证。</p>
<p>直观上，对偶变量（Dual Variables）可以看作约束的“影子价格（Shadow Price）”：如果把约束放宽一点点，最优目标值会如何变化。在很多问题中（例如支持向量机（SVM）），对偶化会把优化变量从“模型参数”转成“约束乘子”，并把数据依赖压缩为内积，从而自然导出核技巧（Kernel Trick）。</p>
<div class="blog_h4"><span class="graybg">影子价格（Shadow Price）</span></div>
<p>对偶变量（Dual Variable）常被称为影子价格（Shadow Price），因为它衡量的是：<span style="background-color: #c0c0c0;">如果把某条约束稍微放宽一点，最优目标值会改善多少</span>。这里的“价格”不是市场价格，而是“约束资源有多值钱”的边际刻度。</p>
<p>可以把约束想成一种稀缺资源。例如训练时有显存限制、预算限制、风险限制或几何边界限制；如果某条约束非常紧，那么它就像一个卡脖子的瓶颈。此时只要把这条约束稍微放宽一点，最优目标值就可能明显改善，于是它对应的对偶变量就会比较大。反过来，如果某条约束本来就很松，放宽它也几乎没有收益，那么它对应的对偶变量通常就是 0 或接近 0。</p>
<p>在数学上，这个直觉可以写成一种局部敏感度关系。若把约束写成</p>
<span displaypfx="" class="mathjax-container">\[g_i(x)\le b_i\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(b_i\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 条约束允许的“资源上限”或“边界位置”，那么对应的最优值函数可以记为 <span displaypfx="inline-" class="mathjax-container">\(p^*(b)\)</span>。在适当条件下，对偶变量 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*\)</span> 可以理解为最优值对 <span displaypfx="inline-" class="mathjax-container">\(b_i\)</span> 的边际变化率，也就是“把第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 条约束放宽一点，最优值会朝什么方向变化、变化多快”。</p>
<p>在最小化问题中，若某条约束对应的 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*\)</span> 很大，通常表示这条约束非常关键：它一旦被放宽，最优目标值会明显下降；若 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*=0\)</span>，则说明该约束在当前最优点并没有真正起作用，放宽它也不会立刻带来收益。这与前面的互补松弛完全一致：<span style="background-color: #c0c0c0;">只有真正卡住最优解的约束，才会拥有非零影子价格</span>。</p>
<p>因此，影子价格提供了一个非常有用的解释视角：原始变量告诉我们“最优解长什么样”，而对偶变量告诉我们“哪些约束最贵、最紧、最值得被放宽”。这也是为什么在优化理论、经济学和机器学习里，对偶变量不仅是求解工具，也是理解模型结构的重要语言。</p>
<div class="blog_h4"><span class="graybg">Slater 条件</span></div>
<p>Slater 条件（Slater's Condition）是凸优化（Convex Optimization）里最常见的正则性条件（Regularity Condition）之一。它的作用不是改变优化问题本身，而是保证对偶理论能够“工作得很干净”：在满足它时，很多凸问题会满足强对偶（Strong Duality），也就是原始问题最优值与对偶问题最优值相等；同时，KKT 条件也更容易从“必要条件”提升为判定最优性的核心条件。</p>
<p>对标准凸优化问题</p>
<span displaypfx="" class="mathjax-container">\[ \min_x f(x)\quad \text{s.t.}\quad g_i(x)\le 0,\ h_j(x)=0 \]</span>
<p>如果 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 和每个 <span displaypfx="inline-" class="mathjax-container">\(g_i(x)\)</span> 都是凸函数（Convex Function），每个 <span displaypfx="inline-" class="mathjax-container">\(h_j(x)\)</span> 都是仿射函数（Affine Function），那么 Slater 条件要求：<span style="background-color: #c0c0c0;">至少存在一个严格可行点（Strictly Feasible Point）</span> <span displaypfx="inline-" class="mathjax-container">\(\tilde{x}\)</span>，使得所有不等式约束都被“严格满足”，也就是</p>
<span displaypfx="" class="mathjax-container">\[ g_i(\tilde{x})<0,\ \forall i,\qquad h_j(\tilde{x})=0,\ \forall j [/latex]
<p>这里“严格满足”四个字最关键。普通可行点只要求 [latex syntax=inline]g_i(x)\le 0\]</span>，也就是允许刚好压在边界上；而 Slater 条件要求存在某个点，能让所有不等式约束都留出一点余量，也就是完全处在可行域内部，而不是贴着边界。</p>
<p>这个条件可以用一个非常直观的图像来理解。把不等式约束围成的可行域想成一个房间：</p>
<ul>
<li>如果房间内部真的有空间，存在一个点站在房间里，不碰任何墙，这就是满足 Slater 条件。</li>
<li>如果所谓“可行域”其实只是几面墙交出来的一条线、一个角、甚至一个点，根本没有真正的内部空间，那么 Slater 条件通常就不满足。</li>
</ul>
<p>因此，Slater 条件本质上是在说：<span style="background-color: #c0c0c0;">这个凸约束系统不能只是勉强拼出一个边界碎片，而要有真正的内部</span>。一旦内部存在，原始问题和对偶问题之间的间隙通常就会消失，拉格朗日乘子和 KKT 条件也会变得更稳定、更有解释力。</p>
<p>一个简单例子可以把它说清楚。考虑约束</p>
<span displaypfx="" class="mathjax-container">\[x\ge 0\]</span>
<p>写成标准形式是 <span displaypfx="inline-" class="mathjax-container">\(g(x)=-x\le 0\)</span>。这个约束满足 Slater 条件，因为取 <span displaypfx="inline-" class="mathjax-container">\(\tilde{x}=1\)</span> 时，有 <span displaypfx="inline-" class="mathjax-container">\(g(1)=-1<0[/latex]。这说明可行域 [latex syntax=inline][0,+\infty)[/latex] 不只是边界点 [latex syntax=inline]x=0[/latex]，而是真正包含内部区域。</p>
<p>再看一个不满足 Slater 条件的例子：</p>
[latex]x^2\le 0\)</span>
<p>由于 <span displaypfx="inline-" class="mathjax-container">\(x^2\)</span> 永远不小于 0，这个约束唯一允许的点只有 <span displaypfx="inline-" class="mathjax-container">\(x=0\)</span>。可行域虽然非空，但没有任何点能让 <span displaypfx="inline-" class="mathjax-container">\(x^2<0[/latex] 成立，所以不存在严格可行点，Slater 条件不成立。这样的约束系统只有边界、没有内部。</p>
<p>在机器学习常见的凸问题里，Slater 条件之所以频繁出现，是因为它几乎就是“强对偶成立的通行证”。例如在线性规划、逻辑回归的某些约束变体、支持向量机（SVM）的凸二次规划中，只要能找到一个严格可行点，就通常可以放心地从原始问题走到对偶问题，再用 KKT 条件解释最优解结构。反过来，如果没有 Slater 条件，就可能出现对偶间隙（Duality Gap），也就是对偶最优值严格小于原始最优值，此时只看对偶或只看 KKT 就不一定足够。</p>
<div class="blog_h4"><span class="graybg">概念速查</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">概念</td>
<td style="text-align: center;">它回答的问题</td>
<td style="text-align: center;">最核心的一句话</td>
<td style="text-align: center;">在这一章里的作用</td>
</tr>
</thead>
<tbody>
<tr>
<td>弱对偶（Weak Duality）</td>
<td>对偶问题和原始问题之间至少有什么关系</td>
<td>对偶最优值永远不会高于原始最优值；它天然是一个下界</td>
<td>先建立“对偶为什么有意义”的最低保证</td>
</tr>
<tr>
<td>对偶间隙（Duality Gap）</td>
<td>为什么有时对偶值还不等于原始最优值</td>
<td>如果对偶下界还没贴住原始最优值，两者之间的差就是对偶间隙</td>
<td>解释为什么“有对偶”不等于“对偶已经足够”</td>
</tr>
<tr>
<td>强对偶（Strong Duality）</td>
<td>什么时候对偶问题就足以精确刻画原问题</td>
<td>原始最优值与对偶最优值完全相等，对偶不再只是保守下界</td>
<td>为从原始问题转到对偶问题提供理论正当性</td>
</tr>
<tr>
<td>Slater 条件（Slater's Condition）</td>
<td>凸优化里什么条件有助于强对偶成立</td>
<td>只要可行域内部存在严格可行点，很多凸问题就能消除对偶间隙</td>
<td>说明为什么强对偶和 KKT 在凸问题里经常可用</td>
</tr>
<tr>
<td>影子价格（Shadow Price）</td>
<td>对偶变量到底在解释什么</td>
<td>它衡量“把某条约束放宽一点，最优值会改善多少”</td>
<td>赋予拉格朗日乘子清晰的经济 / 几何解释</td>
</tr>
<tr>
<td>KKT 条件</td>
<td>最优解在约束下必须满足哪些平衡关系</td>
<td>目标函数的下降趋势与约束施加的反作用力在最优点平衡</td>
<td>把可行性、乘子、驻点与互补松弛统一成最优性条件</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">KKT 条件</span></div>
<div class="blog_h4"><span class="graybg">背景和定义</span></div>
<p>KKT 条件（Karush–Kuhn–Tucker Conditions）讨论的是<span style="background-color: #c0c0c0;">带约束优化问题在最优点必须满足什么条件</span>。无约束优化里，常见做法是令梯度（Gradient）为 0；但一旦问题带有不等式约束或等式约束，只看 [latex syntax=inline]\nabla f(x)=0\)</span> 就不够了，因为最优点很可能被约束“顶”在边界上，而不是落在自由空间里的普通驻点。</p>
<p>标准形式写作：</p>
<span displaypfx="" class="mathjax-container">\[\min_x f(x)\quad \text{s.t.}\quad g_i(x)\le 0,\ h_j(x)=0\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span> 是目标函数（Objective Function），表示希望最小化的量；<span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是优化变量（Optimization Variable）；<span displaypfx="inline-" class="mathjax-container">\(g_i(x)\le 0\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个不等式约束（Inequality Constraint）；<span displaypfx="inline-" class="mathjax-container">\(h_j(x)=0\)</span> 是第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个等式约束（Equality Constraint）。KKT 条件给出的就是：当 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 真的是一个最优解时，它与对应的拉格朗日乘子（Lagrange Multipliers）之间必须满足一组相互配合的条件。</p>
<p>在凸优化（Convex Optimization）里，如果问题满足适当的正则性条件，例如 Slater 条件（Slater's Condition），KKT 条件往往不只是必要条件，还可以成为判定最优性的核心工具。在线性规划、二次规划、支持向量机（Support Vector Machine, SVM）和许多机器学习训练问题中，KKT 条件都直接参与求解过程。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/kkt-paraboloid-plane.png"><img class="alignnone size-full wp-image-41393" src="https://blog.gmem.cc/wp-content/uploads/2026/03/kkt-paraboloid-plane.png" alt="kkt-paraboloid-plane" width="2107" height="1134" /></a></p>
<div class="blog_h4"><span class="graybg">图解：四组条件如何逐条理解</span></div>
<p>上图使用的是活跃边界 <span displaypfx="inline-" class="mathjax-container">\(g(x)=1-x_1-x_2=0\)</span>。边界 <span displaypfx="inline-" class="mathjax-container">\(x_1+x_2=1\)</span> 的切向方向可取 <span displaypfx="inline-" class="mathjax-container">\((1,-1)\)</span>；与它垂直的向量都是法向量（Normal Vector），例如 <span displaypfx="inline-" class="mathjax-container">\((1,1)\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\((-1,-1)\)</span>。如果把约束写成函数 <span displaypfx="inline-" class="mathjax-container">\(g(x)=1-x_1-x_2\)</span>，那么梯度 <span displaypfx="inline-" class="mathjax-container">\(\nabla g(x)=(-1,-1)\)</span> 就是一条法向量；它指向 <span displaypfx="inline-" class="mathjax-container">\(g(x)\)</span> 增大的方向，也就是不可行侧。</p>
<p>图中的橙色箭头对应驻点条件（Stationarity）里真正参与平衡的两个向量：目标梯度 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x^*)\)</span> 与乘子加权后的约束法向量 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*\nabla g(x^*)\)</span>。它们在最优点首尾相接后得到 0，于是 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x^*)+\lambda^*\nabla g(x^*)=0\)</span> 不是抽象记号，而是边界最优点上的几何平衡：</p>
<ul>
<li><span style="background-color: #c0c0c0;">原始可行性（Primal Feasibility）</span>：解必须落在可行域内。对这张图而言，可行域是半空间 <span displaypfx="inline-" class="mathjax-container">\(x_1+x_2\ge 1\)</span>，最优点 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 落在它的活跃边界 <span displaypfx="inline-" class="mathjax-container">\(x_1+x_2=1\)</span> 上。</li>
<li><span style="background-color: #c0c0c0;">对偶可行性（Dual Feasibility）</span>：不等式约束的乘子必须非负，即 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*\ge 0\)</span>。图中的计算给出 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*=0.65\)</span>，表示这条边界在最优点处确实对目标下降施加了正的“反作用”。</li>
<li><span style="background-color: #c0c0c0;">驻点条件（Stationarity）</span>：沿边界的切向方向已经不能继续下降，因此目标梯度不再含有切向分量，只能落在法向空间里。单个活跃约束时，这就变成 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x^*)=-\lambda^*\nabla g(x^*)\)</span>。图中的两根橙色箭头正是这两个大小相等、方向相反的向量。</li>
<li><span style="background-color: #c0c0c0;">互补松弛（Complementary Slackness）</span>：约束若不接触最优点，就不会产生乘子；约束一旦卡在最优点上，对应乘子才会变成正值。当前图像展示的是活跃边界情形，所以 <span displaypfx="inline-" class="mathjax-container">\(g(x^*)=0\)</span> 且 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*>0\)</span> 同时成立。</li>
</ul>
<p>法向量之所以关键，是因为它刻画了“离开边界最快”的方向；切向方向则刻画了“沿边界滑动”的方向。边界最优点的本质结论是：<span style="background-color: #c0c0c0;">目标函数想继续下降的那一部分趋势，已经完全被约束边界的法向作用抵消；沿边界本身则不存在进一步下降方向</span>。</p>
<div class="blog_h4"><span class="graybg">具像化描述</span></div>
<p>可以把 KKT 条件想成一个“遛狗”问题。优化目标是：狗想尽可能往远处跑；约束条件是：主人手里只有一根 5 米长的狗绳，因此狗的活动范围不能超过这条绳子的长度。不等式约束 <span displaypfx="inline-" class="mathjax-container">\(g_i(x)\le 0\)</span> 的作用，就像在说“不能超过这根绳子允许的极限”；等式约束 <span displaypfx="inline-" class="mathjax-container">\(h_j(x)=0\)</span> 则对应那些必须被精确满足的固定条件。</p>
<p>这个比喻里最重要的是区分“绳子是否真正起作用”。如果狗最后只跑到 3 米处就停下，例如已经闻到味道、找到目标，或者在那里已经达到最优状态，那么绳子仍然是松的，还留有 2 米余量。这时约束虽然存在，但它没有真正限制住解。相反，如果狗拼命往前冲，最后正好跑到 5 米极限，绳子就会被拉紧；此时狗还想继续前进，但被约束挡住了。KKT 条件刻画的正是这种“<span style="background-color: #c0c0c0;">目标继续改进的趋势，与约束施加的阻拦作用，在最优点达到平衡</span>”的状态。</p>
<p>其中最形象的一条就是互补松弛（Complementary Slackness）。它表达的是：每一个不等式约束在最优点都只有两种状态。</p>
<ul>
<li>如果某个约束正好卡在边界上，即 <span displaypfx="inline-" class="mathjax-container">\(g_i(x^*)=0\)</span>，就像狗刚好把 5 米狗绳拉到极限，此时这条绳子真的“顶住了”最优解，它对应的乘子 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*\)</span> 可以大于 0。</li>
<li>如果某个约束离边界还有余量，即 <span displaypfx="inline-" class="mathjax-container">\(g_i(x^*)<0[/latex]，就像狗只跑到 3 米处，绳子仍然松弛，那么它对应的乘子必须是 0，也就是 [latex syntax=inline]\lambda_i^*=0[/latex]。</li>
</ul>
<p>因此，乘子 [latex syntax=inline]\lambda_i^*\)</span> 可以直观理解为绳子的<span style="background-color: #c0c0c0;">拉力</span>，也就是约束对最优解施加的压力。在优化语言里，这个量也常被解释为影子价格（Shadow Price）：如果把这条约束稍微放宽一点，最优目标值会改善多少。拉力不为 0，说明这条约束正在真正影响解；拉力为 0，说明这条约束虽然存在，但在最优点处并没有发挥作用。</p>
<div class="blog_h4"><span class="graybg">公式逐元素解释</span></div>
<p>把原问题写成统一形式后，先定义拉格朗日函数（Lagrangian）：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(x,\lambda,\nu)=f(x)+\sum_i \lambda_i g_i(x)+\sum_j \nu_j h_j(x)\]</span>
<p>这个式子里的每个元素都有明确含义：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\mathcal{L}\)</span>：拉格朗日函数，把目标函数和约束统一写进一个式子里。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(x\)</span>：原始变量（Primal Variable），也就是模型真正要优化的参数。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(f(x)\)</span>：目标函数，表示希望最小化的代价、损失或能量。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(g_i(x)\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个不等式约束函数；要求它满足 <span displaypfx="inline-" class="mathjax-container">\(g_i(x)\le 0\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(h_j(x)\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个等式约束函数；要求它满足 <span displaypfx="inline-" class="mathjax-container">\(h_j(x)=0\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\lambda_i\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 个不等式约束对应的拉格朗日乘子；它衡量该约束施加的“压力”或“拉力”大小，也可理解为对应约束资源的影子价格。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\nu_j\)</span>：第 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 个等式约束对应的拉格朗日乘子。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sum_i\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\sum_j\)</span>：分别表示把所有不等式约束和所有等式约束的影响累加起来。</li>
</ul>
<p>在此基础上，KKT 条件通常写成四组：</p>
<span displaypfx="" class="mathjax-container">\[\text{Primal feasibility: } g_i(x^*)\le 0,\ h_j(x^*)=0\]</span>
<span displaypfx="" class="mathjax-container">\[\text{Dual feasibility: } \lambda_i^*\ge 0\]</span>
<span displaypfx="" class="mathjax-container">\[\text{Stationarity: } \nabla f(x^*)+\sum_i \lambda_i^*\nabla g_i(x^*)+\sum_j \nu_j^*\nabla h_j(x^*)=0\]</span>
<span displaypfx="" class="mathjax-container">\[\text{Complementary slackness: } \lambda_i^* g_i(x^*)=0\]</span>
<p>这四组条件可以逐条理解：</p>
<ul>
<li><span style="background-color: #c0c0c0;">原始可行性（Primal Feasibility）</span>：最优解 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 首先必须是合法的，不能跑到可行域之外。也就是说，所有不等式约束都要满足 <span displaypfx="inline-" class="mathjax-container">\(g_i(x^*)\le 0\)</span>，所有等式约束都要精确满足 <span displaypfx="inline-" class="mathjax-container">\(h_j(x^*)=0\)</span>。</li>
<li><span style="background-color: #c0c0c0;">对偶可行性（Dual Feasibility）</span>：不等式约束的乘子必须非负，即 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*\ge 0\)</span>。这保证了它们表达的是“阻止变量越界的压力”，而不是把解往不可行方向拉过去的反向力量。</li>
<li><span style="background-color: #c0c0c0;">驻点条件（Stationarity）</span>：在最优点处，目标函数的梯度 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x^*)\)</span> 与所有约束梯度加权后的合力必须平衡为 0。这里 <span displaypfx="inline-" class="mathjax-container">\(\nabla f(x^*)\)</span> 表示目标函数在 <span displaypfx="inline-" class="mathjax-container">\(x^*\)</span> 处最陡上升方向；<span displaypfx="inline-" class="mathjax-container">\(\nabla g_i(x^*)\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\nabla h_j(x^*)\)</span> 分别表示约束边界的法向方向；乘子 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\nu_j^*\)</span> 则控制这些方向各自施加多少“反作用力”。这条式子本质上是在说：最优点处已经不存在任何仍然可行且还能继续下降的方向。</li>
<li><span style="background-color: #c0c0c0;">互补松弛（Complementary Slackness）</span>：每个不等式约束都满足 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^* g_i(x^*)=0\)</span>。因为这是两个量的乘积等于 0，所以只能出现两种情况：要么 <span displaypfx="inline-" class="mathjax-container">\(g_i(x^*)=0\)</span>，说明约束正好顶在边界上；要么 <span displaypfx="inline-" class="mathjax-container">\(\lambda_i^*=0\)</span>，说明这条约束在最优点没有施加压力。它精确区分了“活跃约束（Active Constraint）”和“不活跃约束（Inactive Constraint）”。</li>
</ul>
<p>一个一维例子可以把这些符号落到实处。考虑：</p>
<span displaypfx="" class="mathjax-container">\[\min_x\ (x+1)^2\quad \text{s.t.}\quad x\ge 0\]</span>
<p>把约束写成标准形式 <span displaypfx="inline-" class="mathjax-container">\(g(x)=-x\le 0\)</span>，于是拉格朗日函数是：</p>
<span displaypfx="" class="mathjax-container">\[\mathcal{L}(x,\lambda)=(x+1)^2+\lambda(-x)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\((x+1)^2\)</span> 是目标函数；<span displaypfx="inline-" class="mathjax-container">\(-x\le 0\)</span> 是不等式约束；<span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 是这条约束对应的乘子。KKT 条件变成：</p>
<span displaypfx="" class="mathjax-container">\[x\ge 0,\quad \lambda\ge 0,\quad 2(x+1)-\lambda=0,\quad \lambda x=0\]</span>
<p>这四项分别表示：解必须在 <span displaypfx="inline-" class="mathjax-container">\(x\ge 0\)</span> 的合法区域内；乘子必须非负；目标函数与约束施加的作用力在最优点平衡；约束要么卡住解、要么不施加压力。无约束时， <span displaypfx="inline-" class="mathjax-container">\((x+1)^2\)</span> 的最低点在 <span displaypfx="inline-" class="mathjax-container">\(x=-1\)</span>，但这不满足 <span displaypfx="inline-" class="mathjax-container">\(x\ge 0\)</span>，所以真正最优点被“推”到边界 <span displaypfx="inline-" class="mathjax-container">\(x^*=0\)</span>；再代入驻点条件 <span displaypfx="inline-" class="mathjax-container">\(2(x+1)-\lambda=0\)</span>，得到 <span displaypfx="inline-" class="mathjax-container">\(\lambda^*=2\)</span>。这正对应“边界在最优点处确实对解施加了压力”。</p>
<p>如果把目标函数换成 <span displaypfx="inline-" class="mathjax-container">\(\min_x (x-2)^2\ \text{s.t.}\ x\ge 0\)</span>，无约束最优点就是 <span displaypfx="inline-" class="mathjax-container">\(x=2\)</span>，它本来就在可行域内部，因此约束没有真正碰到最优点。此时 KKT 会给出 <span displaypfx="inline-" class="mathjax-container">\(x^*=2\)</span>、<span displaypfx="inline-" class="mathjax-container">\(\lambda^*=0\)</span>。这就是互补松弛最直观的含义：<span style="background-color: #c0c0c0;">边界没有碰到解，压力就自动消失；边界一旦碰到解，乘子就会变成非零</span>。</p>
<div class="blog_h1"><span class="graybg">概率论与统计</span></div>
<p>概率论与统计处理两类问题：第一类是<span style="background-color: #c0c0c0;">在不确定性下如何描述事件、变量与数据生成过程</span>；第二类是<span style="background-color: #c0c0c0;">在只观察到有限样本时，如何反推总体规律与模型参数</span>。在 AI 中，分类概率、回归噪声、采样、似然训练、置信评估与后验推断都建立在这套语言之上。</p>
<div class="blog_h2"><span class="graybg">基础概念</span></div>
<p>在进入公式前，先区分几个最常混淆的基础对象。概率论不是一开始就讨论“均值”和“方差”，而是先规定<span style="background-color: #c0c0c0;">随机试验的结果空间、结果上的事件，以及把结果映射成数的随机变量</span>，之后才谈分布、期望和统计推断。</p>
<ul>
<li>样本空间（Sample Space）<span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span>：一次随机试验所有可能结果的集合，其中希腊字母 <span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span> 只是“全部可能结果”的记号。掷骰子时 <span displaypfx="inline-" class="mathjax-container">\(\Omega=\{1,2,3,4,5,6\}\)</span>，表示所有可能点数组成的集合。</li>
<li>事件（Event）：样本空间的子集，表示“哪些结果算作某件事发生”。例如“点数为偶数”对应集合 <span displaypfx="inline-" class="mathjax-container">\(\{2,4,6\}\)</span>；这句话的意思是，只要试验结果落在这个集合里，就说该事件发生。</li>
<li>随机变量（Random Variable）<span displaypfx="inline-" class="mathjax-container">\(X\)</span>：把随机结果映射为数的函数，可写作 <span displaypfx="inline-" class="mathjax-container">\(X:\Omega\to\mathbb{R}\)</span>。这里 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 是这个函数的名字， <span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span> 是输入端的样本空间，箭头 <span displaypfx="inline-" class="mathjax-container">\(\to\)</span> 表示“映射到”， <span displaypfx="inline-" class="mathjax-container">\(\mathbb{R}\)</span> 表示实数集合。也就是说，每个随机结果都会被 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 变成一个实数。掷骰子时最自然的随机变量就是“点数本身”；在机器学习里，输入 <span displaypfx="inline-" class="mathjax-container">\(X\)</span>、标签 <span displaypfx="inline-" class="mathjax-container">\(Y\)</span>、噪声 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 都是随机变量。</li>
<li>概率分布（Probability Distribution）：描述随机变量取不同值的概率规律。离散情形常写作 <span displaypfx="inline-" class="mathjax-container">\(P(X=x)\)</span>，意思是“随机变量 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 恰好取值 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的概率”；连续情形常写作密度 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>，它不是某一点的概率本身，而是概率密度函数（Probability Density Function, PDF），需要在区间上积分才得到概率。</li>
<li>期望（Expectation）<span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]\)</span>：按概率加权的平均，回答“长期来看这个随机变量的典型水平在哪里”。这里 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}\)</span> 是 expectation 的标准记号，方括号中的 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 表示“对哪个随机变量取期望”。</li>
<li>方差（Variance）<span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(X)\)</span>：围绕期望的波动强度，回答“它通常偏离平均水平多大”。其中 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}\)</span> 是 variance 的记号，括号里的 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 表示“考察哪个随机变量的波动”。</li>
<li>参数（Parameter）与统计量（Statistic）：参数描述总体，例如高斯分布中的 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 表示均值（Mean）， <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 表示方差（Variance）；统计量则由样本计算出来，例如 <span displaypfx="inline-" class="mathjax-container">\(\bar{x}\)</span> 表示样本均值（Sample Mean），上面的横线读作“x bar”，意思是“样本中所有 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的平均值”。统计学习的核心任务之一，就是用统计量去估计未知参数。</li>
</ul>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/probability.jpg"><img class="alignnone size-full wp-image-40841" src="https://blog.gmem.cc/wp-content/uploads/2026/03/probability.jpg" alt="probability" width="100%" /></a></p>
<div class="blog_h2"><span class="graybg">似然和概率</span></div>
<p>概率（Probability）与似然（Likelihood）都可以写成 <span displaypfx="inline-" class="mathjax-container">\(p(x|\theta)\)</span> 这样的形式，但它们把“谁是已知、谁是待判断对象”放在不同位置。概率把参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 视为已知、把数据 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 视为随机结果；似然则把数据 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 固定下来，把参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 当作待比较对象。</p>
<p>也正因为公式形式相同，二者很容易混淆；真正的区别不在写法，而在它们回答的是相反方向的问题。条件分布 <span displaypfx="inline-" class="mathjax-container">\(p(x|\theta)\)</span> 在统计里经常同时出现在两种语境中：当参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 固定、数据 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 变化时，它表示“在这个模型下观察到不同数据的概率有多大”；当数据 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 固定、参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 变化时，它表示“哪些参数更能解释这份已经观察到的数据”。</p>
<p>因此，概率的读法是<span style="background-color: #c0c0c0;">参数已知，看数据</span>。例如抛硬币模型里，若已知正面概率 <span displaypfx="inline-" class="mathjax-container">\(\theta=0.7\)</span>，那么 10 次里出现 8 次正面的概率是</p>
<span displaypfx="" class="mathjax-container">\[P(X=8|\theta=0.7)={10 \choose 8}0.7^8 0.3^2\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\({10 \choose 8}\)</span> 表示组合数（Binomial Coefficient）：从 10 次试验里选出 8 次作为正面的不同选法一共有多少种。它负责计数“8 次正面可以出现在哪 8 个位置上”，不是额外的概率项。</p>
<p>而 <span displaypfx="inline-" class="mathjax-container">\(0.7^8 0.3^2\)</span> 来自独立重复试验的乘法法则：如果每次抛掷相互独立，且正面概率是 <span displaypfx="inline-" class="mathjax-container">\(0.7\)</span>、反面概率是 <span displaypfx="inline-" class="mathjax-container">\(0.3\)</span>，那么任意一个“8 次正面、2 次反面”的具体序列，其概率都是 8 个 <span displaypfx="inline-" class="mathjax-container">\(0.7\)</span> 与 2 个 <span displaypfx="inline-" class="mathjax-container">\(0.3\)</span> 的乘积，也就是 <span displaypfx="inline-" class="mathjax-container">\(0.7^8 0.3^2\)</span>。</p>
<p>这里被当作变量的是结果 <span displaypfx="inline-" class="mathjax-container">\(X\)</span>；问题是“给定参数，这样的数据是否常见”。这就是通常意义上的概率或概率密度。</p>
<p>似然的读法正好反过来：<span style="background-color: #c0c0c0;">数据已知，看参数</span>。假设现在已经观察到 10 次抛硬币里有 8 次正面，这组数据不再变化；真正变化的是候选参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>。于是同一个模型被改写为似然函数（Likelihood Function）：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta|X=8)=P(X=8|\theta)={10 \choose 8}\theta^8(1-\theta)^2\]</span>
<p>此时 <span displaypfx="inline-" class="mathjax-container">\(L(\theta|x)\)</span> 不是“参数取某个值的概率”，而是一个关于 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的评分函数：哪个 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 让已观察到的数据 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 更容易出现，哪个 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的似然就更大。最大似然估计（Maximum Likelihood Estimation, MLE）做的正是这件事：寻找使 <span displaypfx="inline-" class="mathjax-container">\(L(\theta|x)\)</span> 最大的参数。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/likelihood.png"><img class="alignnone size-full wp-image-40879" src="https://blog.gmem.cc/wp-content/uploads/2026/03/likelihood.png" alt="likelihood" width="100%" /></a></p>
<p>这里有一个必须严格区分的点：<span style="background-color: #c0c0c0;">似然不是参数的概率分布</span>。对固定数据来说， <span displaypfx="inline-" class="mathjax-container">\(L(\theta|x)\)</span> 不要求对 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 积分为 1，也不能直接解释为 <span displaypfx="inline-" class="mathjax-container">\(P(\theta|x)\)</span>。只有在贝叶斯框架里，把似然与先验（Prior） <span displaypfx="inline-" class="mathjax-container">\(p(\theta)\)</span> 相乘并做归一化之后，才得到参数的后验概率（Posterior）：</p>
<span displaypfx="" class="mathjax-container">\[p(\theta|x)=\frac{p(x|\theta)p(\theta)}{p(x)}\]</span>
<p>所以可以把三者关系记成一条清晰的链： <span displaypfx="inline-" class="mathjax-container">\(p(x|\theta)\)</span> 作为“关于 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的函数”时是概率模型；作为“关于 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的函数”时是似然函数；再结合先验并归一化后，才变成参数的后验概率。很多机器学习教材里说“最小化交叉熵等价于最大化对数似然”，本质上就是固定数据后，在参数空间里寻找最能解释样本的模型。</p>
<div class="blog_h3"><span class="graybg">机器学习视角</span></div>
<p>在机器学习模型里，这种“同一个式子，换个视角名字就变”的现象非常常见。设模型写成 <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y|x)\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 是输入或上下文， <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 是真实标签或真实 token， <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 是模型参数。</p>
<p>当参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 固定、把输出 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 当作随机变量时， <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y|x)\)</span> 是概率模型：它回答“在这个模型已经定好的前提下，不同输出有多可能”。例如语言模型会对整个词表输出一个条件概率分布，其中 <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 个位置之前的上下文（context）， <span displaypfx="inline-" class="mathjax-container">\(y_t\)</span> 表示该位置真实出现的 token，因此 <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y_t|c_t)\)</span> 就是“在上下文 <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span> 下，模型给真实 token <span displaypfx="inline-" class="mathjax-container">\(y_t\)</span> 分配的概率”。</p>
<p>当观测到的 <span displaypfx="inline-" class="mathjax-container">\((x,y)\)</span> 固定、把参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 当作变量时，同一个式子 <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y|x)\)</span> 就变成似然：它回答“哪些参数更能让这条已经看到的数据显得合理”。于是训练时最大化似然，就等价于最小化负对数似然：</p>
<span displaypfx="" class="mathjax-container">\[-\log p_\theta(y|x)\]</span>
<p>因此可以把这个困惑直接拆开：<span style="background-color: #c0c0c0;">固定参数看输出，它是概率；固定输出看参数，它是似然；对它取负对数后，它就是训练里使用的损失项</span>。在语言建模里，对一个具体 token 而言，给定当前模型参数后， <span displaypfx="inline-" class="mathjax-container">\(-\log p_\theta(y_t|c_t)\)</span> 既可以看成该 token 的负对数概率，也可以在训练语境下看成该 token 对参数的负对数似然；两者是同一个数值对象，只是观察角度不同。</p>
<div class="blog_h2"><span class="graybg">基本概率</span></div>
<p>概率（Probability）描述不确定事件发生的可能性。设样本空间（Sample Space）为 <span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span>，事件（Event）为 <span displaypfx="inline-" class="mathjax-container">\(A\subseteq\Omega\)</span>，则概率公理要求 <span displaypfx="inline-" class="mathjax-container">\(P(A)\in[0,1]\)</span>、<span displaypfx="inline-" class="mathjax-container">\(P(\Omega)=1\)</span>，以及互斥事件可加。这里“互斥事件可加”的意思是：如果两个事件不能同时发生，即 <span displaypfx="inline-" class="mathjax-container">\(A\cap B=\{\}\)</span>，那么“<span displaypfx="inline-" class="mathjax-container">\(A\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 发生”的概率就是两者概率直接相加：</p>
<span displaypfx="" class="mathjax-container">\[P(A\cup B)=P(A)+P(B),\quad A\cap B=\{\}\]</span>
<p>之所以能直接相加，是因为两者没有重叠部分，不会重复计数。若有重叠，则不能直接相加，而要减去交集 <span displaypfx="inline-" class="mathjax-container">\(P(A\cap B)\)</span>。</p>
<p>最小例子：掷一枚公平六面骰。样本空间是 <span displaypfx="inline-" class="mathjax-container">\(\Omega=\{1,2,3,4,5,6\}\)</span>。若事件 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 表示“点数为 1”，事件 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 表示“点数为 2”，则它们互斥，因此 <span displaypfx="inline-" class="mathjax-container">\(P(A\cup B)=1/6+1/6=1/3\)</span>。再看一个非互斥例子：若事件 <span displaypfx="inline-" class="mathjax-container">\(C\)</span> 表示“点数为偶数”，则 <span displaypfx="inline-" class="mathjax-container">\(C=\{2,4,6\}\)</span>，因此 <span displaypfx="inline-" class="mathjax-container">\(P(C)=3/6=1/2\)</span>。</p>
<div class="blog_h3"><span class="graybg">联合概率</span></div>
<p>联合概率（Joint Probability）表示多个事件同时发生的概率，即 <span displaypfx="inline-" class="mathjax-container">\(P(A,B)=P(A\cap B)\)</span>。它回答的是“这些条件一起成立的可能性有多大”。</p>
<span displaypfx="" class="mathjax-container">\[P(A,B)=P(A\cap B)\]</span>
<p>仍用骰子例子：令 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 表示“点数为偶数”，<span displaypfx="inline-" class="mathjax-container">\(B\)</span> 表示“点数至少为 4”，则 <span displaypfx="inline-" class="mathjax-container">\(A\cap B=\{4,6\}\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(P(A,B)=2/6=1/3\)</span>。在机器学习里，联合分布 <span displaypfx="inline-" class="mathjax-container">\(p(x,y)\)</span> 表示“输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 与标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 一起出现”的总体规律。</p>
<div class="blog_h3"><span class="graybg">独立性与乘法法则</span></div>
<p>独立（Independence）表示一个事件是否发生，不会改变另一个事件发生的概率。若事件 <span displaypfx="inline-" class="mathjax-container">\(A,B\)</span> 相互独立，则它们同时发生的概率可直接写成概率乘积：</p>
<span displaypfx="" class="mathjax-container">\[P(A\cap B)=P(A)P(B)\]</span>
<p>更一般地，若 <span displaypfx="inline-" class="mathjax-container">\(A_1,\dots,A_n\)</span> 相互独立，则</p>
<span displaypfx="" class="mathjax-container">\[P\!\left(\bigcap_{i=1}^{n}A_i\right)=\prod_{i=1}^{n}P(A_i)\]</span>
<p>例：掷一枚公平硬币并同时掷一枚公平骰子。令 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 表示“硬币为正面”，<span displaypfx="inline-" class="mathjax-container">\(B\)</span> 表示“骰子为偶数”，则 <span displaypfx="inline-" class="mathjax-container">\(P(A)=1/2\)</span>、<span displaypfx="inline-" class="mathjax-container">\(P(B)=1/2\)</span>，且二者独立，因此 <span displaypfx="inline-" class="mathjax-container">\(P(A\cap B)=1/2\times 1/2=1/4\)</span>。</p>
<p>独立时还可等价写成 <span displaypfx="inline-" class="mathjax-container">\(P(A|B)=P(A)\)</span>：知道 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 发生，并不会改变对 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 的判断。</p>
<p>要注意：互斥（Mutually Exclusive）和独立（Independent）不是一回事。互斥表示不能同时发生；独立表示是否发生彼此无关。对非零概率事件而言，互斥通常意味着<span style="background-color: #c0c0c0;">不独立</span>，因为一旦知道 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 发生，就立刻知道 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 不可能发生。</p>
<div class="blog_h3"><span class="graybg">条件概率</span></div>
<p>条件概率（Conditional Probability）表示“已知 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 发生时，<span displaypfx="inline-" class="mathjax-container">\(A\)</span> 发生的概率”：</p>
<span displaypfx="" class="mathjax-container">\[P(A|B)=\frac{P(A,B)}{P(B)},\quad P(B)>0\]</span>
<p>关键点不是“再算一次概率”，而是<span style="background-color: #c0c0c0;">样本空间被缩小了</span>。在上面的骰子例子中，已知 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>（点数至少为 4）后，只剩 <span displaypfx="inline-" class="mathjax-container">\(\{4,5,6\}\)</span> 三种可能，其中偶数是 <span displaypfx="inline-" class="mathjax-container">\(\{4,6\}\)</span>，所以 <span displaypfx="inline-" class="mathjax-container">\(P(A|B)=2/3\)</span>，明显不同于原来的 <span displaypfx="inline-" class="mathjax-container">\(P(A)=1/2\)</span>。</p>
<p>在建模中，几乎所有监督学习目标都可写成条件概率最大化，例如分类模型学习的是 <span displaypfx="inline-" class="mathjax-container">\(P(y|x)\)</span>：给定特征 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，标签 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 的概率有多大。</p>
<div class="blog_h3"><span class="graybg">边缘概率</span></div>
<p>边缘概率（Marginal Probability）是把不关心的变量“求和/积分掉”后得到的概率：</p>
<span displaypfx="" class="mathjax-container">\[P(A)=\sum_b P(A,b),\quad p(x)=\int p(x,z)\,dz\]</span>
<p>这两个公式的意思完全一致，只是分别对应离散情形与连续情形。 <span displaypfx="inline-" class="mathjax-container">\(P(A,b)\)</span> 表示“<span displaypfx="inline-" class="mathjax-container">\(A\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 同时发生”的联合概率；若现在只关心 <span displaypfx="inline-" class="mathjax-container">\(A\)</span>，就必须把所有可能的 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 都加起来。连续情形下同理： <span displaypfx="inline-" class="mathjax-container">\(p(x,z)\)</span> 是关于 <span displaypfx="inline-" class="mathjax-container">\((x,z)\)</span> 的联合密度，若只关心 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，就把所有 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 的可能性沿该维度积分掉，得到 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>。</p>
<p>几何上，可以把边缘化（Marginalization）理解为<span style="background-color: #c0c0c0;">把高维分布沿某个方向压扁后得到的投影</span>。设二维联合分布的两个维度分别是身高与体重，平面上的每个位置都对应一个联合概率密度 <span displaypfx="inline-" class="mathjax-container">\(p(\text{height},\text{weight})\)</span>。如果现在根本不关心体重，只想知道身高的总体分布，那么就把每个固定身高处、沿着体重方向的所有概率质量全部累加起来；累加后的结果就是该身高对应的边缘密度 <span displaypfx="inline-" class="mathjax-container">\(p(\text{height})\)</span>。</p>
<p>因此，求和符号 <span displaypfx="inline-" class="mathjax-container">\(\sum_b P(A,b)\)</span> 或积分符号 <span displaypfx="inline-" class="mathjax-container">\(\int p(x,z)\,dz\)</span> 的几何含义，不是“神秘地消掉一个变量”，而是<span style="background-color: #c0c0c0;">把那个维度上的所有可能性叠加到剩余维度上</span>。它像从侧面看一个三维物体的投影：原来的结构仍然在，但你只保留了当前关心的坐标轴信息。</p>
<p>在 AI 中，边缘概率几乎总伴随着隐藏变量（Latent Variable）。例如主题模型里 <span displaypfx="inline-" class="mathjax-container">\(z\)</span> 可以表示隐藏主题（Hidden Topic），<span displaypfx="inline-" class="mathjax-container">\(x\)</span> 表示某个词；若只关心词出现的总体概率，就要把主题变量消去，得到 <span displaypfx="inline-" class="mathjax-container">\(p(x)=\sum_z p(x,z)\)</span>。很多推断算法的难点，本质上就是这个“沿隐藏维度求和/积分”的步骤代价很高。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/marginzation.jpg"><img class="alignnone size-full wp-image-40833" src="https://blog.gmem.cc/wp-content/uploads/2026/03/marginzation.jpg" alt="marginzation" width="100%" /></a></p>
<div class="blog_h3"><span class="graybg">补事件</span></div>
<p>补事件（Complementary Event）满足</p>
<span displaypfx="" class="mathjax-container">\[P(A^c)=1-P(A)\]</span>
<p>它表示“<span displaypfx="inline-" class="mathjax-container">\(A\)</span> 不发生”的概率等于总概率 1 减去 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 发生的概率。</p>
<div class="blog_h3"><span class="graybg">全概率公式</span></div>
<p>第二，若 <span displaypfx="inline-" class="mathjax-container">\(A_1,\dots,A_n\)</span> 把样本空间划分为互斥且完备的几部分（即两两互斥，且并集为 <span displaypfx="inline-" class="mathjax-container">\(\Omega\)</span>），则全概率公式（Law of Total Probability）为</p>
<span displaypfx="" class="mathjax-container">\[P(B)=\sum_{i=1}^{n}P(B|A_i)P(A_i)\]</span>
<p>它的含义是：事件 <span displaypfx="inline-" class="mathjax-container">\(B\)</span> 的总概率，可以拆成“先落入哪一种原因 <span displaypfx="inline-" class="mathjax-container">\(A_i\)</span>，再在该原因下发生 <span displaypfx="inline-" class="mathjax-container">\(B\)</span>”的加权和。贝叶斯公式中的分母 <span displaypfx="inline-" class="mathjax-container">\(P(B)\)</span>，很多时候就是用全概率公式展开出来的。</p>
<div class="blog_h2"><span class="graybg">贝叶斯定理</span></div>
<p>贝叶斯定理（Bayes' Theorem）把“从原因到结果”的概率反转为“从结果反推原因”：</p>
<span displaypfx="" class="mathjax-container">\[P(A|B)=\frac{P(B|A)\,P(A)}{P(B)}\]</span>
<p>把式子写出来并不难，真正容易混淆的是每一项到底在说什么。下面直接用一个具体场景来解释：设 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 表示“患者真的患病”，<span displaypfx="inline-" class="mathjax-container">\(B\)</span> 表示“检测结果为阳性”。已知患病率为 1%，检测的灵敏度（Sensitivity）为 99%，假阳性率（False Positive Rate）为 5%。则：</p>
<ul>
<li>先验（Prior） <span displaypfx="inline-" class="mathjax-container">\(P(A)\)</span>：在还没看到这次检测结果前，对“此人患病”的原始判断。在这个例子里，先验就是总体患病率 <span displaypfx="inline-" class="mathjax-container">\(P(A)=0.01\)</span>。</li>
<li>似然（Likelihood） <span displaypfx="inline-" class="mathjax-container">\(P(B|A)\)</span>：如果一个人确实患病，那么检测为阳性的概率有多大。在这个例子里，检测灵敏度是 99%，因此 <span displaypfx="inline-" class="mathjax-container">\(P(B|A)=0.99\)</span>。</li>
<li>证据（Evidence） <span displaypfx="inline-" class="mathjax-container">\(P(B)\)</span>：不管一个人是否患病，检测结果为阳性在总体上出现的概率。它等于“真阳性 + 假阳性”的总和： <span displaypfx="inline-" class="mathjax-container">\(P(B)=0.99\times 0.01 + 0.05\times 0.99 = 0.0594\)</span>。</li>
<li>后验（Posterior） <span displaypfx="inline-" class="mathjax-container">\(P(A|B)\)</span>：在已经看到“检测阳性”这个证据之后，此人真正患病的更新后概率。代入上面的数值，有</li>
</ul>
<span displaypfx="" class="mathjax-container">\[P(\text{disease}|+) = \frac{0.99\times 0.01}{0.99\times 0.01 + 0.05\times 0.99} = \frac{1}{6} \approx 16.7\%\]</span>
<p>因此，贝叶斯定理说的不是“把公式套进去算一下”，而是一个非常具体的更新过程：<span style="background-color: #c0c0c0;">先从原始信念出发，用证据对它重新加权，再归一化，得到更新后的信念</span>。这也是为什么常把它概括成：<span style="background-color: #c0c0c0;">后验 = 似然 × 先验 ÷ 证据</span>。</p>
<p>这个例子最重要的结论是：检测阳性后，真实患病概率并不是 99%，而只有约 16.7%。原因不是检测太差，而是先验患病率本来就很低；假阳性虽然比例不高，但基数更大。换言之，<span style="background-color: #c0c0c0;">证据不会凭空决定结论，证据必须结合基线发生率一起解释</span>。在 AI 里，这就是“看到证据后更新信念”的统一公式：朴素贝叶斯分类、贝叶斯滤波、概率图模型都在做这件事。</p>
<div class="blog_h3"><span class="graybg">机器学习视角</span></div>
<p>从机器学习视角看，贝叶斯定理的价值在于它清楚地区分了<span style="background-color: #c0c0c0;">只看数据解释能力</span>与<span style="background-color: #c0c0c0;">把数据证据和先验知识合并起来</span>这两件事。前者对应似然（Likelihood）与最大似然估计（MLE），后者对应后验（Posterior）与最大后验估计（MAP）。</p>
<p>仍用上面的患病率例子。假设观测数据已经固定为“检测结果阳性”，把候选状态写成 <span displaypfx="inline-" class="mathjax-container">\(\theta\in\{\text{患病},\text{未患病}\}\)</span>。若只比较似然，则两个候选状态的评分分别是：</p>
<span displaypfx="" class="mathjax-container">\[L(\theta=\text{患病})=P(+|\text{患病})=0.99\]</span>
<span displaypfx="" class="mathjax-container">\[L(\theta=\text{未患病})=P(+|\text{未患病})=0.05\]</span>
<p>此时，按似然大小排序， <span displaypfx="inline-" class="mathjax-container">\(\theta=\text{患病}\)</span> 的确更优。这正是 MLE 式思路的核心：固定数据，寻找最能解释这份数据的候选参数或候选假设。但这里必须严格区分两件事：<span style="background-color: #c0c0c0;">“似然更大”只表示这个假设更能解释当前观测，不等于“它的后验概率就是 99%”</span>。因为似然没有把总体患病率只有 1% 这一先验事实算进去。</p>
<p>若进一步引入先验 <span displaypfx="inline-" class="mathjax-container">\(P(\text{患病})=0.01\)</span>、<span displaypfx="inline-" class="mathjax-container">\(P(\text{未患病})=0.99\)</span>，就得到后验比较：</p>
<span displaypfx="" class="mathjax-container">\[P(\text{患病}|+)\propto P(+|\text{患病})P(\text{患病})=0.99\times 0.01\]</span>
<span displaypfx="" class="mathjax-container">\[P(\text{未患病}|+)\propto P(+|\text{未患病})P(\text{未患病})=0.05\times 0.99\]</span>
<p>归一化之后， <span displaypfx="inline-" class="mathjax-container">\(P(\text{患病}|+)\approx 16.7\%\)</span>。这就是 MAP / 贝叶斯决策与单纯 MLE 的差别：后验判断不仅问“谁更能解释这份数据”，还问“谁在看到数据之前本来就更常见”。在类别极不平衡、小样本、先验知识明确或误判成本不对称的问题里，这个差别往往是决定性的。</p>
<p>朴素贝叶斯（Naive Bayes）正是把这套思路直接写成分类器。给定特征向量 <span displaypfx="inline-" class="mathjax-container">\(\boldsymbol{x}\)</span> 和类别 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>，它比较的是后验概率</p>
<span displaypfx="" class="mathjax-container">\[p(y|\boldsymbol{x})\propto p(y)\,p(\boldsymbol{x}|y)\]</span>
<p>再在条件独立假设下展开成</p>
<span displaypfx="" class="mathjax-container">\[p(y|\boldsymbol{x})\propto p(y)\prod_{j=1}^{d}p(x_j|y)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(p(y)\)</span> 是类别先验，负责表达“哪类样本本来就更常见”； <span displaypfx="inline-" class="mathjax-container">\(p(x_j|y)\)</span> 是条件似然，负责表达“若类别固定，这个特征出现得是否合理”。训练阶段通常先从数据中估计这些概率；最简单的做法是 MLE，即直接用频数比估计先验和条件概率。若再加入拉普拉斯平滑（Laplace Smoothing）或更一般的共轭先验（Conjugate Prior），则更接近 MAP 估计。预测阶段再把先验与似然相乘并归一化，得到后验分数最高的类别。</p>
<p>因此，从机器学习角度看，贝叶斯定理并不只是概率论中的一条恒等式，而是一个完整的建模分工：<span style="background-color: #c0c0c0;">似然负责解释数据，先验负责表达归纳偏置，后验负责把两者合成为最终判断</span>。后面的 MLE、MAP 与朴素贝叶斯，只是这套分工在不同任务上的具体实现。</p>
<div class="blog_h2"><span class="graybg">概率分布</span></div>
<p>这里的“分布（Distribution）”指随机变量取值的不确定性如何在取值空间上分配。严格地说，概率分布（Probability Distribution）是一种概率测度（Probability Measure），它为每个事件 <span displaypfx="inline-" class="mathjax-container">\(A\)</span> 赋予概率 <span displaypfx="inline-" class="mathjax-container">\(P(A)\)</span>。</p>
<p>对一维实值随机变量 <span displaypfx="inline-" class="mathjax-container">\(X\)</span>，分布最常用的统一表述是累积分布函数（Cumulative Distribution Function, CDF）<span displaypfx="inline-" class="mathjax-container">\(F(x)=P(X\le x)\)</span>。离散与连续的差别，主要体现在如何从 <span displaypfx="inline-" class="mathjax-container">\(F\)</span> 得到“点上/区间上”的概率。</p>
<p>不同任务对应不同的数据分布假设（Distribution Assumption）。分布选得对，建模与推断会更稳定；分布假设错得太远，参数估计、置信区间乃至损失函数解释都会失真。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot_six_basic_dist.png"><img class="alignnone size-full wp-image-40921" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot_six_basic_dist.png" alt="plot_six_basic_dist" width="1848" height="1473" /></a></p>
<div class="blog_h3"><span class="graybg">概率密度/概率质量函数</span></div>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 是离散型随机变量，其概率质量函数（Probability Mass Function, PMF）为 <span displaypfx="inline-" class="mathjax-container">\(p(x)=P(X=x)\)</span>，并满足 <span displaypfx="inline-" class="mathjax-container">\(\sum_x p(x)=1\)</span>。</p>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 是连续型随机变量，其概率密度函数（Probability Density Function, PDF）为 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>，满足 <span displaypfx="inline-" class="mathjax-container">\(p(x)\ge 0\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\int_{-\infty}^{+\infty} p(x)\,dx=1\)</span>；区间概率由积分给出：<span displaypfx="inline-" class="mathjax-container">\(P(a\le X\le b)=\int_a^b p(x)\,dx\)</span>。因此 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span> 本身不是“点上概率”。</p>
<p>两者都可写成同一 CDF 关系：连续情形 <span displaypfx="inline-" class="mathjax-container">\(F(x)=\int_{-\infty}^{x} p(t)\,dt\)</span>，离散情形 <span displaypfx="inline-" class="mathjax-container">\(F(x)=\sum_{t\le x} p(t)\)</span>。</p>
<div class="blog_h3"><span class="graybg">高斯分布（正态分布）</span></div>
<p>高斯分布（Gaussian / Normal Distribution）由均值 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 与方差 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 决定：</p>
<span displaypfx="" class="mathjax-container">\[p(x)=\frac{1}{\sqrt{2\pi}\sigma}\exp\!\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)\]</span>
<p>它的图像是熟悉的“钟形曲线（Bell Curve）”：离 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 越近，概率密度越高；离得越远，概率密度衰减越快。大量独立小扰动叠加后常近似高斯（中心极限定理的结果），所以测量误差、回归残差、传感器噪声、嵌入向量某些方向上的统计近似都常采用高斯模型。</p>
<p>例：若成年男性身高近似服从 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{N}(170,6^2)\)</span>，则 170 cm 附近最常见，而 190 cm 属于离均值超过 3 个标准差的少见样本。回归里假设残差服从高斯，本质上是在说“误差多数小、极大误差少”。</p>
<div class="blog_h3"><span class="graybg">拉普拉斯分布</span></div>
<p>拉普拉斯分布（Laplace Distribution）也是定义在实数轴上的连续分布，常用位置参数（Location） <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 与尺度参数（Scale） <span displaypfx="inline-" class="mathjax-container">\(b>0\)</span> 描述：</p>
<span displaypfx="" class="mathjax-container">\[p(x)=\frac{1}{2b}\exp\left(-\frac{|x-\mu|}{b}\right)\]</span>
<p>它的图像在中心点 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 处更尖、尾部比高斯分布更厚（Heavier Tails）。这表示模型更偏好“大多数误差很小，但偶尔出现相对较大偏差”这一类数据形状，因此它对离群点（Outliers）的容忍度通常高于高斯模型。</p>
<p>拉普拉斯分布和绝对误差（Absolute Error）关系非常紧密：若回归残差假设服从拉普拉斯分布，那么最大似然估计会导向 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 损失；若把参数先验设为拉普拉斯分布，最大后验估计则会导向 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 正则。这也是它在稀疏建模（Sparse Modeling）和鲁棒回归（Robust Regression）里很常见的原因。</p>
<p>例：若某传感器大多数时刻误差接近 0，但偶尔会因为抖动或遮挡产生较大的偏差，那么用拉普拉斯分布描述误差，往往比高斯分布更贴近这种“中心尖、尾部较厚”的统计形状。</p>
<div class="blog_h3"><span class="graybg">伯努利分布</span></div>
<p>伯努利分布（Bernoulli Distribution）描述一次二元结果试验（0/1）：</p>
<span displaypfx="" class="mathjax-container">\[P(X=1)=p,\quad P(X=0)=1-p\]</span>
<p>它是二分类标签建模的最小单元，也是逻辑回归与二分类交叉熵的概率基础。若 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 表示“用户是否点击广告”，则 <span displaypfx="inline-" class="mathjax-container">\(X=1\)</span> 代表点击、<span displaypfx="inline-" class="mathjax-container">\(X=0\)</span> 代表未点击，模型输出的 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 就是点击概率。</p>
<p>伯努利变量的期望是 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]=p\)</span>，方差是 <span displaypfx="inline-" class="mathjax-container">\(p(1-p)\)</span>。因此当 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 接近 0 或 1 时，不确定性反而更小；当 <span displaypfx="inline-" class="mathjax-container">\(p=0.5\)</span> 时，不确定性最大。</p>
<p>如果把同一个伯努利试验独立重复 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 次，并把成功次数加总，那么得到的就不是伯努利分布，而是二项分布（Binomial Distribution）。换句话说，伯努利分布描述“单次是否成功”，二项分布描述“总共成功了多少次”。</p>
<div class="blog_h3"><span class="graybg">二项分布</span></div>
<p>二项分布（Binomial Distribution）描述 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 次独立伯努利试验中的成功次数。若每次成功概率都是 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，随机变量 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 表示总成功次数，则</p>
<span displaypfx="" class="mathjax-container">\[P(X=k)={n \choose k}p^k(1-p)^{n-k},\quad k=0,1,\dots,n\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\({n \choose k}\)</span> 表示“从 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 次试验里选出 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 次成功”的组合数；后面的 <span displaypfx="inline-" class="mathjax-container">\(p^k(1-p)^{n-k}\)</span> 表示某一种具体排列出现的概率。两者相乘，就得到“恰好成功 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 次”的总概率。</p>
<p>如果 <span displaypfx="inline-" class="mathjax-container">\(X_1,\dots,X_n\)</span> 独立同分布且 <span displaypfx="inline-" class="mathjax-container">\(X_i\sim\mathrm{Bernoulli}(p)\)</span>，那么它们的和</p>
<span displaypfx="" class="mathjax-container">\[S_n=\sum_{i=1}^{n}X_i\sim\mathrm{Binomial}(n,p)\]</span>
<p>因此，二项分布本质上就是“多个伯努利随机变量求和后的分布”。它的期望是 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]=np\)</span>，方差是 <span displaypfx="inline-" class="mathjax-container">\(np(1-p)\)</span>。</p>
<p>例：若一枚硬币正面概率为 <span displaypfx="inline-" class="mathjax-container">\(p=0.7\)</span>，连续抛 10 次，则“正面出现几次”服从二项分布。此时恰好出现 7 次正面的概率是</p>
<span displaypfx="" class="mathjax-container">\[P(X=7)={10 \choose 7}0.7^7 0.3^3\]</span>
<div class="blog_h3"><span class="graybg">多项式分布</span></div>
<p>多项式分布（Multinomial Distribution）是“多类别计数版”的伯努利：进行 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 次独立试验，类别概率为 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{p}\)</span>，计数向量 <span displaypfx="inline-" class="mathjax-container">\(\mathbf{k}\)</span> 的概率为</p>
<span displaypfx="" class="mathjax-container">\[P(\mathbf{k})=\frac{n!}{\prod_i k_i!}\prod_i p_i^{k_i},\quad \sum_i k_i=n\]</span>
<p>若只有一次抽样，通常写作分类分布（Categorical Distribution）：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/distribution-categorical.png"><img class="alignnone size-full wp-image-40867" src="https://blog.gmem.cc/wp-content/uploads/2026/03/distribution-categorical.png" alt="distribution-categorical" width="100%" /></a></p>
<p>若把一次抽样重复 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 次并统计每类出现次数，就得到多项式分布，语言模型里的词频计数、主题模型中的词袋（Bag of Words）都与这种计数视角一致。概率分布一节的第三个图就是多项式分布：总计抽样6次，横轴表示分类1的次数，纵轴表示分类2的次数（因为只有3分类，因此不需要绘制三维），颜色表示抽取到不同分类的次数组合的概率。</p>
<div class="blog_h3"><span class="graybg">泊松分布</span></div>
<p>泊松分布（Poisson Distribution）描述单位时间或单位空间内稀有事件的发生次数：</p>
<span displaypfx="" class="mathjax-container">\[P(X=k)=e^{-\lambda}\frac{\lambda^k}{k!},\quad k=0,1,2,\ldots\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span> 既是平均发生次数，也是方差。它适用于“事件相互独立、平均速率稳定、短时间内多次同时发生概率很小”的场景，例如单位分钟请求数、单位小时故障数、单位页面点击数。</p>
<p>例：若一个接口平均每分钟收到 3 次请求，则 <span displaypfx="inline-" class="mathjax-container">\(\lambda=3\)</span>。此时一分钟内 0 次请求的概率是 <span displaypfx="inline-" class="mathjax-container">\(e^{-3}\approx 0.05\)</span>；5 次请求的概率是 <span displaypfx="inline-" class="mathjax-container">\(e^{-3}3^5/5!\approx 0.10\)</span>。泊松分布常被用来做“到达数 / 故障数 / 事件数”建模。</p>
<div class="blog_h2"><span class="graybg">期望</span></div>
<p>期望（Expectation）是随机变量的“按概率加权的平均”，回答“长期来看这个量的典型水平在哪里”。离散情形下，它把每个可能取值按对应概率加权；连续情形下，则对概率密度做积分：</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[X]=\sum_x x\,p(x)\ \ (\text{或 } \int x\,p(x)\,dx)\]</span>
<p><span style="background-color: #c0c0c0;">期望和均值很像，但不是同一个概念。</span> 期望（Expectation）是总体分布层面的量，由概率模型 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span> 决定，描述“如果无限次重复抽样，平均会稳定到哪里”；均值（Mean）通常有两种语境：一是把总体的中心位置也叫“总体均值”，这时它与期望是同一个量；二是指有限样本算出来的样本均值（Sample Mean） <span displaypfx="inline-" class="mathjax-container">\(\bar{x}=\frac{1}{n}\sum_{i=1}^{n}x_i\)</span>，这是用样本近似期望的统计量。</p>
<p>因此可以把三者关系记成：<span style="background-color: #c0c0c0;">总体均值 = 期望；样本均值 = 对期望的估计</span>。例如公平骰子点数的期望是 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]=3.5\)</span>；如果你真的掷 6 次，样本均值可能是 3、4 或 4.5，不必恰好等于 3.5，但随着试验次数增多，它会越来越接近期望。</p>
<p>期望最重要的性质之一是线性性（Linearity）：<span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[aX+b]=a\mathbb{E}[X]+b\)</span>，不要求独立。例：公平骰子点数 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 的期望是 <span displaypfx="inline-" class="mathjax-container">\((1+2+3+4+5+6)/6=3.5\)</span>；若收入模型写成 <span displaypfx="inline-" class="mathjax-container">\(Y=2X+1\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[Y]=2\times 3.5+1=8\)</span>。</p>
<div class="blog_h2"><span class="graybg">矩（Moments）</span></div>
<p>矩（Moment）是概率统计里用来概括分布形状的一组数。理论上，“第 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 阶”指对 <span displaypfx="inline-" class="mathjax-container">\(k\)</span> 次幂取期望：矩天然与期望/均值绑定。</p>
<p>常用两类定义是：</p>
<ul>
<li>原点矩（Raw Moment / Non-central Moment）：<span displaypfx="inline-" class="mathjax-container">\(m_k=\mathbb{E}[X^k]\)</span>（以 0 为参照）。</li>
<li>中心矩（Central Moment）：<span displaypfx="inline-" class="mathjax-container">\(\mu_k=\mathbb{E}\big[(X-\mathbb{E}[X])^k\big]\)</span>（以均值为参照）。</li>
</ul>
<p>直觉上可以用“跷跷板”来理解：把概率质量看作分布在数轴上的“重量”，期望/均值决定“支点放哪里”才平衡；更高阶矩描述“重量围绕支点如何分布”。</p>
<p>一阶矩（First Moment）回答“中心在哪里”：均值/期望 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]\)</span> 给出平衡点。但<span style="background-color: #c0c0c0;">一阶矩无法刻画离散程度</span>：例如 <span displaypfx="inline-" class="mathjax-container">\(X\in\{-1,+1\}\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(X\in\{-10,+10\}\)</span> 都满足 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]=0\)</span>，但后者离中心更远。</p>
<p>二阶量用平方偏差的期望刻画离散程度：平方保证非负、避免正负偏差相互抵消，并对远离均值的样本赋予更大权重。在统计里，这对应二阶中心矩——方差 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(X)=\mathbb{E}[(X-\mathbb{E}[X])^2]\)</span>；在力学类比里，这与转动惯量（Moment of Inertia）中按 <span displaypfx="inline-" class="mathjax-container">\(r^2\)</span> 加权的直觉一致：远处的“重量”对系统的“难摆动/难转动”贡献更大。</p>
<p>二阶原点矩与方差之间满足恒等式：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(X)=\mathbb{E}\big[(X-\mathbb{E}[X])^2\big]=\mathbb{E}[X^2]-\mathbb{E}[X]^2\]</span>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[X^2]=\mathrm{Var}(X)+\mathbb{E}[X]^2\]</span>
<p>它说明二阶原点矩 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X^2]\)</span> 以 0 为参照，会把两类效应叠加在一起：一是分布整体离 0 的偏移（由均值项 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]^2\)</span> 表示），二是围绕自身均值的离散/抖动（由方差项 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(X)\)</span> 表示）。因此 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X^2]\)</span> 变大并不等价于“波动更大”；把所有取值整体平移一个常数，方差不变，但二阶原点矩会随偏移显著改变。</p>
<p>一个最小例子可以把差异看得非常清楚。令 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 等概率取值 <span displaypfx="inline-" class="mathjax-container">\(\{99,100,101\}\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[X]=\frac{99+100+101}{3}=100,\quad \mathbb{E}[X]^2=100^2=10000\]</span>
<span displaypfx="" class="mathjax-container">\[\mathbb{E}[X^2]=\frac{99^2+100^2+101^2}{3}=\frac{30002}{3}\approx 10000.67\]</span>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(X)=\mathbb{E}[X^2]-\mathbb{E}[X]^2=\frac{30002}{3}-10000=\frac{2}{3}\]</span>
<p>可以直接读成一句话：这组数“离 0 很远”（所以 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X^2]\)</span> 很大），但“围绕自身均值很稳定”（所以方差很小）。</p>
<p>这也解释了 Adam 里的常见误解。Adam 说的“一阶矩/二阶矩”是把 mini-batch 梯度 <span displaypfx="inline-" class="mathjax-container">\(g\)</span> 当作随机变量，分别估计 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g]\)</span> 与 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\)</span>（逐参数维度）。它用的是二阶原点矩 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]\)</span>，不是方差 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(g)\)</span>。原因很直接：如果梯度在一段时间内几乎恒定（例如每步都是 <span displaypfx="inline-" class="mathjax-container">\(g=10\)</span>），那么方差为 0，拿它做除法会造成数值灾难；但 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[g^2]=100\)</span> 给出了稳定的“绝对尺度”，使更新项 <span displaypfx="inline-" class="mathjax-container">\(\frac{\mathbb{E}[g]}{\sqrt{\mathbb{E}[g^2]}}\)</span> 仍然是有界的（再加上 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 做数值稳定）。</p>
<div class="blog_h2"><span class="graybg">方差</span></div>
<p>方差（Variance）衡量随机变量围绕均值的波动大小。它不是“偏离均值后再平均”，而是“偏离均值的平方再平均”；平方的作用是避免正负抵消，并放大大偏差：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Var}(X)=\mathbb{E}[(X-\mu)^2]\]</span>
<p>因此，方差小表示样本更集中在均值附近；方差大表示波动更强。它刻画的是“不确定性的尺度”，而不是取值本身的大小。例如两个模型的平均误差相同，方差更大的那个模型，输出往往更不稳定。</p>
<div class="blog_h2"><span class="graybg">协方差</span></div>
<p>协方差（Covariance）描述两个变量是否倾向同向变化：</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Cov}(X,Y)=\mathbb{E}[(X-\mu_X)(Y-\mu_Y)]\]</span>
<p>若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Cov}(X,Y)>0\)</span>，说明 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 大时 <span displaypfx="inline-" class="mathjax-container">\(Y\)</span> 也倾向变大；若 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Cov}(X,Y)<0[/latex]，说明一个变大时另一个倾向变小。直觉例子是：身高与体重常为正协方差，室外温度与暖气功率常为负协方差。</p>
<p>还要区分一点：协方差为 0 不必然意味着独立。例如令 [latex syntax=inline]X\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(\{-1,1\}\)</span> 上等概率取值，定义 <span displaypfx="inline-" class="mathjax-container">\(Y=X^2\)</span>，则 <span displaypfx="inline-" class="mathjax-container">\(Y\)</span> 被 <span displaypfx="inline-" class="mathjax-container">\(X\)</span> 完全决定，但 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Cov}(X,Y)=0\)</span>。因此“零相关”比“独立”弱得多。</p>
<p>协方差矩阵（Covariance Matrix）<span displaypfx="inline-" class="mathjax-container">\(\Sigma\)</span> 在 AI 中极其关键：PCA、马氏距离（Mahalanobis Distance）、卡尔曼滤波与高斯模型都直接依赖它。它编码的是“各方向上的尺度”与“不同维度之间的耦合”。</p>
<div class="blog_h2"><span class="graybg">标准差（Standard Deviation）</span></div>
<p>标准差（Standard Deviation）衡量数据相对均值（Mean）的离散程度（Dispersion）。总体标准差（Population Standard Deviation）定义为：</p>
<span displaypfx="" class="mathjax-container">\[\sigma=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(x_i-\mu)^2}\]</span>
<p>样本标准差（Sample Standard Deviation）使用贝塞尔校正（Bessel's Correction）：</p>
<span displaypfx="" class="mathjax-container">\[s=\sqrt{\frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})^2}\]</span>
<p>先平方再平均是为了避免正负抵消并加重大偏差；最后开平方是为了把单位恢复到原始尺度。若直接用方差，单位会变成“平方单位”，解释往往不直观；标准差则可以直接说成“典型偏离均值大约多少个原始单位”。</p>
<p>例：数据 <span displaypfx="inline-" class="mathjax-container">\(\{2,4,4,4,5,5,7,9\}\)</span> 的均值是 <span displaypfx="inline-" class="mathjax-container">\(5\)</span>，总体方差是 <span displaypfx="inline-" class="mathjax-container">\(4\)</span>，标准差是 <span displaypfx="inline-" class="mathjax-container">\(2\)</span>。这意味着一个典型样本与均值的偏离量级大约是 2，而不是每个点都恰好偏 2。</p>
<p>标准差还常被用来做标准化（Standardization）。例如某考试分数 80 分，班级均值 70、标准差 5，则它的 z-score 是 <span displaypfx="inline-" class="mathjax-container">\((80-70)/5=2\)</span>，表示它比均值高 2 个标准差。近似正态分布下，经验上约有 68% 样本落在 <span displaypfx="inline-" class="mathjax-container">\(\mu\pm\sigma\)</span>，95% 落在 <span displaypfx="inline-" class="mathjax-container">\(\mu\pm2\sigma\)</span>，99.7% 落在 <span displaypfx="inline-" class="mathjax-container">\(\mu\pm3\sigma\)</span>；这就是常见的 68-95-99.7 规则。</p>
<div class="blog_h2"><span class="graybg">最大似然估计（MLE）</span></div>
<p>最大似然估计（Maximum Likelihood Estimation, MLE）先把已经观察到的数据（例如上面抛硬币10次有8次正面）固定住，再在参数空间里寻找“最能生成这批数据”的参数。设数据集为 <span displaypfx="inline-" class="mathjax-container">\(D=\{x_1,\dots,x_n\}\)</span>，参数为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，则定义是：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\theta}_{\mathrm{MLE}}=\arg\max_{\theta}p(D|\theta)\]</span>
<p>其中<span displaypfx="inline-" class="mathjax-container">\(p(D|\theta)\)</span> 是“参数取 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 时看到整批数据 <span displaypfx="inline-" class="mathjax-container">\(D\)</span> 的概率/密度”； <span displaypfx="inline-" class="mathjax-container">\(\arg\max\)</span> 表示“找出让这个值最大的那个参数”。</p>
<p>若样本独立同分布（不相关且在同一个分布，例如抛硬币中10个独立事件。Independent and Identically Distributed, i.i.d.），联合似然可以拆成单个样本似然的乘积：</p>
<span displaypfx="" class="mathjax-container">\[p(D|\theta)=\prod_{i=1}^{n}p(x_i|\theta)\]</span>
<p>于是 MLE 常写成</p>
<span displaypfx="" class="mathjax-container">\[\hat{\theta}_{\mathrm{MLE}}=\arg\max_{\theta}\prod_{i=1}^{n}p(x_i|\theta)\]</span>
<p>实际训练几乎总是改为最大化对数似然（Log-Likelihood）：</p>
<span displaypfx="" class="mathjax-container">\[\ell(\theta)=\log p(D|\theta)=\sum_{i=1}^{n}\log p(x_i|\theta)\]</span>
<p><span style="background-color: #c0c0c0;">取对数不会改变最优解</span>，因为 <span displaypfx="inline-" class="mathjax-container">\(\log\)</span> 是单调递增函数；它只是把难处理的连乘变成易处理的求和。因此，最大化似然与最小化负对数似然（Negative Log-Likelihood, NLL）完全等价。</p>
<p>继续看抛硬币的例子。设单次结果 <span displaypfx="inline-" class="mathjax-container">\(x_i\in\{0,1\}\)</span>，其中 1 表示正面，0 表示反面。设正面概率为 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，那么单个样本就服从伯努利分布（Bernoulli Distribution）。它的概率质量函数（Probability Mass Function, PMF，用于描述离散随机变量，直接给出概率。对应概率密度函数PDF，给出给定连续随机变量对应位置的密度，因为连续，特定点的概率必须为0，只能密度和变量范围积分得到概率）写成：</p>
<span displaypfx="" class="mathjax-container">\[p(x_i|\theta)=\theta^{x_i}(1-\theta)^{1-x_i},\quad x_i\in\{0,1\}\]</span>
<p>左边的 <span displaypfx="inline-" class="mathjax-container">\(p(\cdot|\theta)\)</span> 里的 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是“概率分布/概率质量函数”的记号， <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 是模型参数，也就是“正面概率”。很多教材会把参数也记成 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，写成 <span displaypfx="inline-" class="mathjax-container">\(p(x_i|p)\)</span>；这并不算错，因为参数本身就是一个概率，但两个 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 同时出现时很容易视觉混淆，所以这里改用 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 来区分“模型符号”和“参数符号”。</p>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(x_i=1\)</span> 时，</p>
<span displaypfx="" class="mathjax-container">\[p(x_i=1|\theta)=\theta^1(1-\theta)^0=\theta\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(x_i=0\)</span> 时，</p>
<span displaypfx="" class="mathjax-container">\[p(x_i=0|\theta)=\theta^0(1-\theta)^1=1-\theta\]</span>
<p>如果 10 次里看到 7 次正面、3 次反面，那么整批数据的似然就是：</p>
<span displaypfx="" class="mathjax-container">\[p(D|\theta)=\theta^7(1-\theta)^3\]</span>
<p>对应的对数似然是</p>
<span displaypfx="" class="mathjax-container">\[\ell(\theta)=7\log \theta+3\log(1-\theta)\]</span>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-binomial-loglik.png"><img class="alignnone size-full wp-image-41091" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-binomial-loglik.png" alt="plot-binomial-loglik" width="100%" /></a></p>
<p>我们现在要最大化似然，需要对 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 求导并令导数为 0：</p>
<span displaypfx="" class="mathjax-container">\[\frac{d\ell(\theta)}{d\theta}=\frac{7}{\theta}-\frac{3}{1-\theta}=0\Rightarrow \theta=0.7\]</span>
<p>所以 <span displaypfx="inline-" class="mathjax-container">\(\hat{\theta}_{\mathrm{MLE}}=0.7\)</span>。即“在所有候选 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 里， <span displaypfx="inline-" class="mathjax-container">\(\theta=0.7\)</span> 最能让 7 正 3 反这份数据显得不奇怪”。</p>
<div class="blog_h3"><span class="graybg">从概率建模到损失函数</span></div>
<p>在机器学习里，损失函数往往由<span style="background-color: #c0c0c0;">概率建模假设（Probabilistic Modeling Assumption）</span>诱导出来。做法是先写下观测数据在参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 下的概率模型，再把训练集的负对数似然（Negative Log-Likelihood, NLL）当作优化目标。于是，最小化损失就不再只是一个工程规定，而是等价于最大化“已观测数据在模型下出现的可能性”。</p>
<p>以最常见的回归假设为例，若模型输出写成 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span>，并假设真实标签满足</p>
<span displaypfx="" class="mathjax-container">\[y=f_\theta(x)+\varepsilon,\qquad \varepsilon\sim\mathcal{N}(0,\sigma^2)\]</span>
<p>这里的高斯分布指的不是 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 本身，也不是 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span> 本身，而是<span style="background-color: #c0c0c0;">误差项，也就是残差 <span displaypfx="inline-" class="mathjax-container">\(y-f_\theta(x)\)</span> 的分布</span>。误差总是相对于模型当前给出的预测中心 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span> 来计算：预测值附近最可能出现，偏离越远，概率越低。</p>
<p>从这个误差模型出发，可以把“误差分布”直接改写成“真实标签在给定输入下的条件分布”。因为 <span displaypfx="inline-" class="mathjax-container">\(\varepsilon\)</span> 服从均值为 0 的高斯分布，所以等价地，真实标签服从一个均值为 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span> 的高斯分布：</p>
<span displaypfx="" class="mathjax-container">\[y\,|\,x \sim \mathcal{N}(f_\theta(x),\sigma^2)\]</span>
<p>这条式子的含义是：给定输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 之后，模型并不把输出 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 看成一个确定值，而是把它看成一个<span style="background-color: #c0c0c0;">以预测值 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span> 为中心、方差为 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 的高斯随机变量</span>。根据高斯分布的概率密度函数，可进一步写出条件概率密度：</p>
<span displaypfx="" class="mathjax-container">\[p_\theta(y|x)=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{(y-f_\theta(x))^2}{2\sigma^2}\right)\]</span>
<p>这时 <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y_i|x_i)\)</span> 的含义就自然了：它评估的是在当前参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 下，训练集中已经观测到的真实标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span>，作为“围绕 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 摆动的一个样本”是否足够合理。被评分的始终是<span style="background-color: #c0c0c0;">真实观测到的 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span></span>，而不是模型预测出来的 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span>。<span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 的作用不是直接成为被评分对象，而是作为条件分布的均值或位置参数，决定观测值最可能出现的位置。</p>
<p>这种思路并不限于高斯回归。分类任务里，条件分布不再是高斯，而会改成 Bernoulli 或 Categorical；若把噪声假设换成拉普拉斯分布（Laplacian Distribution），则导出的损失也会从平方误差变成绝对误差。更严谨地说，<span style="background-color: #c0c0c0;">损失函数是概率模型在训练集上的负对数似然写开后的结果</span>：高斯噪声导出平方误差（Squared Error），拉普拉斯噪声导出绝对误差（Absolute Error），Bernoulli 与 Categorical 分布则导出二分类或多分类交叉熵（Cross-Entropy）。</p>
<div class="blog_h3"><span class="graybg">高斯分布的MLE和最小二乘</span></div>
<p>先看监督回归（Supervised Regression）。训练集写成 <span displaypfx="inline-" class="mathjax-container">\(D=\{(x_i,y_i)\}_{i=1}^N\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 是输入特征，<span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 是真实输出。按照上面的统一框架，这里仍然是把 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 当作给定条件，只为输出建模条件分布 <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y|x)\)</span>，并让真实观测到的 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 在模型下尽可能不意外：</p>
<span displaypfx="" class="mathjax-container">\[\hat\theta_{\mathrm{MLE}}=\arg\max_{\theta}\prod_{i=1}^{N}p_\theta(y_i|x_i)\]</span>
<p>这里的参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 不在 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 里，而是出现在假设函数 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span> 里。例如线性回归（Linear Regression）可写成 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)=\mathbf{w}^\top x+b\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\theta=(\mathbf{w},b)\)</span>。回归里常说的“高斯分布”指的不是整张训练集的 <span displaypfx="inline-" class="mathjax-container">\(y\)</span> 服从高斯分布，也不是模型预测值 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\)</span> 本身服从高斯分布，而是指<span style="background-color: #c0c0c0;">误差项，也就是残差 <span displaypfx="inline-" class="mathjax-container">\(y_i-f_\theta(x_i)\)</span> 的分布</span>。误差总是相对于当前模型给出的中心值 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 来计算。</p>
<p>具体假设是：</p>
<span displaypfx="" class="mathjax-container">\[y_i=f_\theta(x_i)+\varepsilon_i,\quad \varepsilon_i\sim\mathcal{N}(0,\sigma^2)\ \text{i.i.d.}\]</span>
<p>等价写成条件分布：</p>
<span displaypfx="" class="mathjax-container">\[y_i\,|\,x_i \sim \mathcal{N}(f_\theta(x_i),\sigma^2)\]</span>
<p>上式的意思是：在给定输入 <span displaypfx="inline-" class="mathjax-container">\(x_i\)</span> 的条件下，输出 <span displaypfx="inline-" class="mathjax-container">\(y_i\)</span> 不再被视为确定值，而被建模为随机变量，并且服从均值为 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span>、方差为 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 的一维高斯分布（Gaussian Distribution），当然，这里的不确定性就是模型引入的。根据高斯分布的概率密度公式，并结合 i.i.d. 假设（联合似然为逐样本似然的乘积），整批数据的似然为</p>
<span displaypfx="" class="mathjax-container">\[p(D|\theta,\sigma^2)=\prod_{i=1}^{N}\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{(y_i-f_\theta(x_i))^2}{2\sigma^2}\right)\]</span>
<p>取负对数似然（Negative Log-Likelihood, NLL）得到训练时真正被最小化的量：</p>
<span displaypfx="" class="mathjax-container">\[-\log p(D|\theta,\sigma^2)=\frac{N}{2}\log(2\pi\sigma^2)+\frac{1}{2\sigma^2}\sum_{i=1}^{N}(y_i-f_\theta(x_i))^2\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 视为常数时，第一项与 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 无关，因此最小化 NLL 关于 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 等价于最小化平方误差和（Sum of Squared Errors, SSE）：</p>
<span displaypfx="" class="mathjax-container">\[\hat\theta_{\mathrm{MLE}}=\arg\min_\theta\sum_{i=1}^{N}(y_i-f_\theta(x_i))^2\]</span>
<p>把残差（Residual）记为 <span displaypfx="inline-" class="mathjax-container">\(r_i=y_i-f_\theta(x_i)\)</span>，则上式就是最小化 <span displaypfx="inline-" class="mathjax-container">\(\sum_i r_i^2\)</span>。因此，在“高斯噪声 + 固定方差”的回归假设下，<span style="background-color: #c0c0c0;">最大化似然（等价最大化对数似然）就是最小化残差平方和</span>。</p>
<p>这就是“最小二乘（Least Squares）”为什么会从概率建模里自然出现；均方误差（Mean Squared Error, MSE）只是把 SSE 再除以 <span displaypfx="inline-" class="mathjax-container">\(N\)</span>，不改变最优解。</p>
<p>如果 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 也未知，则 MLE 会同时估计 <span displaypfx="inline-" class="mathjax-container">\((\theta,\sigma^2)\)</span>。在固定 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 时，对 <span displaypfx="inline-" class="mathjax-container">\(\sigma^2\)</span> 的 MLE 为</p>
<span displaypfx="" class="mathjax-container">\[\hat\sigma^2_{\mathrm{MLE}}=\frac{1}{N}\sum_{i=1}^{N}(y_i-f_\theta(x_i))^2\]</span>
<p>注意这里是 <span displaypfx="inline-" class="mathjax-container">\(1/N\)</span>（MLE），而不是无偏估计常用的 <span displaypfx="inline-" class="mathjax-container">\(1/(N-1)\)</span>。</p>
<p>顺带一提：把回归模型退化为“常数预测” <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)\equiv \mu\)</span>，就回到“在高斯噪声下估计均值 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span>”，其 MLE 是样本均值。这也是很多教材先从 <span displaypfx="inline-" class="mathjax-container">\(x_i=\mu+\varepsilon_i\)</span> 入手的原因：那是回归的一个最小特例（此时 <span displaypfx="inline-" class="mathjax-container">\(\theta=\mu\)</span>）。</p>
<div class="blog_h3"><span class="graybg">Bernoulli / Categorical 分布和交叉熵</span></div>
<p>分类任务与回归不同：标签通常是离散变量，因此这里使用的不是概率密度函数（Probability Density Function, PDF），而是概率质量函数（Probability Mass Function, PMF）。做最大似然估计时，最常见的做法同样是把输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 视为给定条件，只为标签建模条件分布 <span displaypfx="inline-" class="mathjax-container">\(p_\theta(y|x)\)</span>，然后让已观测标签在模型下尽可能“显得合理”。</p>
<p>先看二分类（Binary Classification）。设标签 <span displaypfx="inline-" class="mathjax-container">\(y_i\in\{0,1\}\)</span>，模型输出正类概率 <span displaypfx="inline-" class="mathjax-container">\(\pi_\theta(x_i)=p_\theta(y_i=1|x_i)\)</span>，则单个样本服从伯努利分布（Bernoulli Distribution）：</p>
<span displaypfx="" class="mathjax-container">\[p_\theta(y_i|x_i)=\pi_\theta(x_i)^{y_i}\bigl(1-\pi_\theta(x_i)\bigr)^{1-y_i}\]</span>
<p>在 i.i.d. 假设下，整批数据的似然为</p>
<span displaypfx="" class="mathjax-container">\[p(D|\theta)=\prod_{i=1}^{N}\pi_\theta(x_i)^{y_i}\bigl(1-\pi_\theta(x_i)\bigr)^{1-y_i}\]</span>
<p>取负对数似然，得到</p>
<span displaypfx="" class="mathjax-container">\[-\log p(D|\theta)=-\sum_{i=1}^{N}\Big[y_i\log \pi_\theta(x_i)+(1-y_i)\log\bigl(1-\pi_\theta(x_i)\bigr)\Big]\]</span>
<p>这正是二分类交叉熵（Binary Cross-Entropy, BCE）。因此，在“标签服从伯努利分布”的假设下，<span style="background-color: #c0c0c0;">最大化似然等价于最小化二分类交叉熵</span>。若再把 <span displaypfx="inline-" class="mathjax-container">\(\pi_\theta(x)\)</span> 写成 sigmoid 作用在 logit <span displaypfx="inline-" class="mathjax-container">\(z_\theta(x)\)</span> 上，即 <span displaypfx="inline-" class="mathjax-container">\(\pi_\theta(x)=\sigma(z_\theta(x))\)</span>，就得到工程实现里最常见的 sigmoid + BCE 训练形式。</p>
<p>再看多分类（Multiclass Classification）。设类别总数为 <span displaypfx="inline-" class="mathjax-container">\(C\)</span>，并用 one-hot 向量 <span displaypfx="inline-" class="mathjax-container">\(y_i=(y_{i1},\dots,y_{iC})\)</span> 表示真实标签，其中只有真实类别对应那一项为 1，其余为 0。若模型输出类别概率 <span displaypfx="inline-" class="mathjax-container">\(\pi_{\theta,1}(x_i),\dots,\pi_{\theta,C}(x_i)\)</span>，且满足 <span displaypfx="inline-" class="mathjax-container">\(\sum_{c=1}^{C}\pi_{\theta,c}(x_i)=1\)</span>，则单个样本服从类别分布（Categorical Distribution）：</p>
<span displaypfx="" class="mathjax-container">\[p_\theta(y_i|x_i)=\prod_{c=1}^{C}\pi_{\theta,c}(x_i)^{y_{ic}}\]</span>
<p>整批数据的似然为</p>
<span displaypfx="" class="mathjax-container">\[p(D|\theta)=\prod_{i=1}^{N}\prod_{c=1}^{C}\pi_{\theta,c}(x_i)^{y_{ic}}\]</span>
<p>取负对数似然，得到</p>
<span displaypfx="" class="mathjax-container">\[-\log p(D|\theta)=-\sum_{i=1}^{N}\sum_{c=1}^{C}y_{ic}\log \pi_{\theta,c}(x_i)\]</span>
<p>这正是多分类交叉熵（Categorical Cross-Entropy）。若标签是标准 one-hot，那么每个样本只会保留真实类别那一项，损失就退化成 <span displaypfx="inline-" class="mathjax-container">\(-\log \pi_{\theta,c^*}(x_i)\)</span>；若标签本身是软标签（Soft Label），则公式不变，只是 <span displaypfx="inline-" class="mathjax-container">\(y_{ic}\)</span> 不再只有 0 和 1，而是一个分布。</p>
<div class="blog_h3"><span class="graybg">拉普拉斯分布的MLE和绝对误差</span></div>
<p>若回归任务不再假设误差服从高斯分布，而是假设误差项 <span displaypfx="inline-" class="mathjax-container">\(\varepsilon\)</span> 服从拉普拉斯分布（Laplacian Distribution），即</p>
<span displaypfx="" class="mathjax-container">\[y_i=f_\theta(x_i)+\varepsilon_i,\qquad \varepsilon_i\sim \mathrm{Laplace}(0,b)\ \text{i.i.d.}\]</span>
<p>那么等价地，真实标签在给定输入后的条件分布可写成</p>
<span displaypfx="" class="mathjax-container">\[y_i\,|\,x_i \sim \mathrm{Laplace}(f_\theta(x_i),b)\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x_i)\)</span> 是位置参数（Location Parameter），<span displaypfx="inline-" class="mathjax-container">\(b\)</span> 是尺度参数（Scale Parameter）。拉普拉斯分布的一维概率密度函数为</p>
<span displaypfx="" class="mathjax-container">\[p_\theta(y_i|x_i)=\frac{1}{2b}\exp\left(-\frac{|y_i-f_\theta(x_i)|}{b}\right)\]</span>
<p>在 i.i.d. 假设下，整批数据的似然为</p>
<span displaypfx="" class="mathjax-container">\[p(D|\theta,b)=\prod_{i=1}^{N}\frac{1}{2b}\exp\left(-\frac{|y_i-f_\theta(x_i)|}{b}\right)\]</span>
<p>取负对数似然，得到</p>
<span displaypfx="" class="mathjax-container">\[-\log p(D|\theta,b)=N\log(2b)+\frac{1}{b}\sum_{i=1}^{N}|y_i-f_\theta(x_i)|\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(b\)</span> 视为常数时，第一项与 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 无关，因此最小化 NLL 关于 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 等价于最小化绝对误差和（Sum of Absolute Errors, SAE）：</p>
<span displaypfx="" class="mathjax-container">\[\hat\theta_{\mathrm{MLE}}=\arg\min_\theta\sum_{i=1}^{N}|y_i-f_\theta(x_i)|\]</span>
<p>这正是平均绝对误差（Mean Absolute Error, MAE）背后的概率来源。与高斯分布相比，拉普拉斯分布在中心更尖、尾部更厚，因此它对应的损失不再像平方误差那样强烈放大大残差，而是按线性方式惩罚偏差。也正因为如此，MAE 通常比 MSE 对离群点（Outlier）更稳健。</p>
<p>因此，高斯分布与最小二乘、拉普拉斯分布与绝对误差、Bernoulli / Categorical 分布与交叉熵，其实是同一个统计逻辑在不同任务类型下的三种展开：<span style="background-color: #c0c0c0;">先假设观测数据服从某个条件分布，再对训练集做最大似然估计；把负对数似然写开后，就得到具体的训练损失</span>。</p>
<div class="blog_h2"><span class="graybg">最大后验估计（MAP）</span></div>
<p>最大后验估计（Maximum A Posteriori, MAP）在 MLE 的“数据解释能力”之外，再加入参数的先验（Prior）信息。它选择后验概率（Posterior）最大的参数：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\theta}_{\mathrm{MAP}}=\arg\max_{\theta}p(\theta|D)\]</span>
<p>根据贝叶斯定理：</p>
<span displaypfx="" class="mathjax-container">\[p(\theta|D)=\frac{p(D|\theta)p(\theta)}{p(D)}\]</span>
<p>其中 <span displaypfx="inline-" class="mathjax-container">\(p(D|\theta)\)</span> 是似然（Likelihood）， <span displaypfx="inline-" class="mathjax-container">\(p(\theta)\)</span> 是先验， <span displaypfx="inline-" class="mathjax-container">\(p(D)\)</span> 是证据（Evidence）。因为 <span displaypfx="inline-" class="mathjax-container">\(p(D)\)</span> 与参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 无关，所以在求最大值时它只是常数，可以直接忽略：</p>
<span displaypfx="" class="mathjax-container">\[\hat{\theta}_{\mathrm{MAP}}=\arg\max_{\theta}p(D|\theta)p(\theta)=\arg\max_{\theta}\big(\log p(D|\theta)+\log p(\theta)\big)\]</span>
<p>这个式子清楚地说明了 MAP 的结构： <span displaypfx="inline-" class="mathjax-container">\(\log p(D|\theta)\)</span> 负责拟合数据， <span displaypfx="inline-" class="mathjax-container">\(\log p(\theta)\)</span> 负责约束参数不要偏离先验认知。若改写成最小化形式，则有</p>
<span displaypfx="" class="mathjax-container">\[\hat{\theta}_{\mathrm{MAP}}=\arg\min_\theta\big(-\log p(D|\theta)-\log p(\theta)\big)\]</span>
<p>因此，MAP 可以理解为“负对数似然 + 先验诱导的惩罚项（Penalty）”。这也是为什么很多正则化（Regularization）能够从贝叶斯视角解释：L2 正则通常对应高斯先验，L1 正则通常对应拉普拉斯先验。若先验在可行域内与 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 无关（例如均匀先验： <span displaypfx="inline-" class="mathjax-container">\(p(\theta)=c\)</span>），则 <span displaypfx="inline-" class="mathjax-container">\(\log p(\theta)=\log c\)</span> 是关于 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的常数；在 <span displaypfx="inline-" class="mathjax-container">\(\arg\max_\theta\)</span> 中加上或去掉这一项都不改变最优点，因此 MAP 与 MLE 给出同一组参数（若均匀先验还隐含“只在某个范围内为常数、范围外为 0”，则等价于在该范围约束下做 MLE）。</p>
<p>继续用抛硬币解释。现在参数已经统一记作 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>，因此先验也写成 <span displaypfx="inline-" class="mathjax-container">\(p(\theta)\)</span>，而不再写 <span displaypfx="inline-" class="mathjax-container">\(p(p)\)</span>。若对 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 设 Beta 分布（Beta Distribution）先验</p>
<p>Beta 分布是定义在区间 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span> 上的连续分布，最常用来描述“某个概率值本身的不确定性”，因此非常适合作为伯努利参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的先验。</p>
<span displaypfx="" class="mathjax-container">\[p(\theta)\propto \theta^{\alpha-1}(1-\theta)^{\beta-1},\quad 0\le \theta\le 1\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\propto\)</span> 表示“正比于”，意思是左边真正的概率密度函数还差一个归一化常数（Normalization Constant），这个常数负责保证密度在区间 <span displaypfx="inline-" class="mathjax-container">\([0,1]\)</span> 上积分为 1。对 MAP 而言，这个常数不影响最大值位置，所以通常省略不写。</p>
<p>参数 <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 和 <span displaypfx="inline-" class="mathjax-container">\(\beta\)</span> 控制分布形状： <span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span> 越大，分布越偏向较大的 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>； <span displaypfx="inline-" class="mathjax-container">\(\beta\)</span> 越大，分布越偏向较小的 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span>。例如 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Beta}(2,2)\)</span> 会把概率质量更多放在中间区域，表达“更倾向认为硬币接近公平”； <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Beta}(8,2)\)</span> 则更偏向 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 接近 1。</p>
<p>那么它表达的是：在看到数据之前，我们已经对“硬币正面概率应该落在什么范围”有一个先验偏好。</p>
<p>把这个先验与 7 正 3 反的似然 <span displaypfx="inline-" class="mathjax-container">\(p(D|\theta)=\theta^7(1-\theta)^3\)</span> 相乘，得到后验：</p>
<span displaypfx="" class="mathjax-container">\[p(\theta|D)\propto \theta^{7+\alpha-1}(1-\theta)^{3+\beta-1}\]</span>
<p>这说明后验仍然是 Beta 分布：</p>
<span displaypfx="" class="mathjax-container">\[p(\theta|D)=\mathrm{Beta}(7+\alpha,3+\beta)\]</span>
<p>众数（Mode）指的是<span style="background-color: #c0c0c0;">概率密度函数最高的那个位置</span>，也就是“这个分布最偏好的参数值”。因为 MAP 的定义本来就是寻找后验概率/后验密度最大的参数，所以当后验分布已经写出来时，MAP 估计就是这个后验分布的众数。</p>
<p>因为后验与 <span displaypfx="inline-" class="mathjax-container">\(\theta^{7+\alpha-1}(1-\theta)^{3+\beta-1}\)</span> 成正比，所以只需最大化这个函数。取对数后，等价目标变成</p>
<span displaypfx="" class="mathjax-container">\[(7+\alpha-1)\log \theta+(3+\beta-1)\log(1-\theta)\]</span>
<p>对 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 求导并令导数为 0：</p>
<span displaypfx="" class="mathjax-container">\[\frac{7+\alpha-1}{\theta}-\frac{3+\beta-1}{1-\theta}=0\]</span>
<p>移项并整理可得</p>
<span displaypfx="" class="mathjax-container">\[(7+\alpha-1)(1-\theta)=(3+\beta-1)\theta\Rightarrow \hat{\theta}_{\mathrm{MAP}}=\frac{7+\alpha-1}{10+\alpha+\beta-2}\]</span>
<p>因此，当 <span displaypfx="inline-" class="mathjax-container">\(\alpha,\beta>1\)</span> 时，Beta 分布的众数（Mode）也就是 MAP 估计。</p>
<p>若取 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Beta}(2,2)\)</span> 先验，则</p>
<span displaypfx="" class="mathjax-container">\[\hat{\theta}_{\mathrm{MAP}}=\frac{7+2-1}{10+2+2-2}=\frac{8}{12}=\frac{2}{3}\]</span>
<p>它比 MLE 的 <span displaypfx="inline-" class="mathjax-container">\(0.7\)</span> 更靠近 <span displaypfx="inline-" class="mathjax-container">\(0.5\)</span>。原因很具体： <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Beta}(2,2)\)</span> 可以理解为在真实观测之外，先额外放入了“1 次正面 + 1 次反面”的温和偏好，所以最终估计不会被7正3反的样本完全带着走。样本很少时，先验的影响明显；样本很多时，数据项占主导，MAP 会逐渐逼近 MLE。</p>
<p>在机器学习里，MAP 与正则化（Regularization）本质上是同一件事的两种表述。不是“随意加一个惩罚项”，而是：<span style="background-color: #c0c0c0;">一旦你给参数指定先验，取负对数后，这个先验就会自动变成优化目标里的正则项</span>。</p>
<p>先看一般形式。MAP 最大化的是后验概率 <span displaypfx="inline-" class="mathjax-container">\(p(\theta|D)\)</span>，等价于最小化负对数后验：</p>
<span displaypfx="" class="mathjax-container">\[-\log p(\theta|D)=-\log p(D|\theta)-\log p(\theta)+\mathrm{const}\]</span>
<p>第一项 <span displaypfx="inline-" class="mathjax-container">\(-\log p(D|\theta)\)</span> 是数据拟合项，第二项 <span displaypfx="inline-" class="mathjax-container">\(-\log p(\theta)\)</span> 就是先验带来的惩罚项。因此“正则化”并不是凭空发明出来的工程技巧，而是贝叶斯先验在优化问题里的直接体现。</p>
<p>若参数向量 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 服从零均值各向同性高斯先验（Isotropic Gaussian Prior），可以写成</p>
<span displaypfx="" class="mathjax-container">\[p(\theta)\propto \exp\left(-\frac{1}{2\tau^2}\|\theta\|_2^2\right)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\tau^2\)</span> 控制先验方差：方差越小，越强地偏好参数靠近 0。对它取负对数得到</p>
<span displaypfx="" class="mathjax-container">\[-\log p(\theta)=\frac{1}{2\tau^2}\|\theta\|_2^2+\mathrm{const}\]</span>
<p>把常数 <span displaypfx="inline-" class="mathjax-container">\(\frac{1}{2\tau^2}\)</span> 记成 <span displaypfx="inline-" class="mathjax-container">\(\lambda\)</span>，就得到熟悉的目标：</p>
<span displaypfx="" class="mathjax-container">\[-\log p(D|\theta)+\lambda\|\theta\|_2^2\]</span>
<p>这正是 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则。它的作用可以理解为：先验认为“大权重不太可信”，所以优化时不仅要拟合数据，还要为过大的参数付出代价。由于高斯先验在 0 附近平滑、尾部衰减快，它通常会把参数整体压小，但不特别鼓励大量参数精确等于 0。</p>
<p>若先验改成拉普拉斯（Laplacian）分布：</p>
<span displaypfx="" class="mathjax-container">\[p(\theta)\propto \exp\big(-\lambda\|\theta\|_1\big)\]</span>
<p>则负对数先验变成</p>
<span displaypfx="" class="mathjax-container">\[-\log p(\theta)=\lambda\|\theta\|_1+\mathrm{const}\]</span>
<p>于是 MAP 等价于最小化</p>
<span displaypfx="" class="mathjax-container">\[-\log p(D|\theta)+\lambda\|\theta\|_1\]</span>
<p>这就是 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 正则。它与 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 的关键区别在于：拉普拉斯先验在 0 处尖得更厉害，因此更强地偏好很多参数直接变成 0，也就是产生稀疏性（Sparsity）。</p>
<p>所以可以把对应关系记成一条很清楚的链：<span style="background-color: #c0c0c0;">高斯先验对应 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span>，拉普拉斯先验对应 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span>；正则项的形状，本质上就是先验密度形状的负对数</span>。很多看起来像“人为添加的惩罚项”，其实都可以解释为“在做 MAP”。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/regularization-distribution-assumptions.png"><img class="alignnone size-full" src="https://blog.gmem.cc/wp-content/uploads/2026/03/regularization-distribution-assumptions.png" alt="regularization-distribution-assumptions" width="1920" height="1064" /></a></p>
<p>这里的分布假设有两个不同落点。若假设的是数据误差或残差 <span displaypfx="inline-" class="mathjax-container">\(\epsilon\)</span> 的分布，它进入的是似然 <span displaypfx="inline-" class="mathjax-container">\(p(D|\theta)\)</span>，决定数据拟合项的形状；若假设的是参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 自身的分布，它进入的是先验 <span displaypfx="inline-" class="mathjax-container">\(p(\theta)\)</span>，决定正则项的形状。前者回答“数据会怎样围绕模型波动”，后者回答“参数本身更可能落在什么区域”。</p>
<p>因此，高斯和拉普拉斯并不只用于描述数据集。高斯残差假设会把负对数似然化成平方误差，拉普拉斯残差假设会把负对数似然化成绝对误差；与此同时，零均值高斯参数先验会把负对数先验化成 <span displaypfx="inline-" class="mathjax-container">\(L_2\)</span> 正则，零均值拉普拉斯参数先验会把负对数先验化成 <span displaypfx="inline-" class="mathjax-container">\(L_1\)</span> 正则。它们使用的是同一类分布族，但一个作用在数据项 <span displaypfx="inline-" class="mathjax-container">\(p(D|\theta)\)</span>，一个作用在参数项 <span displaypfx="inline-" class="mathjax-container">\(p(\theta)\)</span>。</p>
<div class="blog_h2"><span class="graybg">大数定律与中心极限定理</span></div>
<div class="blog_h3"><span class="graybg">大数定律（Law of Large Numbers, LLN）</span></div>
<p>大数定律说明：当独立同分布样本越来越多时，样本平均会稳定地靠近真实期望（Expectation）。若 <span displaypfx="inline-" class="mathjax-container">\(X_1,\dots,X_n\)</span> 独立同分布，且 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]\)</span> 存在，则</p>
<span displaypfx="" class="mathjax-container">\[\frac{1}{n}\sum_{i=1}^{n}X_i \to \mathbb{E}[X],\quad n\to\infty\]</span>
<p>这里的重点是“平均值会稳定下来”，而不是“每一次新观测都会让结果更接近真值”。它描述的是长期趋势：样本量越大，随机波动在平均过程中被不断抵消，样本平均就越不容易偏离真实期望太远。</p>
<p>最经典的例子是抛公平硬币。设正面记为 1、反面记为 0，则单次试验的期望是 <span displaypfx="inline-" class="mathjax-container">\(\mathbb{E}[X]=0.5\)</span>。前几次抛掷时，正面比例可能是 1、0、0.67、0.25，波动很大；但抛掷次数达到几百、几千次后，样本平均 <span displaypfx="inline-" class="mathjax-container">\(\bar{X}\)</span> 会越来越稳定地靠近 0.5。大数定律回答的是“为什么频率会稳定到概率附近”。</p>
<div class="blog_h3"><span class="graybg">中心极限定理（Central Limit Theorem, CLT）</span></div>
<p>中心极限定理（Central Limit Theorem, CLT）描述的是：在独立同分布且方差有限的条件下，样本均值经过适当标准化后，会趋近于正态分布（Normal Distribution）。它给出的不是“平均值最终落到哪里”的结论，而是“平均值围绕真值波动时，波动形状如何分布”的规律。</p>
<p>因此，中心极限定理讨论的重点不是“均值会不会收敛”，而是<span style="background-color: #c0c0c0;">样本均值在真值附近如何波动，以及这种波动的分布长什么样</span>。标准写法是：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\sqrt{n}(\bar{X}-\mu)}{\sigma}\Rightarrow \mathcal{N}(0,1)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(\mu=\mathbb{E}[X]\)</span> 是总体均值， <span displaypfx="inline-" class="mathjax-container">\(\sigma^2=\mathrm{Var}(X)\)</span> 是总体方差。所谓<span style="background-color: #c0c0c0;">标准化（Standardization）</span>，就是先减去均值 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span>，把中心移到 0；再除以标准差 <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span>，把尺度统一成“多少个标准差”；再乘上 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{n}\)</span>，把样本均值随着样本量增大而缩小的波动重新放回可比较的尺度。标准化之后，不同样本量下的波动可以放到同一个参考系里比较。</p>
<p>所谓<span style="background-color: #c0c0c0;">方差有限</span>，就是随机变量的波动强度不是无限大，满足 <span displaypfx="inline-" class="mathjax-container">\(\mathrm{Var}(X)<\infty[/latex]。直观上，这意味着样本虽然会波动，但不会被极端值以“无限强”的方式主导。像伯努利分布、二项分布、泊松分布、均匀分布和高斯分布都满足这个条件；而某些重尾分布则可能不满足，因此不能直接套用最基础的 CLT 表述。</p>
<p>为什么要乘上 [latex syntax=inline]\sqrt{n}\)</span>？因为样本均值的波动规模大约是 <span displaypfx="inline-" class="mathjax-container">\(\sigma/\sqrt{n}\)</span>：样本越多，均值越稳定。如果不乘 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{n}\)</span>，当 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 增大时， <span displaypfx="inline-" class="mathjax-container">\(\bar{X}-\mu\)</span> 会越来越接近 0，看不出其分布形状；乘上 <span displaypfx="inline-" class="mathjax-container">\(\sqrt{n}\)</span> 后，波动被拉回到常数量级，才会显现出稳定的正态极限。</p>
<p>继续看硬币例子。设 <span displaypfx="inline-" class="mathjax-container">\(X_i\in\{0,1\}\)</span> 且正面概率为 0.5，则 <span displaypfx="inline-" class="mathjax-container">\(\mu=0.5\)</span>， <span displaypfx="inline-" class="mathjax-container">\(\sigma^2=0.25\)</span>，标准差 <span displaypfx="inline-" class="mathjax-container">\(\sigma=0.5\)</span>。CLT 说的是：</p>
<span displaypfx="" class="mathjax-container">\[\frac{\sqrt{n}(\bar{X}-0.5)}{0.5}\Rightarrow \mathcal{N}(0,1)\]</span>
<p>例如当 <span displaypfx="inline-" class="mathjax-container">\(n=100\)</span> 时，正面比例 <span displaypfx="inline-" class="mathjax-container">\(\bar{X}\)</span> 的标准差大约是 <span displaypfx="inline-" class="mathjax-container">\(0.5/\sqrt{100}=0.05\)</span>。这意味着正面比例通常会落在 <span displaypfx="inline-" class="mathjax-container">\(0.5\pm 0.1\)</span> 这一量级附近；更精确地，用正态近似可写成</p>
<span displaypfx="" class="mathjax-container">\[\bar{X}\approx \mathcal{N}\left(0.5,\frac{0.25}{100}\right)=\mathcal{N}(0.5,0.0025)\]</span>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-clt.png"><img class="alignnone size-full wp-image-40929" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot-clt.png" alt="plot-clt" width="100%" /></a></p>
<p>上图是样本量分别为1, 5, 20, 100的Bernoulli(0.5)分布，分别进行了30000轮测试，绘制的（每轮）标准化后的分布，可以看到样本量足够多时，其均值倾向于向中心（未标准化前的0.5对应此伯努利分布的总体均值）靠近。</p>
<p>所以 CLT 回答的是“为什么很多统计量在样本足够大时会近似高斯”，而 LLN 回答的是“为什么样本平均会逼近真值”。前者给出分布近似，后者给出收敛结论。置信区间、显著性检验、A/B test 和 mini-batch 梯度噪声分析，主要依赖的是 CLT 提供的近似正态性。</p>
<div class="blog_h3"><span class="graybg">置信度与置信区间</span></div>
<p>置信度（Confidence Level）与置信区间（Confidence Interval）属于统计推断（Statistical Inference）中的区间估计（Interval Estimation）。它们处理的不是“样本本身长什么样”，而是<span style="background-color: #c0c0c0;">在只看到一批有限样本时，怎样给未知总体参数画出一个有覆盖保证的范围</span>。在机器学习实验、A/B test、离线评测和指标报表里，样本均值、准确率、点击率、转化率和误差率后面常跟着一个区间，这个区间就是区间估计的产物。</p>
<p>若目标是估计总体均值 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span>，样本均值为 <span displaypfx="inline-" class="mathjax-container">\(\bar{X}\)</span>，且样本量足够大或总体近似正态，那么基于中心极限定理，可以构造近似的双侧置信区间：</p>
<span displaypfx="" class="mathjax-container">\[\bar{X}\pm z_{1-\alpha/2}\frac{\sigma}{\sqrt{n}}\]</span>
<p>若总体标准差 <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 未知，而样本来自正态总体或样本量适中且使用样本标准差 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 近似，则常写作：</p>
<span displaypfx="" class="mathjax-container">\[\bar{X}\pm t_{1-\alpha/2,\,n-1}\frac{s}{\sqrt{n}}\]</span>
<p>这里每个元素的含义分别是：</p>
<ul>
<li><span displaypfx="inline-" class="mathjax-container">\(\bar{X}\)</span>：样本均值（Sample Mean），是当前样本对总体均值的点估计（Point Estimate）。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(n\)</span>：样本量（Sample Size）。样本越多，区间通常越窄。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span>：总体标准差（Population Standard Deviation）；若未知，常用样本标准差 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 替代。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\frac{\sigma}{\sqrt{n}}\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(\frac{s}{\sqrt{n}}\)</span>：标准误（Standard Error），表示样本均值本身的波动尺度，而不是单个样本点的波动。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(\alpha\)</span>：显著性水平（Significance Level）。若置信度为 95%，则 <span displaypfx="inline-" class="mathjax-container">\(\alpha=0.05\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(z_{1-\alpha/2}\)</span>：标准正态分布的分位数（Quantile）；95% 置信度时大约是 <span displaypfx="inline-" class="mathjax-container">\(1.96\)</span>。</li>
<li><span displaypfx="inline-" class="mathjax-container">\(t_{1-\alpha/2,\,n-1}\)</span>：Student t 分布的分位数，带有 <span displaypfx="inline-" class="mathjax-container">\(n-1\)</span> 个自由度（Degrees of Freedom），用于总体方差未知时。</li>
</ul>
<p>在这个表达式里，<span style="background-color: #c0c0c0;">置信区间</span> 指的是最终得到的区间本身，例如 <span displaypfx="inline-" class="mathjax-container">\([1.8,2.4]\)</span>；<span style="background-color: #c0c0c0;">置信度</span> 指的是构造这个区间的方法在重复抽样下的长期覆盖率，例如 95%、99%。因此，95% 置信区间的严格含义是：如果用同一种抽样方式和同一种区间构造公式反复重复实验，那么大约 95% 的区间会覆盖真实参数。</p>
<p>这一定义有一个极其重要的解释边界。总体参数 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span> 在经典频率学派（Frequentist）统计里被视为固定但未知的常数；随机的是样本，因此随机的是区间端点。样本一旦观察完成，这次得到的区间要么覆盖 <span displaypfx="inline-" class="mathjax-container">\(\mu\)</span>，要么不覆盖，概率意义已经不再施加在参数本身上。于是，95% 置信度描述的是<span style="background-color: #c0c0c0;">区间构造程序的长期可靠性</span>，而不是“参数有 95% 概率落在这次区间里”的后验概率表述。</p>
<p>置信度并不局限于连续分布。它的定义依赖的是“重复抽样下的覆盖率”，而不是总体分布是否连续。对伯努利分布（Bernoulli Distribution）、二项分布（Binomial Distribution）、泊松分布（Poisson Distribution）这类离散分布，同样可以对参数构造 95% 或 99% 的区间估计。例如对二项分布中的成功概率 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>，就可以根据观测到的成功次数构造 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 的置信区间；若参数或目标对象不是一维连续量，更宽泛的名称则是置信集合（Confidence Set）。</p>
<p>离散分布与连续分布的差别不在“能不能谈置信度”，而在于区间构造的细节。连续情形下，区间端点可以较平滑地调节到目标覆盖率附近；离散情形下，统计量只能取离散值，覆盖率往往无法恰好等于名义值，例如正好 95%，而常常只能做到“不低于 95%”。因此，离散分布里的精确区间常带有一定保守性（Conservativeness）：区间更宽一些，但覆盖保证更稳。这也是为什么在离散统计推断里，除了近似正态区间外，还常见 Clopper–Pearson 这类精确区间构造方法。</p>
<p>一个具体例子最容易说明这两个概念。设某模型在 100 次独立测试中的平均准确率估计为 <span displaypfx="inline-" class="mathjax-container">\(\bar{X}=0.82\)</span>，样本标准差为 <span displaypfx="inline-" class="mathjax-container">\(s=0.10\)</span>。若采用近似 95% 置信区间，可写成：</p>
<span displaypfx="" class="mathjax-container">\[0.82\pm 1.96\times \frac{0.10}{\sqrt{100}}=0.82\pm 0.0196\]</span>
<p>于是区间约为：</p>
<span displaypfx="" class="mathjax-container">\[[0.8004,\ 0.8396]\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\([0.8004,0.8396]\)</span> 是置信区间，95% 是置信度。它表示：按这种抽样和构造方式长期重复下去，约 95% 的此类区间会覆盖真实平均准确率；这次具体得到的这个区间只是其中一个实现结果。</p>
<p>区间宽度主要由三个因素共同决定：</p>
<ul>
<li>样本波动越大，即 <span displaypfx="inline-" class="mathjax-container">\(\sigma\)</span> 或 <span displaypfx="inline-" class="mathjax-container">\(s\)</span> 越大，区间越宽。</li>
<li>样本量越大，即 <span displaypfx="inline-" class="mathjax-container">\(n\)</span> 越大，标准误越小，区间越窄。</li>
<li>置信度越高，例如从 95% 提高到 99%，对应分位数更大，区间也会变宽。</li>
</ul>
<p>因此，置信区间是在样本有限时对参数估计可靠性的定量表达；置信度则是这套区间构造方法愿意给出的覆盖承诺。两者必须一起理解：只有区间没有置信度，范围没有统计保证；只有置信度没有区间，覆盖承诺也无法落到具体参数估计上。</p>
<div class="blog_h2"><span class="graybg">随机过程</span></div>
<p>前面讨论的随机变量（Random Variable）通常只对应一次不确定试验，例如抛一次硬币、测一次身高、抽取一个样本标签。随机过程（Stochastic Process）则讨论<span style="background-color: #c0c0c0;">一串彼此有关、按时间或空间索引起来的随机变量</span>。它适合描述会随时间演化的不确定系统，例如天气变化、股价波动、队列长度、用户行为序列、传感器读数，以及自然语言中的 token 序列。</p>
<p>形式上，随机过程可记为：</p>
<span displaypfx="" class="mathjax-container">\[\{X_t\}_{t\in T}\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(T\)</span> 是索引集合（Index Set），常表示时间； <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span> 是时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 的随机变量。若 <span displaypfx="inline-" class="mathjax-container">\(T=\{0,1,2,\dots\}\)</span>，过程是离散时间（Discrete-time）的；若 <span displaypfx="inline-" class="mathjax-container">\(T=[0,\infty)\)</span>，过程则是连续时间（Continuous-time）的。每个 <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span> 都有自己的分布，但更重要的是这些随机变量之间的联合分布（Joint Distribution）与依赖结构，因为随机过程关心的不是“某一时刻单独会怎样”，而是“整个序列怎样一起变化”。</p>
<p>从结果形态看，一次随机过程的实现不再是一个点，而是一条轨迹（Trajectory）或样本路径（Sample Path）。例如，设 <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span> 表示第 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 天的气温，那么一次观测得到的是一整段温度序列 <span displaypfx="inline-" class="mathjax-container">\((X_1,X_2,\dots,X_T)\)</span>；若 <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span> 表示语言模型在位置 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 生成的 token，那么一次实现就是一整句文本。</p>
<p>随机过程之所以在机器学习里重要，是因为很多任务天生就是时序问题。时间序列预测关心未来值如何演化，隐马尔可夫模型（Hidden Markov Model, HMM）关心隐藏状态如何随时间转移，强化学习关心状态—动作—奖励序列怎样展开，自回归语言模型则是在建模 token 序列的联合概率。因此，随机过程可以看作“把单个随机变量扩展到整条时间轴上的概率建模语言”。</p>
<div class="blog_h3"><span class="graybg">马尔可夫性与马尔可夫过程</span></div>
<p>马尔可夫性（Markov Property）讨论的是随机过程中的“记忆如何被压缩”。它刻画这样一种情形：<span style="background-color: #c0c0c0;">如果当前状态已经把与未来演化有关的信息概括完整，那么预测下一步时就不再需要显式回看更久的历史</span>。在这种表示下，过去对未来的影响已经通过当前状态被浓缩进来了。</p>
<p>设随机过程为 <span displaypfx="inline-" class="mathjax-container">\(\{X_t\}_{t=0}^{\infty}\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span> 表示时刻 <span displaypfx="inline-" class="mathjax-container">\(t\)</span> 的随机状态。若它满足一阶马尔可夫性，则有：</p>
<span displaypfx="" class="mathjax-container">\[P(X_{t+1}\mid X_t,X_{t-1},\dots,X_0)=P(X_{t+1}\mid X_t)\]</span>
<p>这条式子的含义是：在已经知道当前状态 <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span> 的前提下，再额外知道更早的历史 <span displaypfx="inline-" class="mathjax-container">\(X_{t-1},\dots,X_0\)</span>，不会改变对下一时刻 <span displaypfx="inline-" class="mathjax-container">\(X_{t+1}\)</span> 的条件分布判断。左边是“条件在完整历史上的下一步分布”，右边是“条件在当前状态上的下一步分布”；两者相等，正是马尔可夫性的定义。</p>
<p>一个直接例子是天气变化。若把每天的天气记作随机变量 <span displaypfx="inline-" class="mathjax-container">\(X_t\)</span>，状态空间为 <span displaypfx="inline-" class="mathjax-container">\(\{\text{晴},\text{阴},\text{雨}\}\)</span>，那么常见建模假设是：明天是否下雨主要由今天的天气决定，而不需要把更早几天的天气逐项保留。此时“当前天气”就扮演了压缩历史信息的状态摘要。若今天是雨天，明天继续下雨的概率可以较大；若今天是晴天，明天下雨的概率可以较小。这种“只通过当前状态决定下一步分布”的结构，就是马尔可夫性。</p>
<p>满足马尔可夫性的随机过程，称为马尔可夫过程（Markov Process）或马尔可夫链（Markov Chain，离散时间、有限或可数状态时的常见名称）。因此，马尔可夫性是一个性质，马尔可夫过程是满足该性质的一类随机过程。若状态空间有限，常把一步转移概率写成转移矩阵（Transition Matrix） <span displaypfx="inline-" class="mathjax-container">\(P\)</span>，其中：</p>
<span displaypfx="" class="mathjax-container">\[P_{ij}=P(X_{t+1}=j\mid X_t=i)\]</span>
<p>这里 <span displaypfx="inline-" class="mathjax-container">\(P_{ij}\)</span> 表示“当前在状态 <span displaypfx="inline-" class="mathjax-container">\(i\)</span> 时，下一步转移到状态 <span displaypfx="inline-" class="mathjax-container">\(j\)</span> 的概率”。矩阵的每一行对应当前状态固定后的下一步分布，因此每一行元素都非负，且行和为 1。</p>
<p>马尔可夫过程之所以重要，是因为它把一个潜在上非常复杂的时序依赖问题，压缩成了局部转移规律。隐马尔可夫模型（Hidden Markov Model, HMM）、马尔可夫决策过程（Markov Decision Process, MDP）、很多时间序列状态模型，以及强化学习中的环境状态转移，都建立在这一思想之上。它们的差别不在于是否使用马尔可夫性，而在于：状态是否可见、是否存在动作、是否附带奖励，以及是否只关心下一步还是关心长期回报。</p>
<p>马尔可夫性是一种建模假设，不是自然界自动保证的真理。若“当前状态”定义得不充分，历史信息就没有被真正压缩进去，此时过程在这个状态表示下就不具有马尔可夫性。例如，仅用“当前股价”预测明天走势通常不够，因为成交量、市场情绪、宏观事件等信息并未包含在状态中；但若把更完整的市场状态向量一并纳入，马尔可夫近似会更合理。因此，马尔可夫性的关键不只是公式，而是<span style="background-color: #c0c0c0;">当前状态是否足以成为过去对未来影响的充分摘要</span>。</p>
<div class="blog_h2"><span class="graybg">信息论</span></div>
<div class="blog_h3"><span class="graybg">熵（Entropy）</span></div>
<p>熵（Entropy）刻画一个概率分布的不确定性（Uncertainty）。对离散分布 <span displaypfx="inline-" class="mathjax-container">\(p\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[H(p)=-\sum_i p_i\log p_i\]</span>
<p>把 <span displaypfx="inline-" class="mathjax-container">\(-\log p(x)\)</span> 理解为“事件 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的信息量（Information Content）/惊奇”，熵就是它的期望值：越均匀的分布越难预测，熵越大；越尖锐（某个事件概率接近 1）的分布越确定，熵越小。</p>
<p>例：若 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 在 <span displaypfx="inline-" class="mathjax-container">\(K\)</span> 个类别上均匀（<span displaypfx="inline-" class="mathjax-container">\(p_i=1/K\)</span>），则</p>
<span displaypfx="" class="mathjax-container">\[H(p)=\log K\]</span>
<p>对数底决定单位：以 <span displaypfx="inline-" class="mathjax-container">\(\log_2\)</span> 为底单位是比特（Bits），以自然对数为底单位是纳特（Nats）。</p>
<p>语言也能帮助建立对熵的直觉。以字符为单位时，汉字集合更大，单个常用字符的平均出现概率往往更低，因此 <span displaypfx="inline-" class="mathjax-container">\(-\log p(x)\)</span> 更大；同时，汉语序列里的下一字符通常也更难直接预测，所以常给人“字符级信息更密、熵更高”的直觉。对比之下，韩语的字符系统更小，序列模式也通常更规则，下一字符更容易预测，因此字符级冗余感更强、熵的直觉更低。</p>
<p>若再做一个极粗略的字符级估算：把常用汉字集合近似看作 <span displaypfx="inline-" class="mathjax-container">\(V=3000\)</span>、把韩文字母集合近似看作 <span displaypfx="inline-" class="mathjax-container">\(V=40\)</span>，并暂时假设“下一字符”在各自词表上均匀分布，则最大熵分别约为 <span displaypfx="inline-" class="mathjax-container">\(\log_2 3000\approx 11.55\)</span> bits 与 <span displaypfx="inline-" class="mathjax-container">\(\log_2 40\approx 5.32\)</span> bits。这对应的就是：词表越大、单符号平均概率越低，单位符号的潜在信息量上界越高。</p>
<div class="blog_h3"><span class="graybg">KL 散度</span></div>
<p>KL 散度（Kullback–Leibler Divergence）衡量两个分布之间的“相对熵”（Relative Entropy）。对离散分布 <span displaypfx="inline-" class="mathjax-container">\(p,q\)</span>：</p>
<span displaypfx="" class="mathjax-container">\[D_{\mathrm{KL}}(p\|q)=\sum_i p_i\log\frac{p_i}{q_i}\]</span>
<p>它满足 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(p\|q)\ge 0\)</span>，且当且仅当 <span displaypfx="inline-" class="mathjax-container">\(p=q\)</span> 时取 0；但它一般不对称（Asymmetric），即 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(p\|q)\ne D_{\mathrm{KL}}(q\|p)\)</span>。信息论解释是：用 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 的码本去编码来自 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 的样本，会额外多付出多少平均码长。</p>
<p>在机器学习里，KL 常作为正则项出现，例如把新策略/新模型约束在参考分布附近（KL Regularization）。</p>
<div class="blog_h3"><span class="graybg">交叉熵</span></div>
<p>交叉熵（Cross Entropy）是信息论里衡量“用分布 <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 去编码来自分布 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 的样本”所需平均信息量的量。这里 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 是真实分布（True Distribution）/数据分布， <span displaypfx="inline-" class="mathjax-container">\(q\)</span> 是模型分布（Model Distribution）/预测分布：</p>
<span displaypfx="" class="mathjax-container">\[H(p,q)=-\sum_{i} p_i\log q_i\]</span>
<p>当 <span displaypfx="inline-" class="mathjax-container">\(p\)</span> 固定时，最小化 <span displaypfx="inline-" class="mathjax-container">\(H(p,q)\)</span> 等价于最小化 KL 散度（Kullback–Leibler Divergence）：</p>
<span displaypfx="" class="mathjax-container">\[H(p,q)=H(p)+D_{\mathrm{KL}}(p\|q)\]</span>
<p>分类问题里，真实标签通常用 one-hot 分布表示，这会把交叉熵简化成对“真实类别概率”的负对数：交叉熵损失（Cross-Entropy Loss）本质上就是负对数似然（Negative Log-Likelihood）。</p>
<p>“信息量为什么和概率有关？”因为在最优编码（Optimal Coding）里，一个事件 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的最短平均码长与 <span displaypfx="inline-" class="mathjax-container">\(-\log p(x)\)</span> 同阶；以 <span displaypfx="inline-" class="mathjax-container">\(\log_2\)</span> 为底时单位是比特（Bits），以自然对数为底时单位是纳特（Nats）。熵（Entropy）就是期望信息量。</p>
<p>一个具体例子：设真实分布 <span displaypfx="inline-" class="mathjax-container">\(p=(0.5,0.5)\)</span>，模型分布 <span displaypfx="inline-" class="mathjax-container">\(q=(0.9,0.1)\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[H(p,q)=-0.5\log 0.9-0.5\log 0.1\approx 1.204\ (\text{nats})\]</span>
<p>而真实熵 <span displaypfx="inline-" class="mathjax-container">\(H(p)=-0.5\log 0.5-0.5\log 0.5\approx 0.693\)</span>，两者的差就是 <span displaypfx="inline-" class="mathjax-container">\(D_{\mathrm{KL}}(p\|q)\approx 0.511\)</span>：模型越“错得自信”，KL 越大：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2026/03/plot_entropy_cross.png"><img class="alignnone size-full wp-image-40943" src="https://blog.gmem.cc/wp-content/uploads/2026/03/plot_entropy_cross.png" alt="plot_entropy_cross" width="100%" /></a></p>
<div class="blog_h4"><span class="graybg">惊奇（Surprise）</span></div>
<p>惊奇（Surprise）/信息量是针对单个事件的度量：若事件 <span displaypfx="inline-" class="mathjax-container">\(x\)</span> 的概率为 <span displaypfx="inline-" class="mathjax-container">\(p(x)\)</span>，则</p>
<span displaypfx="" class="mathjax-container">\[\mathrm{Surprise}(x)=-\log p(x)\]</span>
<p>概率越小，惊奇越大；概率越大，惊奇越小。在语言建模里，若当前模型参数已经固定，那么真实 token 的 <span displaypfx="inline-" class="mathjax-container">\(-\log p_\theta(y_t|c_t)\)</span> 本质上是该 token 的负对数概率，也就是它的“惊奇”。这里 <span displaypfx="inline-" class="mathjax-container">\(c_t\)</span> 表示当前位置之前的上下文， <span displaypfx="inline-" class="mathjax-container">\(y_t\)</span> 表示当前位置真实出现的 token。若把同一个式子看成关于参数 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 的函数，它又是该 token 对参数的负对数似然（Negative Log-Likelihood, NLL）。因此这里的“惊奇”和 token-level NLL 在数值上是同一个对象，只是视角不同。如果再按各个事件的真实概率对这种“惊奇”做加权平均，得到的就是熵（Entropy）：<span displaypfx="inline-" class="mathjax-container">\(H(X)=\mathbb{E}[-\log p(X)]\)</span>。换句话说，<span style="background-color: #c0c0c0;">惊奇是单个事件的信息量，熵是这种信息量在整个分布下的期望</span>。</p>
<div class="blog_h4"><span class="graybg">困惑度（Perplexity）</span></div>
<p>困惑度（Perplexity）把“平均惊奇”指数化，得到一个更直观的尺度：可把它理解为模型在每一步预测时的“有效分支数（Effective Branching Factor）”。对长度为 <span displaypfx="inline-" class="mathjax-container">\(N\)</span> 的 token 序列，模型给出的条件概率是 <span displaypfx="inline-" class="mathjax-container">\(q(x_t|x_{<t})[/latex]，则平均 NLL（也就是交叉熵）为：</p>
[latex]\mathrm{NLL}=-\frac{1}{N}\sum_{t=1}^{N}\log q(x_t|x_{<t})[/latex]
<p>若使用自然对数，困惑度定义为：</p>
[latex]\mathrm{PPL}=\exp(\mathrm{NLL})\)</span>
<p>因此：分布越均匀（更不确定），熵越高，平均惊奇越大，困惑度也越高。注意困惑度强烈依赖 tokenization 与评测语料，跨不同分词器/词表直接比较往往没有意义。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-1">人工智能理论知识 - 数学基础</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ai-knowledge-quick-ref-1/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>人工智能理论知识 - 简介</title>
		<link>https://blog.gmem.cc/ai-knowledge-quick-ref-0</link>
		<comments>https://blog.gmem.cc/ai-knowledge-quick-ref-0#comments</comments>
		<pubDate>Wed, 15 Apr 2026 00:38:16 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=42203</guid>
		<description><![CDATA[<p>这一篇作为整套 AI 总纲的导论，先不进入公式和具体模型细节，而是回答更根本的问题：什么叫智能，人工智能究竟在试图做什么，机器为什么能从数据中学会某些能力，为什么这个方向在近十几年才真正爆发，以及机器学习、深度学习与大语言模型之间到底是什么关系。后续第 1 篇会进入数学基础，第 2 篇进入机器学习与神经网络，第 3 篇进入 Transformer 与大模型，第 4 篇进入 RAG、上下文工程与 Agent 系统。 什么是人工智能 人工智能的核心问题 人工智能（Artificial Intelligence, AI）讨论的不是“怎样让机器算得更快”，而是“怎样让机器表现出某种智能行为”。这里的智能行为，至少包括感知（Perception）、判断（Decision）、学习（Learning）、推理（Reasoning）与适应（Adaptation）。因此，AI 的目标从来不只是写一个会执行指令的程序，而是让系统能够在不完全由人工穷举规则的前提下，对复杂环境做出有效反应。 <a class="read-more" href="https://blog.gmem.cc/ai-knowledge-quick-ref-0">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-0">人工智能理论知识 - 简介</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>这一篇作为整套 AI 总纲的导论，先不进入公式和具体模型细节，而是回答更根本的问题：什么叫智能，人工智能究竟在试图做什么，机器为什么能从数据中学会某些能力，为什么这个方向在近十几年才真正爆发，以及机器学习、深度学习与大语言模型之间到底是什么关系。后续第 1 篇会进入数学基础，第 2 篇进入机器学习与神经网络，第 3 篇进入 Transformer 与大模型，第 4 篇进入 RAG、上下文工程与 Agent 系统。</p>
<div class="blog_h1"><span class="graybg">什么是人工智能</span></div>
<div class="blog_h2"><span class="graybg">人工智能的核心问题</span></div>
<p>人工智能（Artificial Intelligence, AI）讨论的不是“怎样让机器算得更快”，而是“怎样让机器表现出某种智能行为”。这里的智能行为，至少包括感知（Perception）、判断（Decision）、学习（Learning）、推理（Reasoning）与适应（Adaptation）。因此，AI 的目标从来不只是写一个会执行指令的程序，而是让系统能够在不完全由人工穷举规则的前提下，对复杂环境做出有效反应。</p>
<p>从工程角度看，AI 最常见的外显形式包括图像识别、语音识别、推荐系统、自动驾驶、机器翻译、问答系统、代码生成和多轮智能体。但这些表象背后要解决的是同一个更抽象的问题：<span style="background-color: #c0c0c0;">如何把真实世界中的复杂输入映射为可执行的判断与行动</span>。</p>
<div class="blog_h2"><span class="graybg">从自动化到智能</span></div>
<p>并不是所有“看起来自动”的系统都属于智能。普通自动化系统更多依赖预先写好的流程和明确规则，例如“若温度超过阈值则启动风扇”“若用户点击按钮则调用接口”。这类系统的行为边界，主要由人类工程师提前定义好。它们可以非常有用，但并不等于具备真正的学习能力。</p>
<p>智能系统的关键差别在于：它不只会执行既定步骤，还能从经验中修正自己的内部表示与决策策略。也就是说，它不仅回答“当前该做什么”，还会通过数据逐步形成“以后遇到类似情况时应该怎样判断”。这种能力一旦出现，系统行为就不再完全等价于人工编写的 if-else 规则树。</p>
<div class="blog_h2"><span class="graybg">AI、机器学习、深度学习与大模型的关系</span></div>
<p>这几个概念在日常讨论里经常被混用，但它们不是同一个层级。人工智能（AI）是最大的外层概念，讨论的是“让机器表现出智能行为”这一总目标。机器学习（Machine Learning, ML）是实现 AI 的一大类方法，它强调从数据中学习规律，而不是完全依赖手工规则。深度学习（Deep Learning, DL）又是机器学习中的一个重要分支，核心是通过多层神经网络自动学习表示。大语言模型（LLM）和现代基础模型（Foundation Model）则建立在深度学习之上，是特定时代的代表性形态，而不是与深度学习并列的全新学科。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">概念</td>
<td style="text-align: center;">它回答的问题</td>
<td style="text-align: center;">与其他概念的关系</td>
</tr>
</thead>
<tbody>
<tr>
<td>人工智能（AI）</td>
<td>怎样让机器表现出感知、判断、学习、推理和行动能力</td>
<td>最大外层目标</td>
</tr>
<tr>
<td>机器学习（ML）</td>
<td>怎样让机器从数据而不是纯规则中学习映射关系</td>
<td>AI 的主要实现路线之一</td>
</tr>
<tr>
<td>深度学习（DL）</td>
<td>怎样用多层神经网络自动学习层级表示</td>
<td>ML 的一个重要分支</td>
</tr>
<tr>
<td>大语言模型 / 基础模型</td>
<td>怎样通过大规模预训练得到通用生成与迁移能力</td>
<td>建立在深度学习之上的现代主线</td>
</tr>
</tbody>
</table>
<p>因此，后续学习不应把这些词当成互相替代的流行口号，而应始终记住它们的层级关系：<span style="background-color: #c0c0c0;">AI 是目标，ML 是方法族，DL 是方法族中的核心分支，LLM 是 DL 在特定时代和特定架构下的代表形态</span>。一旦这个层级关系理顺，后面的许多概念就不会显得混乱。</p>
<div class="blog_h1"><span class="graybg">什么叫智能</span></div>
<div class="blog_h2"><span class="graybg">学习能力与迁移能力</span></div>
<p>若要给“智能”一个足够实用、又不过分空泛的定义，一个很好的工作表述是：<span style="background-color: #c0c0c0;">智能 = 学习能力 + 迁移能力</span>。学习能力指系统能够从有限经验中提取规律；迁移能力指系统学到规律之后，能够在未见过但结构相近的新情境中继续做出合理判断。</p>
<p>这个定义的重要性在于，它把“记住训练样本”与“真正学会规律”区分开了。一个系统若只是把所有见过的情况硬背下来，那么它最多拥有记忆，不一定拥有智能。智能的难点不在于把过去储存起来，而在于从过去抽取出可泛化的结构。</p>
<div class="blog_h2"><span class="graybg">智能不是单一能力</span></div>
<p>把智能理解成单一分数，会掩盖许多关键差异。真实系统中的智能通常至少包含几类不同能力：感知（能否从复杂输入中抽取有用信息）、表征（能否形成稳定内部概念）、记忆（能否保留历史经验）、推理（能否在已知条件上做组合与演绎）、规划（能否为目标拆解步骤）、行动（能否把判断落成可执行决策），以及沟通（能否把内部状态转换成外部可用表达）。</p>
<p>一个系统可能在其中某些方面很强，在另一些方面很弱。例如大型分类模型在感知和表征上可能很强，但不一定擅长长期规划；语言模型在表达和知识调用上很强，但若缺少外部工具和环境反馈，就未必具备可靠行动能力。因此，讨论智能不能只问“它聪不聪明”，还要问“它在哪些维度上具备能力、在哪些维度上仍有缺口”。</p>
<div class="blog_h2"><span class="graybg">从经验到概念</span></div>
<p>人类识别“苹果”这类概念时，通常并不是先掌握一组严密定义，再去匹配世界；更常见的路径是通过大量经验，在脑中逐步形成一个模糊但稳定的概念边界。这个边界并不是几何参数、颜色阈值和纹理公式的明确列表，而是一种能够支持识别和迁移的内部表征。</p>
<p>这件事对 AI 尤其重要。它说明很多关键知识并不天然适合写成规则，而更适合通过样本驱动的表示学习形成。机器学习和深度学习之所以有效，正是因为它们允许系统用大量样本不断调整内部参数，最后形成“什么样的输入更像某个概念”的高维表示。</p>
<div class="blog_h2"><span class="graybg">泛化为什么重要</span></div>
<p>泛化（Generalization）是机器学习和人工智能中的中心概念。设训练数据来自分布 <span displaypfx="inline-" class="mathjax-container">\(\mathcal{D}_{\text{train}}\)</span>，模型在训练集上的经验风险（Empirical Risk）为</p>
<span displaypfx="" class="mathjax-container">\[\hat R(f)=\frac{1}{n}\sum_{i=1}^{n}\ell(f(x_i),y_i)\]</span>
<p>真正重要的并不是 <span displaypfx="inline-" class="mathjax-container">\(\hat R(f)\)</span> 本身，而是模型在未来未见样本上的期望风险（Expected Risk）</p>
<span displaypfx="" class="mathjax-container">\[R(f)=\mathbb{E}_{(x,y)\sim \mathcal{D}}\big[\ell(f(x),y)\big]\]</span>
<p>所谓泛化能力，本质上就是：训练中学到的规律，能否从有限样本扩展到更广泛但相关的真实世界分布。一个模型若只能在训练数据上表现良好，而离开训练分布就失效，它就更像记忆系统而不是智能系统。</p>
<p>这也是为什么 AI 讨论中总会反复出现过拟合（Overfitting）、分布偏移（Distribution Shift）、鲁棒性（Robustness）和迁移学习（Transfer Learning）这些概念。它们关心的都是同一件事：模型学到的东西，究竟是在逼近世界规律，还是只是在背训练题答案。</p>
<div class="blog_h2"><span class="graybg">专用智能与通用智能</span></div>
<p>按照任务适用范围，可以把 AI 粗略分成专用人工智能（Narrow AI）与通用人工智能（Artificial General Intelligence, AGI）。专用人工智能通常在某个任务上表现极强，例如围棋、图像分类、语音识别、广告排序或蛋白质结构预测；但它的能力边界高度依赖训练目标与任务环境，换一个问题往往就需要重新建模、重新训练甚至重写系统。</p>
<p>AGI 则要求系统具备更广泛的理解、推理、学习与迁移能力，能够跨任务、跨场景、跨知识域持续适应。这一目标远比单任务最优困难，因为它要求模型不只是对单个问题拟合得好，而是对世界结构形成更一般的内部表示。</p>
<p>因此，AlphaGo 可以击败顶级围棋选手，但这并不自动意味着它具备通用智能。它展现的是在一个定义良好、奖励明确、规则固定的任务中实现超人性能；这当然非常重要，但离“在不同领域都能自主迁移和解决问题”的 AGI 仍有明显距离。</p>
<div class="blog_h1"><span class="graybg">人工智能的早期路线</span></div>
<div class="blog_h2"><span class="graybg">符号主义与专家系统</span></div>
<p>人工智能早期最自然的思路是符号主义（Symbolicism）：既然人类能用语言、逻辑、规则和概念来描述世界，那么是否可以直接把这些规则写给机器，让机器照此推理。专家系统（Expert System）就是这种路线的代表。工程师通过知识库（Knowledge Base）、规则库（Rule Base）和推理机（Inference Engine），把领域专家的经验编码成显式规则，让机器在给定条件下自动给出结论。</p>
<p>这种方法在规则边界相对稳定、领域知识比较明确的任务上可以取得不错效果。例如早期的诊断辅助系统、规则客服、配置推荐系统，都曾从专家系统中受益。它的优势是可解释性强、行为边界清晰、局部领域内可控性高。</p>
<div class="blog_h2"><span class="graybg">为什么纯规则路线走不通</span></div>
<p>符号主义的根本局限，不在于规则无用，而在于现实世界太复杂。若想靠人工穷举规则来覆盖所有场景，很快会遇到三类问题。第一，规则组合爆炸：例外情况会越来越多，规则之间开始相互冲突。第二，感知输入难以被精确定义：光照变化、遮挡、噪声、语义歧义和上下文依赖，都会让“明确规则”变得脆弱。第三，规则迁移差：一个任务里定义好的知识，很难自然扩展到另一个任务。</p>
<p>更关键的是，世界中许多重要模式本来就不是人类能够轻易写成规则的。例如“什么样的像素组合像苹果”“什么样的句法和语义结构代表讽刺”“什么样的驾驶情境意味着危险”。人类能识别这些现象，并不意味着人类能把识别依据完整显式表达出来。</p>
<p>因此，若目标是让机器获得更强的适应性和泛化能力，系统必须学会从数据中提取模式，而不是永远依赖人类手工枚举所有知识。这就引出了机器学习路线。</p>
<div class="blog_h2"><span class="graybg">连接主义之外，还要看到统计学习</span></div>
<p>早期 AI 叙事常把历史压缩成“符号主义失败，连接主义胜利”。这个说法有一定直观性，但仍然过粗。真正推动现代 AI 成熟的，不只是神经网络路线本身，还包括统计学习（Statistical Learning）这一整套思想：经验风险最小化、泛化误差、正则化、概率建模、优化理论、核方法、集成学习和贝叶斯方法，都对今天的 AI 基础有决定性影响。</p>
<p>也就是说，现代 AI 并不是简单从“写规则”切换到“神经网络万能”，而是逐步形成了三层视角：符号视角强调显式知识与逻辑结构，统计视角强调从样本分布中估计规律，连接主义强调用大规模参数化函数学习复杂表示。今天真正有效的系统，往往同时吸收了这三条传统中的不同优点。</p>
<div class="blog_h1"><span class="graybg">机器为什么能学</span></div>
<div class="blog_h2"><span class="graybg">机器学习的基本思想</span></div>
<p>Arthur Samuel 在 1959 年对机器学习（Machine Learning）的经典定义，核心就在于一句话：<span style="background-color: #c0c0c0;">让计算机在不被显式编程的情况下获得学习能力</span>。这里“不被显式编程”不是说完全没有程序，而是说我们不再把任务规则逐条写死，而是给定数据、目标和优化机制，让系统自己去调整内部参数。</p>
<p>因此，机器学习与传统编程的分工发生了变化。传统编程更像</p>
<span displaypfx="" class="mathjax-container">\[\text{Rules} + \text{Data} \rightarrow \text{Output}\]</span>
<p>而机器学习更像</p>
<span displaypfx="" class="mathjax-container">\[\text{Data} + \text{Targets} + \text{Optimization} \rightarrow \text{Model}\]</span>
<p>人类仍然负责编写训练流程、定义损失函数、设计模型结构和评价指标，但不再手写所有领域规则；真正的映射关系由模型在数据中自动学得。</p>
<div class="blog_h2"><span class="graybg">模型究竟是什么</span></div>
<p>在最抽象的数学意义上，模型（Model）就是一个参数化函数（Parameterized Function）。给定输入 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>，模型输出预测 <span displaypfx="inline-" class="mathjax-container">\(\hat y=f_\theta(x)\)</span>，其中 <span displaypfx="inline-" class="mathjax-container">\(\theta\)</span> 表示模型参数。机器学习训练的目标，就是在庞大的参数空间里找到一组参数，使这个函数尽可能解释数据中的规律。</p>
<p>因此，“训练模型”本质上不是把知识一条条写进程序，而是在参数空间里搜索一个更好的函数。这个函数可以很简单，例如线性回归中的 <span displaypfx="inline-" class="mathjax-container">\(f_\theta(x)=w^\top x+b\)</span>；也可以极其复杂，例如拥有数十亿参数的大语言模型。复杂度不同，但本质没有变：它们都在试图逼近某个把输入映射到输出的规律。</p>
<p>这也是为什么机器学习常被表述为函数拟合（Function Approximation）。区别只在于，AI 面对的函数远比中学里的 <span displaypfx="inline-" class="mathjax-container">\(y=f(x)\)</span> 更复杂：输入可能是图片、文本、语音、视频、图结构或交互历史，输出可能是类别、数值、动作、文本序列甚至多步决策。</p>
<div class="blog_h2"><span class="graybg">训练在做什么</span></div>
<p>训练（Training）就是在反复试错中更新参数。设损失函数（Loss Function）为 <span displaypfx="inline-" class="mathjax-container">\(\ell(f_\theta(x),y)\)</span>，训练集上的目标函数通常写成</p>
<span displaypfx="" class="mathjax-container">\[J(\theta)=\frac{1}{n}\sum_{i=1}^{n}\ell(f_\theta(x_i),y_i)\]</span>
<p>优化算法会根据 <span displaypfx="inline-" class="mathjax-container">\(\nabla_\theta J(\theta)\)</span> 的方向逐步更新参数，使损失下降。对小模型而言，这可以理解为“不断试着把函数曲线调到更贴近数据”；对大模型而言，它仍然是同一件事，只是参数数量、数据规模和优化难度都被放大到了前所未有的量级。</p>
<p>因此，训练并不是神秘的“让机器突然开窍”，而是一套规模化、可重复、可优化的搜索过程。机器学习的历史突破，很大程度上就是让这套搜索过程在更大数据、更复杂模型和更强硬件上变得可行。</p>
<div class="blog_h2"><span class="graybg">一个学习问题由什么构成</span></div>
<p>一个完整的学习问题，通常至少包含六个要素：输入表示 <span displaypfx="inline-" class="mathjax-container">\(x\)</span>、目标或反馈 <span displaypfx="inline-" class="mathjax-container">\(y\)</span>、模型 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span>、损失函数 <span displaypfx="inline-" class="mathjax-container">\(\ell\)</span>、优化算法，以及评估标准。只有把这几件事同时说清楚，问题才真正被定义完成。否则“做一个 AI 模型”这句话本身几乎没有技术含量。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">组成部分</td>
<td style="text-align: center;">它决定什么</td>
<td style="text-align: center;">典型问题</td>
</tr>
</thead>
<tbody>
<tr>
<td>输入表示</td>
<td>模型实际看见什么</td>
<td>文本是分词后 token、图像是像素还是 patch、表格特征是否标准化</td>
</tr>
<tr>
<td>目标 / 反馈</td>
<td>模型被鼓励学会什么</td>
<td>分类标签、回归值、奖励信号、对比学习正负样本</td>
</tr>
<tr>
<td>模型结构</td>
<td>函数族的表达能力与归纳偏置</td>
<td>线性模型、树模型、CNN、Transformer、MoE</td>
</tr>
<tr>
<td>损失函数</td>
<td>什么叫“预测得不好”</td>
<td>交叉熵、均方误差、对比损失、强化学习目标</td>
</tr>
<tr>
<td>优化算法</td>
<td>参数怎样被更新</td>
<td>SGD、AdamW、学习率调度、梯度裁剪</td>
</tr>
<tr>
<td>评估标准</td>
<td>模型是否真的有用</td>
<td>Accuracy、F1、AUC、BLEU、ROUGE、胜率、人工偏好</td>
</tr>
</tbody>
</table>
<p>这张表的意义在于：AI 的成败几乎从来不只由“模型结构”单独决定。很多看似是模型问题的失败，实际来自目标函数错位、输入表示粗糙、数据质量差或评估指标不对。理解这一点，后续学习才不会把注意力全部误投到“模型名字”本身。</p>
<div class="blog_h2"><span class="graybg">训练与推理不是同一件事</span></div>
<p>训练（Training）和推理（Inference）是两个阶段。训练阶段的任务是用大量样本更新参数，让模型学到函数 <span displaypfx="inline-" class="mathjax-container">\(f_\theta\)</span>；推理阶段的任务则是在参数已经固定后，用这个函数处理新的输入。很多初学者会把“模型生成答案”与“模型学会能力”混成一件事，但这两个阶段的资源需求、系统结构和优化目标都不同。</p>
<p>训练更关注数据规模、梯度计算、参数更新和收敛稳定性；推理更关注延迟、吞吐、显存占用、服务成本与输出质量。例如一个模型可能训练非常昂贵，但推理相对便宜；也可能训练已完成，但由于上下文窗口、解码策略和缓存机制设计不佳，推理时依然很难落地。后续第 3 篇和第 4 篇会反复遇到这个区分。</p>
<div class="blog_h2"><span class="graybg">监督不是唯一反馈来源</span></div>
<p>视频中的直觉更偏向“给很多样本，模型就去学”。这当然是核心，但还需要补充：模型并不只从人工标注标签中学习。现代 AI 至少有三类主要反馈来源。第一类是监督学习（Supervised Learning），即直接给出正确答案或目标标签。第二类是自监督学习（Self-Supervised Learning），即从数据自身构造预测任务，例如掩码语言建模或下一个 token 预测。第三类是强化学习（Reinforcement Learning），即系统通过与环境交互，根据奖励信号优化长期行为。</p>
<p>这三类反馈机制并不是互斥关系。很多现代系统会把它们组合起来：先用自监督预训练打好通用表示，再用监督微调适配具体任务，最后再用强化学习或偏好优化调整行为。这种多阶段训练配方，正是现代大模型系统的常见做法。</p>
<div class="blog_h1"><span class="graybg">一个 AI 系统由什么组成</span></div>
<div class="blog_h2"><span class="graybg">数据、模型、目标、反馈</span></div>
<p>导论里很容易把“AI”误听成某个神奇单体，好像只要有一个模型名字，一切就自动发生了。真实系统远不是这样。一个可工作的 AI 系统，至少要把数据、模型、目标、反馈、训练、评估和部署串成一条闭环。模型当然是中心，但它只是闭环中的一个部件，而不是全部。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">部件</td>
<td style="text-align: center;">作用</td>
<td style="text-align: center;">若出问题会怎样</td>
</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>
<tr>
<td>部署系统</td>
<td>决定模型在真实环境里的延迟、成本、可靠性与安全性</td>
<td>即使模型本身很强，也无法稳定落地</td>
</tr>
</tbody>
</table>
<p>从这个角度看，AI 更像一条生产线，而不是一个神秘黑盒。后续 1 到 4 篇其实就是沿这条生产线逐步展开：先理解数学语言，再理解模型与训练，再理解基础模型，再理解系统层落地。</p>
<div class="blog_h2"><span class="graybg">数据不是越多越好，而是越对越好</span></div>
<p>“海量数据”当然重要，但导论里还必须补一句更现实的话：数据不是越多就必然越好，而是越接近任务结构、越干净、越有覆盖、越能提供有效反馈越好。大量重复、偏差严重、标注粗糙或分布失真的数据，完全可能把模型推向错误方向。现代 AI 的许多难题，其实不是出在模型结构，而是出在数据分布和目标构造不匹配。</p>
<p>因此，AI 工程里常常真正稀缺的不是“任意数据”，而是高质量、覆盖关键边界条件、与评估目标一致的数据。这个判断对后续所有篇章都成立，无论是在经典机器学习、深度学习还是大模型系统里。</p>
<div class="blog_h2"><span class="graybg">评估不是最后一步，而是前提</span></div>
<p>另一个常见误区，是把评估（Evaluation）理解成模型训完之后再顺手看一眼分数。更准确的看法应该是：评估标准在问题定义阶段就已经介入了，因为它决定我们到底在优化什么。若任务真正关心的是风险控制，单纯追求 Accuracy 可能毫无意义；若任务关心排序质量，分类正确率就不是核心；若任务面向真实用户，延迟、稳定性和校准能力也会与“准确率”同等重要。</p>
<p>所以一个成熟 AI 系统的闭环顺序不是“先训练，最后随便评一下”，而是“先定义任务与评估，再决定模型和训练”。很多失败项目的问题，不是模型不够先进，而是一开始就没有把问题定义清楚。</p>
<div class="blog_h1"><span class="graybg">为什么直到近十几年才爆发</span></div>
<div class="blog_h2"><span class="graybg">数据是学习的原料</span></div>
<p>机器若要从经验中学习，首先必须“见得足够多”。互联网和数字化社会提供了前所未有的数据规模：网页文本、百科知识、社交媒体、搜索日志、点击记录、语音、图片、视频、传感器数据，几乎把大量人类行为与知识活动都转写成了可计算的数字语料。没有这些数据，模型就像只见过极少样本的人，难以形成稳定概念。</p>
<p>数据的重要性不只在量，还在覆盖范围。若训练数据太少，模型学不到稳健规律；若数据分布太窄，模型就很难泛化到复杂世界。现代 AI 的许多能力之所以能够出现，前提正是训练集规模和多样性的剧烈提升。</p>
<div class="blog_h2"><span class="graybg">算力让搜索过程成为现实</span></div>
<p>深度学习和大模型训练，本质上依赖海量矩阵乘法、卷积、注意力和梯度计算。若没有足够强的硬件，这些优化过程在工程上根本跑不动。GPU、TPU 以及后续更专业的 AI 加速器，把高度并行的张量计算变成了现实，也让“数十亿参数、数万亿 token”这类训练规模进入可操作区间。</p>
<p>因此，AI 并不是一个“理论早就成熟、只是最近才被发现”的领域。更准确地说，很多核心思想提出得很早，但只有当<span style="background-color: #c0c0c0;">数据、算力、算法和软件基础设施</span>同时成熟，思想才能真正落地成具有产业影响力的系统。</p>
<div class="blog_h2"><span class="graybg">算法与工程闭环</span></div>
<p>即使有数据和硬件，若缺少有效算法，训练仍然可能失败。现代 AI 的成功同样依赖优化方法、初始化、正则化、残差连接、归一化、分布式训练、自动求导框架和部署工具链的共同成熟。真正让人工智能爆发的，不是单个孤立发明，而是整套技术生态形成了闭环。</p>
<p>换言之，AI 的发展并不是线性地“某一年突然变聪明”，而是多条技术线在同一时期交汇：数据解决“学什么”，算力解决“算得动吗”，算法解决“学得稳吗”，工程系统解决“能不能规模化复现与部署”。</p>
<div class="blog_h2"><span class="graybg">规模效应与预训练范式</span></div>
<p>近十几年 AI 爆发还有一个视频里只隐约提到、但实际上极其关键的因素：规模效应（Scaling Effect）。当模型参数、训练数据和计算预算在一定范围内同步扩大时，模型性能往往不是随机波动，而会呈现相对平滑、可预测的提升趋势。也就是说，很多能力并不是靠手工加入某个单独规则突然获得，而是在足够大的训练规模下逐步显现出来。</p>
<p>预训练（Pretraining）因此成为现代 AI 的核心范式。其基本逻辑是：先在大规模通用数据上学习通用表示或通用预测能力，再通过微调（Finetuning）、指令对齐（Instruction Tuning）或其他后训练方式适配具体任务。这个范式改变了整个行业的工作方式，因为模型不再是“每个任务单独训练一个小系统”，而更像一个可复用的能力底座。</p>
<div class="blog_h1"><span class="graybg">现代人工智能的几条主线</span></div>
<div class="blog_h2"><span class="graybg">机器学习</span></div>
<p>机器学习是现代 AI 的第一条主线。它的关键突破在于：不再完全依赖手工规则，而是让系统从数据中学习统计规律。在线性模型、树模型、支持向量机、聚类、概率模型和集成学习这些方法中，模型容量通常相对可控，特征工程的地位仍然很高，人类需要较多参与“该喂什么特征”。</p>
<p>这一阶段的 AI 已经能在很多任务上显著优于纯规则系统，例如垃圾邮件识别、信用风险评估、推荐排序和基本文本分类。但它的边界也很清楚：模型更多是在人工定义好的特征空间里工作，而不是从原始高维感知数据中自主学习层级表示。</p>
<div class="blog_h2"><span class="graybg">深度学习</span></div>
<p>深度学习（Deep Learning）把机器学习进一步推进为表示学习（Representation Learning）。系统不再严重依赖人工手工提特征，而是使用多层神经网络从原始输入中逐层学习更抽象的表示。图像中的边缘、纹理、部件与对象，语音中的音素与韵律，文本中的词义、语法和上下文关系，开始由模型内部自动形成。</p>
<p>这条主线带来了感知智能的大规模突破。图像识别、目标检测、语音识别、人脸识别、机器翻译、自动驾驶感知与 AlphaGo 这样的系统，都建立在深度学习及其扩展方法之上。它们展现了极强的专用智能，但在广泛迁移和跨任务统一上仍存在明显限制。</p>
<div class="blog_h2"><span class="graybg">强化学习</span></div>
<p>强化学习（Reinforcement Learning, RL）讨论的是另一类问题：当系统不是只做一次静态预测，而是要在环境中连续行动、不断接收反馈并优化长期收益时，该怎样学习策略（Policy）。它和监督学习最大的不同，是反馈不一定立即出现，也不一定告诉模型“正确答案是什么”；系统往往只能看到某种奖励（Reward）或惩罚，再自己推断哪些行为序列更优。</p>
<p>强化学习之所以在 AI 总纲里重要，不只是因为 AlphaGo。它代表了从“识别与预测”走向“决策与行动”的关键跨越。后来的 RLHF、RLAIF、Agent 规划、自动控制和机器人学习，都延续了这条主线。即使很多大模型系统的主体不是用 RL 从零训练出来，强化学习仍然是现代 AI 中不可绕开的基本思想之一。</p>
<div class="blog_h2"><span class="graybg">大语言模型与生成式 AI</span></div>
<p>大语言模型（Large Language Model, LLM）把 AI 推到了第三条主线：生成式 AI（Generative AI）。与很多传统系统主要做“判断题”不同，语言模型的训练目标是不断预测下一个 token。这一目标看似简单，却迫使模型在大规模文本中学习词法、句法、语义、知识、逻辑关系和风格模式，从而涌现出问答、总结、翻译、写作、代码生成与多步推理等能力。</p>
<p>Transformer 是这一阶段最关键的结构基础。通过自注意力（Self-Attention）、残差连接和大规模预训练，语言模型第一次在统一架构中同时表现出较强的通用知识调用能力、生成能力和任务迁移能力。这也是为什么后续篇章会把 Transformer 和大模型单独拿出来展开。</p>
<div class="blog_h2"><span class="graybg">几条主线的关系</span></div>
<p>这些主线不是互相替代的断裂历史，而是一条不断扩展的连续谱。机器学习提供了“从数据中学习”的基本范式；深度学习进一步把表示学习纳入模型内部；强化学习把学习目标扩展到行动与长期回报；大语言模型则把预训练、生成和通用迁移能力推到更高尺度。它们彼此叠加，而不是互相否定。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">阶段</td>
<td style="text-align: center;">核心问题</td>
<td style="text-align: center;">主要特征</td>
<td style="text-align: center;">典型代表</td>
</tr>
</thead>
<tbody>
<tr>
<td>机器学习</td>
<td>如何从数据中学习统计规律</td>
<td>特征工程重要；模型相对浅；强调监督学习与泛化</td>
<td>逻辑回归、SVM、随机森林、GBDT</td>
</tr>
<tr>
<td>深度学习</td>
<td>如何自动学习层级表示</td>
<td>多层神经网络；端到端训练；感知任务突破</td>
<td>CNN、RNN、ResNet、AlphaGo</td>
</tr>
<tr>
<td>强化学习</td>
<td>如何在行动中根据反馈优化长期策略</td>
<td>强调状态、动作、奖励和长期回报；面向决策与控制</td>
<td>Q-Learning、Policy Gradient、AlphaGo、RLHF</td>
</tr>
<tr>
<td>大语言模型</td>
<td>如何在统一架构中获得通用生成与迁移能力</td>
<td>Transformer、自监督预训练、生成式任务、规模效应</td>
<td>GPT、PaLM、Llama、Claude 类模型</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">为什么语言模型率先突破</span></div>
<div class="blog_h2"><span class="graybg">语言是世界知识的高密度压缩</span></div>
<p>语言之所以在 AGI 讨论中占据中心位置，一个重要原因是：人类大量知识本来就以文本形式被压缩和记录。科学规律、历史经验、社会规范、技术文档、小说叙事、代码、对话、数学推理，许多内容都已经被写进文字体系。对模型而言，学习语言并不只是学习词语排列，而是在学习人类如何编码世界结构。</p>
<p>例如，“所以”常常隐含因果关系，“但是”常常隐含转折，“如果……那么……”隐含条件推理，故事叙事中又包含时间、动机、行为与结果的链式结构。语言并不是世界本身，但它是人类认知世界的一种高度压缩表示。因此，在海量文本上训练下一个 token 预测器，可能间接逼迫模型学习大量世界规律。</p>
<div class="blog_h2"><span class="graybg">下一个 token 预测为什么会产生复杂能力</span></div>
<p>语言模型的训练目标表面上非常简单：给定上下文 <span displaypfx="inline-" class="mathjax-container">\(x_{1:t-1}\)</span>，预测下一个 token <span displaypfx="inline-" class="mathjax-container">\(x_t\)</span>，也就是最大化</p>
<span displaypfx="" class="mathjax-container">\[p(x_t\mid x_1,\dots,x_{t-1})\]</span>
<p>但这个目标的约束其实非常强。若模型想要准确预测法律文本中的下一句，它就必须理解法律概念和逻辑结构；若想预测一段代码的下一行，它就必须理解语法、控制流与 API 用法；若想预测一个故事的结局，它就要理解人物动机、叙事结构和常识。这使得“下一个 token 预测”虽然形式简单，内在上却会逼迫模型学习深层模式。</p>
<p>因此，大语言模型看起来像是在逐词生成，实质上是在通过这个统一目标吸收大量分布式知识表示。这也是现代生成式 AI 产生涌现能力（Emergent Capability）的关键背景之一。</p>
<div class="blog_h2"><span class="graybg">为什么不是视觉先走到通用智能</span></div>
<p>从直觉上看，视觉似乎比语言更接近真实世界，因此很多人曾认为计算机视觉（Computer Vision, CV）才是通向通用智能的最直接道路。这个判断并不荒谬，因为视觉确实与空间、物体、运动和物理交互关系更紧。但历史上率先爆发的却是语言模型，其重要原因在于：语言数据比高质量世界交互数据更容易大规模收集、更容易统一标注、更容易压缩高层知识结构。</p>
<p>换言之，视觉更贴近世界本体，语言更贴近人类已经整理好的世界知识。前者更“原始”，后者更“高密度”。语言模型之所以先爆发，不一定说明语言比空间更根本，而更可能说明语言先在可训练性、数据规模和目标统一性上形成了更好的工程条件。</p>
<div class="blog_h1"><span class="graybg">当前 AI 的能力与边界</span></div>
<div class="blog_h2"><span class="graybg">它擅长什么</span></div>
<p>若把当代 AI 的长处概括一下，它最擅长的是：在大规模数据中提取统计规律；在高维输入中学习分布式表示；在局部定义清晰的目标上反复优化；在单次或短链任务中产生非常强的模式识别、生成和匹配能力。图像分类、语音识别、推荐排序、检索匹配、文档摘要、代码补全和多轮问答，都属于这种优势可以被直接放大的领域。</p>
<p>这些能力的共同底层，是模型非常擅长处理大规模模式压缩与重组。它可以把海量经验浓缩进参数，把看似分散的线索组合成输出，这正是现代 AI 之所以显得“聪明”的原因。</p>
<div class="blog_h2"><span class="graybg">它为什么还会犯低级错误</span></div>
<p>AI 的弱点同样有共性。第一，它学到的大多是分布规律，而不一定是人类意义上的显式因果结构。第二，它可能非常擅长局部模式匹配，但在长链规划、跨步骤一致性、外部事实校验和真实世界 grounding 上仍不稳定。第三，它的输出是否可信，很大程度上取决于训练分布、上下文、工具链和验证机制，而不是只取决于模型参数规模。</p>
<p>这也是为什么大模型会出现幻觉（Hallucination）：模型并不总是在“查询一个外部真值数据库”，它更多是在根据训练中见过的大量分布模式，生成当前看起来最合理的延续。若任务需要精确事实、长链一致性或外部环境对齐，单靠内部参数往往不够，必须依赖检索、工具调用、状态管理与验证闭环。</p>
<div class="blog_h2"><span class="graybg">会生成不等于已经理解世界</span></div>
<p>生成能力之所以容易让人误判，是因为流畅输出很像理解。一个模型能写得很像、说得很像、总结得很像，并不自动意味着它已经拥有与人类相同的世界模型。理解至少还涉及可迁移性、反事实推理、跨情境一致性、与环境交互后的自我修正能力，以及对物理和社会约束的稳定把握。</p>
<p>因此，导论里必须保留一个清醒的判断：现代 AI 已经极大突破了“模式识别”和“符号生成”的边界，但它距离稳定、统一、具身、可验证的通用智能仍有明显距离。既不能低估它已经做到的事，也不能因为生成效果惊艳就提前宣布问题已经全部解决。</p>
<div class="blog_h1"><span class="graybg">AGI 仍然悬而未决</span></div>
<div class="blog_h2"><span class="graybg">语言智能是否足够</span></div>
<p>即使语言模型取得巨大进展，是否仅靠语言就能实现 AGI，仍然存在强烈争议。一种观点认为，语言已经高度压缩了世界知识，大规模语言建模因此足以逼近通用智能；另一种观点则认为，语言只是世界的符号映射，而不是世界本身，真正的智能还需要空间感知、物理直觉、行动反馈和长期交互经验。</p>
<p>这种分歧的关键不在“语言有没有价值”，而在“语言是否足够”。如果一个系统不理解空间关系、物体恒常性、因果交互和物理约束，那么它可能仍然停留在对符号统计规律的高度拟合，而没有真正建立起可用于行动的世界模型（World Model）。</p>
<div class="blog_h2"><span class="graybg">空间智能与世界模型</span></div>
<p>空间智能（Spatial Intelligence）强调，智能体不仅要会处理符号和文本，还要能理解物体、距离、运动、遮挡、三维结构和物理一致性。对生物而言，这种能力与生存高度相关；对机器而言，它决定了系统是否能从“会说”进一步走向“会看、会做、会交互”。</p>
<p>这也是为什么近年来多模态模型、世界模型、机器人学习和具身智能（Embodied AI）重新成为 AGI 讨论中的核心方向。未来更可能出现的，不是“语言智能”和“空间智能”二选一，而是多种能力逐步汇合：语言提供高密度知识压缩，感知与行动提供对真实世界约束的接触，二者共同构成更完整的通用智能基础。</p>
<div class="blog_h1"><span class="graybg">如何阅读后续篇章</span></div>
<div class="blog_h2"><span class="graybg">这一套 AI 知识为什么这样编排</span></div>
<p>AI 学习最常见的问题不是资料不够，而是层级混乱。很多人一开始就直接进入模型名称、训练技巧和论文细节，但没有先回答几个最根本的问题：模型为什么存在、训练到底在优化什么、泛化为什么重要、不同阶段的 AI 方法究竟解决了什么问题。没有这层导论，后续知识就容易变成孤立名词堆。</p>
<p>因此，这套 quick reference 采用从抽象到具体的顺序：</p>
<ul>
<li>第 0 篇先回答“什么是智能、什么是模型、AI 为什么能学”。</li>
<li>第 1 篇给出数学语言：向量、矩阵、导数、概率、信息量。</li>
<li>第 2 篇进入机器学习、神经网络、训练、评估与正则化。</li>
<li>第 3 篇进入 Transformer、大模型、多模态与推理优化。</li>
<li>第 4 篇进入上下文工程、RAG、Agent 与系统层落地。</li>
</ul>
<p>按这条顺序阅读，后面的每一层都会回答前一层留下的问题，而不是凭空多出一个新术语体系。这样整套内容才更接近一张完整地图，而不是若干互不连通的知识岛。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ai-knowledge-quick-ref-0">人工智能理论知识 - 简介</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ai-knowledge-quick-ref-0/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>DevPod 远程开发环境搭建笔记</title>
		<link>https://blog.gmem.cc/devpod</link>
		<comments>https://blog.gmem.cc/devpod#comments</comments>
		<pubDate>Fri, 10 Apr 2026 07:22:22 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Cloud]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=42107</guid>
		<description><![CDATA[<p>DevPod 是一个开源的开发环境管理工具，支持在 Docker、K8s、SSH 主机及多种云平台上创建可复现的开发环境。本文记录在 K8s 集群上使用 DevPod 搭建远程开发环境的完整实践，涵盖持久卷策略、自定义镜像、文件同步、IDE 集成以及 GPU 接入中遇到的典型问题与解决方案。 DevPod 简介 DevPod 由 Loft Labs 开发，核心理念是将开发环境的定义与基础设施解耦。开发者通过 [crayon-69e1486b9cf84495701713-i/] 描述环境需求（基础镜像、工具链、端口），DevPod 负责在指定的 Provider <a class="read-more" href="https://blog.gmem.cc/devpod">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/devpod">DevPod 远程开发环境搭建笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>DevPod 是一个开源的开发环境管理工具，支持在 Docker、K8s、SSH 主机及多种云平台上创建可复现的开发环境。本文记录在 K8s 集群上使用 DevPod 搭建远程开发环境的完整实践，涵盖持久卷策略、自定义镜像、文件同步、IDE 集成以及 GPU 接入中遇到的典型问题与解决方案。</p>
<div class="blog_h1"><span class="graybg">DevPod 简介</span></div>
<p>DevPod 由 Loft Labs 开发，核心理念是将开发环境的定义与基础设施解耦。开发者通过 <pre class="crayon-plain-tag">devcontainer.json</pre> 描述环境需求（基础镜像、工具链、端口），DevPod 负责在指定的 <span style="background-color: #c0c0c0;">Provider</span> 上创建并管理对应的 Workspace。</p>
<p>三个核心概念：</p>
<ul>
<li>Provider：基础设施后端。内置支持 Docker、K8s、SSH，以及 AWS、GCP、Azure 等云平台。</li>
<li>Workspace：一个独立的开发环境实例，对应 Provider 上的一个容器或虚拟机。</li>
<li>devcontainer.json：遵循 Dev Container 规范的配置文件，定义镜像、生命周期钩子、端口转发等。</li>
</ul>
<p>与 GitHub Codespaces 和 Gitpod 相比，DevPod 的关键差异在于它是<span style="background-color: #c0c0c0;">客户端工具</span>——不依赖 SaaS 平台，可以对接任何你已有的基础设施。在自建 K8s 集群的场景下，这意味着完全掌控网络、存储和安全策略。</p>
<div class="blog_h1"><span class="graybg">K8s Provider 架构</span></div>
<p>选择 K8s 作为 Provider 时，DevPod 在目标集群中创建 Pod 来承载开发环境。整个配置由三个文件协同工作：</p>
<ol>
<li><pre class="crayon-plain-tag">devcontainer.json</pre>：声明基础镜像、工作目录、端口转发、生命周期命令。</li>
<li><pre class="crayon-plain-tag">pod-manifest.yaml</pre>：K8s Pod 模板，定义安全上下文、资源限制、卷挂载等 K8s 特有配置。</li>
<li>编排脚本（如 <pre class="crayon-plain-tag">devpod.sh</pre>）：封装 <pre class="crayon-plain-tag">devpod up</pre>、文件同步、环境初始化等流程，是胶水层。</li>
</ol>
<div class="blog_h2"><span class="graybg">Workspace 生命周期</span></div>
<p>典型的操作流程：</p>
<pre class="crayon-plain-tag"># 创建并启动 Workspace（在 K8s 中创建 Pod）
devpod up . --ide none --provider K8s

# 同步本地源码到远端
rsync -az --exclude='node_modules' ./project/ remote:/workspace/project/

# SSH 进入开发环境
devpod ssh my-workspace

# 停止（Pod 被删除，PVC 保留）
devpod stop my-workspace

# 彻底删除（Pod + PVC 全部清理）
devpod delete my-workspace</pre>
<p>关键行为：<pre class="crayon-plain-tag">devpod stop</pre> 删除 Pod 但保留 PVC（Persistent Volume Claim），下次 <pre class="crayon-plain-tag">devpod up</pre> 会重建 Pod 并挂回同一 PVC。这意味着工作区数据在 Pod 重建之间是持久的。</p>
<div class="blog_h2"><span class="graybg">多环境管理</span></div>
<p>通过编排脚本的参数区分环境，典型做法是为每个环境维护独立的 Pod Manifest：</p>
<pre class="crayon-plain-tag"># 编排脚本示例：按环境选择 Manifest 和磁盘大小
case "$ENV" in
  prod) MANIFEST="pod-manifest.yaml";      DISK="300Gi" ;;
  dev)  MANIFEST="pod-manifest-dev.yaml";   DISK="50Gi"  ;;
  test) MANIFEST="pod-manifest-test.yaml";  DISK="500Gi" ;;
esac

devpod up . --ide none \
  --provider K8s \
  --provider-option DISK_SIZE="$DISK" \
  --provider-option POD_MANIFEST="$MANIFEST"</pre>
<p>不同环境可以指定不同的节点选择器、资源配额和安全策略，而共享同一套 <pre class="crayon-plain-tag">devcontainer.json</pre> 和基础镜像。</p>
<div class="blog_h1"><span class="graybg">持久卷挂载策略</span></div>
<p>PVC 的挂载点选择直接决定了哪些数据能在 Pod 重建后存活。</p>
<div class="blog_h2"><span class="graybg">推荐：挂载到 $HOME</span></div>
<p>将 PVC 挂载到容器的 <pre class="crayon-plain-tag">$HOME</pre> 目录（如 <pre class="crayon-plain-tag">/root</pre>）是最省心的方案。好处是：</p>
<ul>
<li>IDE 的 Server 端（VS Code Server、Cursor Server）默认安装在 <pre class="crayon-plain-tag">~/.vscode-server</pre> 或 <pre class="crayon-plain-tag">~/.cursor-server</pre>，自动落在持久存储上。</li>
<li>工具链配置（<pre class="crayon-plain-tag">~/.nvm</pre>、<pre class="crayon-plain-tag">~/.local/bin</pre>）无需额外符号链接。</li>
<li>Shell 配置文件（<pre class="crayon-plain-tag">~/.bashrc</pre>）也是持久的，环境变量只需注入一次。</li>
</ul>
<p>如果挂载到其他路径（如 <pre class="crayon-plain-tag">/workspace</pre>），则需要为上述目录创建符号链接或在每次 Pod 启动时重新安装工具。</p>
<div class="blog_h2"><span class="graybg">目录布局示例</span></div>
<pre class="crayon-plain-tag">/root/                          # PVC 挂载点 = $HOME
├── .cursor-server/             # IDE Server + 扩展（持久）
│   ├── cli/                    # Server 二进制（可重建）
│   └── extensions/             # 已安装扩展（需保留）
├── .nvm/                       # Node.js 版本管理器（持久）
├── .local/bin/                 # kubectl 等工具（持久）
├── .bashrc                     # Shell 配置（持久）
├── Projects/
│   ├── my-project/             # 项目源码
│   └── shared-libs/            # 共享库
└── .config/                    # 各工具配置</pre>
<div class="blog_h1"><span class="graybg">常用命令</span></div>
<p>DevPod 通过命令行工具 <pre class="crayon-plain-tag">devpod</pre> 管理 Workspace 的完整生命周期。以下是日常开发中最常用的命令。</p>
<div class="blog_h2"><span class="graybg">Provider 管理</span></div>
<p>使用前需要先添加并配置 Provider：</p>
<pre class="crayon-plain-tag"># 添加 Kubernetes Provider
devpod provider add kubernetes

# 查看已配置的 Provider
devpod provider list

# 设置 Provider 选项（如命名空间、Pod Manifest 路径）
devpod provider set-options kubernetes \
  --option KUBERNETES_NAMESPACE=devpod \
  --option POD_MANIFEST=pod-manifest.yaml</pre>
<div class="blog_h2"><span class="graybg">Workspace 生命周期</span></div>
<pre class="crayon-plain-tag"># 创建并启动 Workspace
# --ide none 跳过 IDE 自动连接，适合脚本化流程
devpod up . --provider kubernetes --ide none

# 查看所有 Workspace 状态
devpod list

# SSH 进入 Workspace
devpod ssh my-workspace

# 停止 Workspace（删除 Pod，保留 PVC）
devpod stop my-workspace

# 彻底删除（Pod + PVC 全部清理）
devpod delete my-workspace</pre>
<p>关键行为：<pre class="crayon-plain-tag">stop</pre> 只删除 Pod，PVC 上的数据（IDE 扩展、工具链、源码）全部保留。下次 <pre class="crayon-plain-tag">up</pre> 会重建 Pod 并挂回同一 PVC，环境几乎瞬间恢复。</p>
<div class="blog_h2"><span class="graybg">常用 Provider 选项</span></div>
<p>Kubernetes Provider 支持通过 <pre class="crayon-plain-tag">--provider-option</pre> 传递额外参数：</p>
<pre class="crayon-plain-tag">devpod up . --provider kubernetes --ide none \
  --provider-option DISK_SIZE=100Gi \
  --provider-option POD_MANIFEST=pod-manifest-test.yaml \
  --provider-option KUBERNETES_NAMESPACE=devpod</pre>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%;">选项</td>
<td>说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>DISK_SIZE</td>
<td>PVC 容量，如 50Gi、300Gi。</td>
</tr>
<tr>
<td>POD_MANIFEST</td>
<td>自定义 Pod Manifest 文件路径。</td>
</tr>
<tr>
<td>KUBERNETES_NAMESPACE</td>
<td>Pod 创建的目标命名空间。</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">状态检查与调试</span></div>
<pre class="crayon-plain-tag"># 查看 Workspace 详细状态
devpod status my-workspace

# 直接查看底层 Pod 状态（需要 kubectl 访问同一集群）
kubectl get pod -n devpod -l app=devpod

# 查看 Pod 事件（排查启动失败）
kubectl describe pod my-workspace -n devpod</pre>
<div class="blog_h1"><span class="graybg">配置详解</span></div>
<p><pre class="crayon-plain-tag">devcontainer.json</pre> 是 Dev Container 规范的核心配置文件，定义了开发环境的镜像、生命周期钩子、端口转发、IDE 定制等一切参数。DevPod 完整支持该规范。文件通常位于 <pre class="crayon-plain-tag">.devcontainer/devcontainer.json</pre>。</p>
<p>以下是一个面向 Kubernetes 远程开发的完整示例：</p>
<pre class="crayon-plain-tag">{
  "name": "my-workspace",

  // 预装全部工具的自定义镜像，省去 onCreateCommand 等待
  "image": "registry.example.com/dev/ubuntu:22.04-tools",

  // 工具已烘焙进镜像，跳过首次创建命令
  "onCreateCommand": "true",

  // PVC 挂载到 $HOME（/root），IDE 配置和扩展天然持久化
  // workspaceMount 故意留空——DevPod v0.6.x 的 .devpodignore 存在已知 bug，
  // 大型单仓库会被全量上传。改用自定义 rsync 同步源码。
  "workspaceFolder": "/root",

  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance",
        "ms-python.debugpy",
        "redhat.vscode-yaml",
        "ms-kubernetes-tools.vscode-kubernetes-tools"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "editor.formatOnSave": true,
        "terminal.integrated.defaultProfile.linux": "bash"
      }
    }
  },

  "forwardPorts": [8000, 8080, 5432, 6379],
  "portsAttributes": {
    "8000": { "label": "API Server" },
    "8080": { "label": "Web UI" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
    "6379": { "label": "Redis", "onAutoForward": "silent" }
  },
  "otherPortsAttributes": {
    "onAutoForward": "silent"
  }
}</pre>
<div class="blog_h2"><span class="graybg">镜像与构建</span></div>
<p>指定容器基础镜像有两种方式：直接引用镜像或通过 Dockerfile 构建。</p>
<p><pre class="crayon-plain-tag">image</pre> 字段接受任何 OCI 镜像地址（DockerHub、GHCR、私有仓库均可）。对于 Kubernetes 远程开发，推荐<span style="background-color: #c0c0c0;">预构建镜像</span>而非运行时构建——将所有开发工具烘焙进镜像可以将 Pod 启动时间从分钟级缩短到秒级。</p>
<p>如果需要在镜像基础上定制，可以使用 <pre class="crayon-plain-tag">build</pre> 字段：</p>
<pre class="crayon-plain-tag">{
  "build": {
    "dockerfile": "Dockerfile",
    "context": "..",
    "args": {
      "PYTHON_VERSION": "3.11"
    }
  }
}</pre>
<p><pre class="crayon-plain-tag">context</pre> 默认为 <pre class="crayon-plain-tag">"."</pre>（即 <pre class="crayon-plain-tag">devcontainer.json</pre> 所在目录）。设为 <pre class="crayon-plain-tag">".."</pre> 可以在 Dockerfile 中引用项目根目录的文件。</p>
<div class="blog_h2"><span class="graybg">workspaceFolder 与 workspaceMount</span></div>
<p><pre class="crayon-plain-tag">workspaceFolder</pre> 定义 IDE 连接后默认打开的目录。在 Kubernetes 场景下，建议将其设为 PVC 的挂载点（如 <pre class="crayon-plain-tag">/root</pre>），使工作区与持久存储完全对齐。</p>
<p><pre class="crayon-plain-tag">workspaceMount</pre> 控制本地源码如何挂载到容器。在本地 Docker 场景下它很有用，但在 Kubernetes 远程开发中通常<span style="background-color: #c0c0c0;">故意留空</span>。原因是 DevPod v0.6.x 存在一个已知问题（<a href="https://github.com/loft-sh/devpod/issues/1885">#1885</a>）：<pre class="crayon-plain-tag">.devpodignore</pre> 在流式上传本地仓库时被忽略，导致大型工作区（包括 venv、node_modules 等）被全量上传。更好的做法是使用自定义 rsync 脚本精确控制同步内容。</p>
<div class="blog_h2"><span class="graybg">生命周期钩子</span></div>
<p>Dev Container 规范定义了六个生命周期钩子，按以下顺序执行：</p>
<pre class="crayon-plain-tag">initializeCommand     # 在宿主机上执行（每次启动）
  ↓
onCreateCommand       # 容器首次创建后执行（仅一次）
  ↓
updateContentCommand  # 源码更新后执行（至少一次）
  ↓
postCreateCommand     # 分配给用户后执行（可访问用户密钥）
  ↓
postStartCommand      # 每次容器启动后执行
  ↓
postAttachCommand     # 每次 IDE 连接后执行</pre>
<p>每个钩子都接受三种格式：</p>
<ul>
<li>字符串：通过 <pre class="crayon-plain-tag">/bin/sh</pre> 执行。</li>
<li>数组：直接执行，不经过 shell（更安全）。</li>
<li>对象：多个命名命令<span style="background-color: #c0c0c0;">并行执行</span>，适合同时启动多个服务。</li>
</ul>
<pre class="crayon-plain-tag">{
  "postAttachCommand": {
    "api-server": "cd /root/api &amp;&amp; python -m uvicorn main:app --port 8000",
    "worker": "cd /root/worker &amp;&amp; python -m celery -A tasks worker"
  }
}</pre>
<p>实践建议：</p>
<ul>
<li>如果所有工具已烘焙进镜像，将 <pre class="crayon-plain-tag">onCreateCommand</pre> 设为 <pre class="crayon-plain-tag">"true"</pre> 跳过。</li>
<li><pre class="crayon-plain-tag">postStartCommand</pre> 适合放启动时的环境检查或服务预热。</li>
<li><pre class="crayon-plain-tag">waitFor</pre> 字段控制 IDE 在哪个阶段之后才开始连接，默认为 <pre class="crayon-plain-tag">"updateContentCommand"</pre>。</li>
</ul>
<div class="blog_h2"><span class="graybg">IDE 定制</span></div>
<p><pre class="crayon-plain-tag">customizations.vscode</pre> 下可以声明扩展和设置，IDE 连接后自动应用：</p>
<pre class="crayon-plain-tag">"customizations": {
  "vscode": {
    "extensions": [
      "ms-python.python",
      "ms-python.vscode-pylance",
      "ms-python.debugpy",
      "redhat.vscode-yaml",
      "ms-kubernetes-tools.vscode-kubernetes-tools"
    ],
    "settings": {
      "python.defaultInterpreterPath": "/usr/local/bin/python",
      "editor.formatOnSave": true,
      "terminal.integrated.defaultProfile.linux": "bash"
    }
  }
}</pre>
<p><pre class="crayon-plain-tag">extensions</pre> 中声明的扩展会在首次连接时自动安装到远端。结合 PVC 持久化，后续连接无需重复安装。<pre class="crayon-plain-tag">settings</pre> 中的配置优先级高于用户本地设置，确保团队成员使用一致的编辑器行为。</p>
<div class="blog_h2"><span class="graybg">端口转发</span></div>
<p><pre class="crayon-plain-tag">forwardPorts</pre> 声明的端口会在 IDE 连接后自动转发到本地。容器内的服务在这些端口上启动时，本地浏览器可以直接通过 <pre class="crayon-plain-tag">localhost:port</pre> 访问，无需任何手动设置。</p>
<p><pre class="crayon-plain-tag">portsAttributes</pre> 为每个端口配置显示名称和行为：</p>
<pre class="crayon-plain-tag">"forwardPorts": [8000, 8080, 5432, 6379],
"portsAttributes": {
  "8000": { "label": "API Server" },
  "8080": { "label": "Web UI", "onAutoForward": "openBrowser" },
  "5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
  "6379": { "label": "Redis", "onAutoForward": "silent" }
},
"otherPortsAttributes": {
  "onAutoForward": "silent"
}</pre>
<p><pre class="crayon-plain-tag">onAutoForward</pre> 控制端口首次被检测到时的行为：<pre class="crayon-plain-tag">"notify"</pre>（默认，弹通知）、<pre class="crayon-plain-tag">"openBrowser"</pre>（自动打开浏览器）、<pre class="crayon-plain-tag">"silent"</pre>（静默转发，适合数据库等后台服务）、<pre class="crayon-plain-tag">"ignore"</pre>（完全忽略）。<pre class="crayon-plain-tag">otherPortsAttributes</pre> 为未显式配置的端口设置默认行为。</p>
<div class="blog_h2"><span class="graybg">环境变量</span></div>
<p>Dev Container 规范区分两层环境变量：</p>
<ul>
<li><pre class="crayon-plain-tag">containerEnv</pre>：设置在容器本身上，所有进程可见，容器生命周期内不变（修改需重建）。</li>
<li><pre class="crayon-plain-tag">remoteEnv</pre>：仅对 IDE 启动的进程（终端、任务、调试）可见，可以引用 <pre class="crayon-plain-tag">${containerEnv:VAR}</pre> 来扩展已有变量，修改后无需重建容器。</li>
</ul>
<pre class="crayon-plain-tag">{
  "containerEnv": {
    "PYTHONPATH": "/root/libs/common:/root/libs/shared"
  },
  "remoteEnv": {
    "PATH": "${containerEnv:PATH}:/root/.local/bin"
  }
}</pre>
<p>两个字段都支持 <pre class="crayon-plain-tag">${localEnv:VAR}</pre> 语法引用宿主机环境变量，例如 <pre class="crayon-plain-tag">${localEnv:HOME}</pre>。</p>
<div class="blog_h2"><span class="graybg">Features</span></div>
<p>Dev Container Features 是可复用的 Dockerfile 片段，以 OCI 制品形式分发。通过 <pre class="crayon-plain-tag">features</pre> 字段可以在不修改基础镜像的情况下安装额外工具：</p>
<pre class="crayon-plain-tag">{
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
      "version": "latest"
    },
    "ghcr.io/devcontainers/features/node:1": {
      "version": "22"
    }
  }
}</pre>
<p>可用的 Features 列表参见 <a href="https://containers.dev/features">containers.dev/features</a>。对于 Kubernetes 远程开发，推荐将工具烘焙进基础镜像而非依赖 Features，以避免每次创建容器时的安装延迟。Features 更适合本地 Docker 场景下的快速原型搭建。</p>
<div class="blog_h2"><span class="graybg">容器行为控制</span></div>
<p>几个影响容器运行方式的字段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%;">字段</td>
<td style="width: 20%;">默认值</td>
<td>说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>overrideCommand</td>
<td>true</td>
<td>覆盖容器默认命令为无限循环（保持容器存活）。使用自定义镜像时通常保持默认。</td>
</tr>
<tr>
<td>shutdownAction</td>
<td>stopContainer</td>
<td>IDE 关闭时的行为：stopContainer（停止容器）、none（保持运行）。K8s 场景建议 none。</td>
</tr>
<tr>
<td>init</td>
<td>false</td>
<td>使用 tini 作为 init 进程，处理僵尸进程回收。</td>
</tr>
<tr>
<td>privileged</td>
<td>false</td>
<td>特权模式。Docker 场景下通过此字段设置，K8s 场景在 Pod Manifest 中设置。</td>
</tr>
<tr>
<td>containerUser</td>
<td>root 或 Dockerfile USER</td>
<td>容器内所有操作使用的用户。</td>
</tr>
<tr>
<td>remoteUser</td>
<td>同 containerUser</td>
<td>IDE 终端和任务使用的用户，可以与 containerUser 不同。</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">预定义变量</span></div>
<p><pre class="crayon-plain-tag">devcontainer.json</pre> 的字符串值中可以使用以下预定义变量：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 40%;">变量</td>
<td>含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>${localEnv:VAR_NAME}</td>
<td>宿主机环境变量，支持默认值：${localEnv:VAR:default}</td>
</tr>
<tr>
<td>${containerEnv:VAR_NAME}</td>
<td>容器环境变量（仅在 remoteEnv 中可用）</td>
</tr>
<tr>
<td>${localWorkspaceFolder}</td>
<td>宿主机上打开的工作区路径</td>
</tr>
<tr>
<td>${containerWorkspaceFolder}</td>
<td>容器内的工作区路径</td>
</tr>
<tr>
<td>${devcontainerId}</td>
<td>容器的唯一标识符，重建后保持稳定</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">镜像定制</span></div>
<p>Dev Container 的 <pre class="crayon-plain-tag">image</pre> 字段虽然可以填写任意公共镜像，但在 Kubernetes 远程开发场景下，<span style="background-color: #c0c0c0;">应当构建专用的基础镜像，将所有开发工具、语言运行时和系统库固化到镜像层中</span>。这样做的好处是：</p>
<ul>
<li>Pod 启动即可用，无需等待 <pre class="crayon-plain-tag">onCreateCommand</pre> 安装依赖。</li>
<li>环境一致性有保障——团队成员共享同一镜像，不会因安装顺序或网络问题导致环境差异。</li>
<li>Pod 重建后工具链自动恢复，不依赖外部包管理器的可用性。</li>
</ul>
<div class="blog_h2"><span class="graybg">Dockerfile 分层原则</span></div>
<p>合理的分层可以提高构建缓存命中率：变更频率低的工具放在底层，变更频率高的放在上层。每个 <pre class="crayon-plain-tag">RUN</pre> 指令末尾执行 <pre class="crayon-plain-tag">apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</pre> 减小层体积，安装时使用 <pre class="crayon-plain-tag">--no-install-recommends</pre> 避免拉入不必要的依赖。</p>
<p>以下示例构建了一个包含 Python 3.11、常用系统工具和 NVIDIA CUDA 运行时的开发镜像：</p>
<pre class="crayon-plain-tag">FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive

# Layer 1: 系统工具 + Python 3.11 + 所有 PPA（在切换默认 Python 之前添加）
RUN apt-get update &amp;&amp; \
    apt-get install -y --no-install-recommends \
      software-properties-common gnupg2 wget curl ca-certificates &amp;&amp; \
    add-apt-repository -y ppa:deadsnakes/ppa &amp;&amp; \
    add-apt-repository -y ppa:graphics-drivers/ppa &amp;&amp; \
    wget -qO /tmp/cuda-keyring.deb \
      https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb &amp;&amp; \
    dpkg -i /tmp/cuda-keyring.deb &amp;&amp; rm /tmp/cuda-keyring.deb &amp;&amp; \
    apt-get update &amp;&amp; \
    apt-get install -y --no-install-recommends \
      python3.11 python3.11-venv python3.11-dev python3-pip \
      git make vim jq postgresql-client \
      openssh-server procps iproute2 iputils-ping \
      rsync htop telnet &amp;&amp; \
    update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 &amp;&amp; \
    update-alternatives --install /usr/bin/python  python  /usr/bin/python3.11 1 &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

# Layer 2: NVIDIA 驱动工具（nvidia-smi 等）
RUN apt-get update &amp;&amp; \
    apt-get install -y --no-install-recommends nvidia-utils-580-server &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

# Layer 3: CUDA 运行时库（独立层，便于单独更新）
RUN apt-get update &amp;&amp; \
    apt-get install -y --no-install-recommends cuda-libraries-12-8 &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</pre>
<p>几个关键设计决策：</p>
<ul>
<li>所有 PPA 和 GPG 密钥在 Layer 1 中、<pre class="crayon-plain-tag">update-alternatives</pre> 之前添加。切换默认 Python 后，<pre class="crayon-plain-tag">add-apt-repository</pre> 会因 <pre class="crayon-plain-tag">apt_pkg</pre> 模块绑定系统原生 Python 而报错 <pre class="crayon-plain-tag">No module named 'apt_pkg'</pre>。</li>
<li>NVIDIA 驱动工具和 CUDA 库分别放在独立层中。这样更新驱动版本时只需重建 Layer 2，不影响 Layer 1 的缓存。</li>
<li>安装 <pre class="crayon-plain-tag">nvidia-utils-xxx-server</pre> 而非 <pre class="crayon-plain-tag">nvidia-utils-xxx</pre>。后者在 Ubuntu 仓库中是过渡空壳包，不包含实际的 <pre class="crayon-plain-tag">nvidia-smi</pre> 二进制。</li>
<li>选择 <pre class="crayon-plain-tag">cuda-libraries-12-8</pre>（运行时库，约 1.2 GB）而非 <pre class="crayon-plain-tag">cuda-toolkit-12-8</pre>（完整工具包，约 10 GB）。开发环境通常只需要运行时库来执行 CUDA 程序，不需要编译器和调试器。</li>
</ul>
<div class="blog_h2"><span class="graybg">镜像与 devcontainer.json 的配合</span></div>
<p>当所有工具都已烘焙进镜像后，<pre class="crayon-plain-tag">devcontainer.json</pre> 可以极大简化：</p>
<pre class="crayon-plain-tag">{
  "image": "registry.example.com/dev/ubuntu:22.04-cuda12.8",
  "onCreateCommand": "true",
  "workspaceFolder": "/root"
}</pre>
<p><pre class="crayon-plain-tag">onCreateCommand</pre> 设为 <pre class="crayon-plain-tag">"true"</pre> 表示跳过——因为没有需要在容器首次启动时安装的东西。Pod 创建后立即可用。</p>
<div class="blog_h1"><span class="graybg">Pod规格定制</span></div>
<p>Pod Manifest 是 K8s Provider 的核心配置，控制着 DevPod 无法通过 <pre class="crayon-plain-tag">devcontainer.json</pre> 表达的 K8s 原生能力。</p>
<div class="blog_h2"><span class="graybg">模板变量</span></div>
<p>DevPod 在创建 Pod 前会对 Manifest 进行模板渲染，支持以下占位符：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%;">变量</td>
<td>含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>{{.WorkspaceId}}</td>
<td>Workspace 名称，用作 Pod 名和标签。</td>
</tr>
<tr>
<td>{{.Image}}</td>
<td>devcontainer.json 中声明的镜像地址。</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安全上下文</span></div>
<p>远程开发容器通常需要比生产容器更宽松的权限。常见配置项及其用途：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%;">配置</td>
<td>用途</td>
<td>风险</td>
</tr>
</thead>
<tbody>
<tr>
<td>privileged: true</td>
<td>Docker-in-Docker、设备访问、调试工具</td>
<td>容器可访问宿主内核全部能力</td>
</tr>
<tr>
<td>SYS_ADMIN</td>
<td>mount、cgroup 操作</td>
<td>中等</td>
</tr>
<tr>
<td>SYS_PTRACE</td>
<td>strace、gdb 等调试</td>
<td>低</td>
</tr>
<tr>
<td>NET_ADMIN</td>
<td>网络调试、iptables</td>
<td>中等</td>
</tr>
<tr>
<td>hostNetwork: true</td>
<td>直接使用宿主网络栈，避免 CNI 开销</td>
<td>端口冲突、网络隔离丧失</td>
</tr>
<tr>
<td>hostPID: true</td>
<td>查看宿主进程，便于系统级调试</td>
<td>进程隔离丧失</td>
</tr>
</tbody>
</table>
<p>原则：开发环境按需放宽权限，但仍应限定在专用命名空间和节点上，避免影响生产负载。</p>
<div class="blog_h2"><span class="graybg">资源声明</span></div>
<pre class="crayon-plain-tag">resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "16"
    memory: "64Gi"</pre>
<p><pre class="crayon-plain-tag">requests</pre> 设低一些确保 Pod 能调度成功，<pre class="crayon-plain-tag">limits</pre> 设高一些保留突发空间。开发环境通常不会持续占满资源，但编译、测试时可能有短暂峰值。</p>
<div class="blog_h1"><span class="graybg">文件同步策略</span></div>
<div class="blog_h2"><span class="graybg">DevPod 默认同步 vs 自定义 rsync</span></div>
<p>DevPod 内置了基于 <pre class="crayon-plain-tag">devpod up</pre> 的文件同步机制，对于小型项目效果良好。但在大型多仓库工作区（数十个子项目、上百万文件）下，默认同步存在两个问题：</p>
<ul>
<li>首次同步耗时极长，且无法精细控制排除规则。</li>
<li>DevPod 会尝试上传整个 <pre class="crayon-plain-tag">workspaceFolder</pre> 内容，包括 <pre class="crayon-plain-tag">node_modules</pre>、<pre class="crayon-plain-tag">.git</pre> 等不需要的目录。</li>
</ul>
<p>解决方案是使用 <pre class="crayon-plain-tag">--ide none</pre> 启动 DevPod（跳过 IDE 自动同步），然后用自定义的 rsync 命令精确控制同步内容。</p>
<div class="blog_h2"><span class="graybg">存根目录技巧</span></div>
<p>即使使用 <pre class="crayon-plain-tag">--ide none</pre>，DevPod 仍会在 <pre class="crayon-plain-tag">devpod up</pre> 阶段尝试同步 <pre class="crayon-plain-tag">workspaceFolder</pre> 对应的本地目录。如果该目录很大，首次 up 会非常慢。一个技巧是在 up 之前临时创建一个空的存根目录来替代：</p>
<pre class="crayon-plain-tag">STUB_DIR=$(mktemp -d)
devpod up "$STUB_DIR" --ide none --provider K8s ...
rm -rf "$STUB_DIR"
# 然后用 rsync 同步真正的源码</pre>
<div class="blog_h2"><span class="graybg">rsync 实践</span></div>
<pre class="crayon-plain-tag">SSH_CMD="ssh my-workspace.devpod"

rsync -az \
  --exclude='node_modules' \
  --exclude='.git' \
  --exclude='__pycache__' \
  --exclude='venv' \
  --exclude='.venv' \
  --exclude='dist' \
  --exclude='.next' \
  --exclude='.temp' \
  --exclude='.logs' \
  --exclude='.vscode/sessions.json' \
  --copy-unsafe-links \
  ./my-project/ my-workspace.devpod:/root/Projects/my-project/</pre>
<p>关键参数说明：</p>
<ul>
<li><pre class="crayon-plain-tag">-az</pre>：归档模式 + 压缩传输。不要加 <pre class="crayon-plain-tag">--progress</pre>，大量小文件时进度输出会拖慢 SSH 管道，甚至导致 Broken pipe。</li>
<li><pre class="crayon-plain-tag">--copy-unsafe-links</pre>：将指向同步目录之外的符号链接（Symbolic Link）解引用为实际文件。在多仓库工作区中，项目间的符号链接（如共享 Skills 目录）在远端无法解析，此选项可以自动将其替换为文件副本。</li>
<li><pre class="crayon-plain-tag">--exclude</pre>：排除所有不需要同步的目录。<pre class="crayon-plain-tag">.vscode/sessions.json</pre> 会频繁变更且与远端状态冲突，应排除。</li>
</ul>
<div class="blog_h1"><span class="graybg">IDE 远程连接</span></div>
<p>VS Code 和 Cursor 的远程开发功能通过在容器内安装一个 Server 端（Remote Extension Host）来工作。IDE 通过 SSH 隧道与 Server 通信。</p>
<div class="blog_h2"><span class="graybg">Server 安装机制</span></div>
<p>IDE 的 Server 端与客户端版本严格绑定（通过 commit hash 匹配）。安装流程通常是：</p>
<ol>
<li>从本地客户端获取当前版本的 commit hash。</li>
<li>下载对应版本的 Server 二进制包。</li>
<li>通过 SSH 传输并解压到远端的 <pre class="crayon-plain-tag">~/.cursor-server/cli/servers/Stable-{commit}/</pre>。</li>
</ol>
<p>编排脚本应实现幂等的安装检查：</p>
<pre class="crayon-plain-tag">COMMIT=$(get_ide_commit_hash)
SERVER_BIN="$HOME/.cursor-server/cli/servers/Stable-$COMMIT/server/bin/code-server"

if $SSH_CMD "test -x $SERVER_BIN"; then
  echo "Server already installed"
else
  # 下载并安装 Server
  install_ide_server "$COMMIT"
fi</pre>
<div class="blog_h2"><span class="graybg">扩展持久化</span></div>
<p>IDE 扩展安装在 <pre class="crayon-plain-tag">~/.cursor-server/extensions/</pre>（或 <pre class="crayon-plain-tag">~/.vscode-server/extensions/</pre>）。当 PVC 挂载到 <pre class="crayon-plain-tag">$HOME</pre> 时，扩展天然持久。</p>
<p>一个常见的陷阱是在重新安装 Server 时误删整个 <pre class="crayon-plain-tag">~/.cursor-server</pre> 目录，导致扩展全部丢失。正确做法是<span style="background-color: #c0c0c0;">只清理 Server 二进制目录</span>：</p>
<pre class="crayon-plain-tag"># 错误：会删除扩展
rm -rf ~/.cursor-server

# 正确：只删除 Server 二进制，保留扩展
rm -rf ~/.cursor-server/cli</pre>
<div class="blog_h2"><span class="graybg">扩展批量同步</span></div>
<p>首次设置远端环境时，可以将本地已安装的扩展批量同步到远端，避免逐个从 Marketplace 下载：</p>
<pre class="crayon-plain-tag">rsync -az \
  ~/.cursor-server/extensions/ \
  my-workspace.devpod:~/.cursor-server/extensions/</pre>
<p>同步后需要检查扩展中是否有断裂的符号链接。某些扩展包含指向本地 Node.js 路径的符号链接，在远端无法解析。修复方式是用实际文件替换：</p>
<pre class="crayon-plain-tag"># 在远端查找断裂的符号链接
find ~/.cursor-server/extensions/ -type l ! -exec test -e {} \; -print

# 对每个断裂链接，用目标文件的副本替换
# （需要从本地获取原始文件）</pre>
<div class="blog_h2"><span class="graybg">首次连接缓慢</span></div>
<p>首次通过 IDE 连接远端 Workspace 时，通常需要 30 秒到数分钟。这是因为 IDE 需要：</p>
<ul>
<li>建立 SSH 隧道（DevPod 的 SSH 代理有一定开销）。</li>
<li>下载并安装 Server 端（如果尚未安装）。</li>
<li>初始化所有已安装的扩展。</li>
</ul>
<p>后续连接会快很多，因为 Server 和扩展都已在 PVC 上就绪。</p>
<div class="blog_h1"><span class="graybg">K8s 容器中的 GPU 接入</span></div>
<p>在 K8s 中使用 GPU 需要多个组件协同工作：节点上的驱动、设备插件（Device Plugin）、容器运行时钩子。任何一层配置不当都会导致容器内看不到 GPU 设备。</p>
<div class="blog_h2"><span class="graybg">NVIDIA 设备插件的工作原理</span></div>
<p>NVIDIA 提供的 <span style="background-color: #c0c0c0;">Device Plugin</span> 以 DaemonSet 形式运行在每个 GPU 节点上，向 K8s 注册 <pre class="crayon-plain-tag">nvidia.com/gpu</pre> 扩展资源。Pod 通过在 <pre class="crayon-plain-tag">resources.limits</pre> 中声明 GPU 数量来请求分配：</p>
<pre class="crayon-plain-tag">resources:
  limits:
    nvidia.com/gpu: "4"
  requests:
    nvidia.com/gpu: "4"</pre>
<p>调度器根据 <pre class="crayon-plain-tag">requests</pre> 选择有足够 GPU 的节点，设备插件负责将具体的 GPU 设备（<pre class="crayon-plain-tag">/dev/nvidia0</pre> 等）注入到容器中。</p>
<div class="blog_h2"><span class="graybg">runtimeClassName: nvidia</span></div>
<p>仅声明 GPU 资源不够。K8s 还需要知道使用哪个<span style="background-color: #c0c0c0;">容器运行时</span>来处理 GPU 设备的挂载。这通过 Pod 的 <pre class="crayon-plain-tag">runtimeClassName</pre> 字段指定：</p>
<pre class="crayon-plain-tag">spec:
  runtimeClassName: nvidia
  containers:
    - name: devpod
      # ...</pre>
<p>如果不指定 <pre class="crayon-plain-tag">runtimeClassName</pre>，即使 Pod 获得了 GPU 资源配额，容器运行时也不会调用 NVIDIA 的 prestart hook，导致 <pre class="crayon-plain-tag">/dev/nvidia*</pre> 设备节点不会出现在容器内。这是最常见的 GPU 接入失败原因之一。</p>
<div class="blog_h2"><span class="graybg">AppArmor 拦截</span></div>
<p>一个容易忽略的事实是：<pre class="crayon-plain-tag">privileged: true</pre> 并不等同于 AppArmor <pre class="crayon-plain-tag">unconfined</pre>。在启用了 AppArmor 的节点上，即使容器以特权模式运行，默认的 AppArmor profile（如 <pre class="crayon-plain-tag">cri-containerd.apparmor.d</pre>）仍然可能阻止容器访问 GPU 设备节点。</p>
<p>解决方式是在 Pod 的 <pre class="crayon-plain-tag">metadata.annotations</pre> 中显式声明 AppArmor 为 <pre class="crayon-plain-tag">unconfined</pre>：</p>
<pre class="crayon-plain-tag">metadata:
  annotations:
    container.apparmor.security.beta.K8s.io/devpod: unconfined</pre>
<p>其中 <pre class="crayon-plain-tag">devpod</pre> 是容器名称。该 annotation 需要与容器名精确匹配。</p>
<div class="blog_h2"><span class="graybg">NVIDIA_VISIBLE_DEVICES 陷阱</span></div>
<p>直觉上可能会在 Pod Manifest 中设置环境变量 <pre class="crayon-plain-tag">NVIDIA_VISIBLE_DEVICES=all</pre> 来暴露所有 GPU。然而，当与 <pre class="crayon-plain-tag">runtimeClassName: nvidia</pre> 同时使用时，<span style="background-color: #c0c0c0;">手动设置此变量会干扰设备插件的自动注入逻辑</span>。</p>
<p>NVIDIA Container Runtime 的行为是：</p>
<ul>
<li>如果 <pre class="crayon-plain-tag">NVIDIA_VISIBLE_DEVICES</pre> 由设备插件注入，运行时会根据该值精确挂载对应设备。</li>
<li>如果用户在 Manifest 中手动设置了 <pre class="crayon-plain-tag">NVIDIA_VISIBLE_DEVICES=all</pre>，该值会覆盖设备插件的注入，导致运行时在设备映射阶段产生冲突。</li>
</ul>
<p>正确做法是<span style="background-color: #c0c0c0;">不要手动设置 <pre class="crayon-plain-tag">NVIDIA_VISIBLE_DEVICES</pre></span>，让设备插件自动管理。可以保留 <pre class="crayon-plain-tag">NVIDIA_DRIVER_CAPABILITIES=all</pre> 来开放全部驱动能力（compute、utility、graphics 等）。</p>
<div class="blog_h2"><span class="graybg">容器内的 nvidia-smi</span></div>
<p><pre class="crayon-plain-tag">nvidia-smi</pre> 是验证 GPU 可用性的首选工具。在容器中安装它有一个陷阱：某些 Linux 发行版的官方仓库中，名为 <pre class="crayon-plain-tag">nvidia-utils-xxx</pre> 的包是<span style="background-color: #c0c0c0;">过渡空壳包</span>（transitional dummy package），安装后不包含实际的 <pre class="crayon-plain-tag">nvidia-smi</pre> 二进制文件。</p>
<p>以 Ubuntu 22.04 为例，正确的做法是：</p>
<ol>
<li>添加 <pre class="crayon-plain-tag">ppa:graphics-drivers/ppa</pre>。</li>
<li>安装 <pre class="crayon-plain-tag">nvidia-utils-xxx-server</pre>（注意 <pre class="crayon-plain-tag">-server</pre> 后缀），这个包包含实际的命令行工具。</li>
</ol>
<p>如果不便修改镜像，临时方案是通过 <pre class="crayon-plain-tag">hostPath</pre> 挂载宿主机的驱动库和工具到容器内：</p>
<pre class="crayon-plain-tag">volumeMounts:
  - name: host-root
    mountPath: /host
    readOnly: true
volumes:
  - name: host-root
    hostPath:
      path: /</pre>
<p>然后在容器启动后将 <pre class="crayon-plain-tag">/host/usr/lib/x86_64-linux-gnu</pre> 加入 <pre class="crayon-plain-tag">LD_LIBRARY_PATH</pre>，直接调用 <pre class="crayon-plain-tag">/host/usr/bin/nvidia-smi</pre>。这是临时手段，长期方案应将驱动工具烘焙进镜像。</p>
<div class="blog_h2"><span class="graybg">NVML Unknown Error 排查路径</span></div>
<p>当 <pre class="crayon-plain-tag">nvidia-smi</pre> 报出 <pre class="crayon-plain-tag">Failed to initialize NVML: Unknown Error</pre> 时，按以下顺序排查：</p>
<ol>
<li>AppArmor：检查 Pod annotation 是否设置为 <pre class="crayon-plain-tag">unconfined</pre>。用 <pre class="crayon-plain-tag">cat /proc/1/attr/current</pre> 确认容器实际 profile。</li>
<li>设备节点：检查 <pre class="crayon-plain-tag">ls /dev/nvidia*</pre> 是否存在。如果不存在，问题在运行时或设备插件。</li>
<li>运行时类：确认 Pod spec 中是否设置了 <pre class="crayon-plain-tag">runtimeClassName: nvidia</pre>，以及集群中是否存在对应的 RuntimeClass 资源。</li>
<li>环境变量：检查 <pre class="crayon-plain-tag">NVIDIA_VISIBLE_DEVICES</pre> 是否被手动覆盖。</li>
<li>驱动版本：确认容器内的 NVIDIA 用户态库版本与宿主机内核驱动版本兼容。</li>
</ol>
<div class="blog_h1"><span class="graybg">常见故障排查</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%;">现象</td>
<td style="width: 30%;">原因</td>
<td>解决</td>
</tr>
</thead>
<tbody>
<tr>
<td>Pod 进入 Dead / Failed 状态</td>
<td>OOM、节点问题或配置错误</td>
<td>devpod stop → 修复 Manifest → devpod up。PVC 数据不丢。</td>
</tr>
<tr>
<td>SSH exit code 255</td>
<td>Pod 未就绪或 SSH 隧道中断</td>
<td>检查 Pod 状态，等待 Running 后重试。若 Server 安装中断，手动重执行安装脚本。</td>
</tr>
<tr>
<td>rsync 报 Broken pipe</td>
<td>大量文件的进度输出压垮 SSH 管道</td>
<td>使用 rsync -az，不加 --progress 或 --info=progress2。</td>
</tr>
<tr>
<td>add-apt-repository 报 No module named 'apt_pkg'</td>
<td>默认 Python 被切换，apt_pkg 绑定旧版本</td>
<td>在 update-alternatives 之前完成所有 PPA 添加。</td>
</tr>
<tr>
<td>IDE 扩展在 Pod 重建后丢失</td>
<td>Server 重装脚本误删了 extensions 目录</td>
<td>只清理 cli/ 子目录，保留 extensions/。</td>
</tr>
<tr>
<td>nvidia-smi: command not found</td>
<td>安装了空壳过渡包</td>
<td>从 ppa:graphics-drivers/ppa 安装 nvidia-utils-xxx-server。</td>
</tr>
<tr>
<td>NVML Unknown Error</td>
<td>AppArmor / 运行时类 / 设备注入 / 环境变量</td>
<td>按 AppArmor → 设备节点 → runtimeClassName → 环境变量的顺序逐层排查。</td>
</tr>
<tr>
<td>/dev/nvidia* 不存在</td>
<td>缺少 runtimeClassName: nvidia 或设备插件未运行</td>
<td>确认 RuntimeClass 资源存在且 DaemonSet 正常运行。</td>
</tr>
</tbody>
</table>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/devpod">DevPod 远程开发环境搭建笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/devpod/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>OpenClaw学习笔记</title>
		<link>https://blog.gmem.cc/claw</link>
		<comments>https://blog.gmem.cc/claw#comments</comments>
		<pubDate>Wed, 01 Apr 2026 12:05:22 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=41991</guid>
		<description><![CDATA[<p>四个月，343,000 颗星 2025 年 11 月 24 日，一个名为 OpenClaw 的开源项目在 GitHub 上悄然创建。四个月后，它的 Star 数突破 343,000，成为近年来增速最快的非聚合类开源项目之一，超过了 React、Vue 和 Tailwind CSS 在同等时间段内的增长速度。这条陡峭的增长曲线背后，是一个清晰的价值主张：让每个人都能在自己的设备上运行一个完全属于自己的 AI 助手。 <a class="read-more" href="https://blog.gmem.cc/claw">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/claw">OpenClaw学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">四个月，343,000 颗星</span></div>
<p>2025 年 11 月 24 日，一个名为 <span style="background-color: #c0c0c0;">OpenClaw</span> 的开源项目在 GitHub 上悄然创建。四个月后，它的 Star 数突破 343,000，成为近年来增速最快的非聚合类开源项目之一，超过了 React、Vue 和 Tailwind CSS 在同等时间段内的增长速度。这条陡峭的增长曲线背后，是一个清晰的价值主张：让每个人都能在自己的设备上运行一个完全属于自己的 AI 助手。</p>
<p>OpenClaw 的一句话定位是 <em>"Your own personal AI assistant. Any OS. Any Platform. The lobster way. "</em>。这句话的核心不在于功能描述，而在于哲学立场——<span style="background-color: #c0c0c0;">本地优先（Local-First）</span>不是一个功能选项，而是整个系统的设计前提。在 AI 助手普遍依赖云端服务的 2025 年，这个立场击中了开发者群体中日益强烈的数据主权（Data Sovereignty）诉求。</p>
<p>"The lobster way" 不只是一句口号。龙虾（Lobster）意象贯穿了整个项目：工作流编排工具命名为 Lobster，社区成员自称 lobster，甚至 GitHub 上的口号 "EXFOLIATE! EXFOLIATE!"（蜕皮！蜕皮！）暗示项目的激进重构和快速迭代哲学——龙虾通过周期性蜕壳实现生长，OpenClaw 通过频繁的破坏性变更实现架构进化。</p>
<p>从工程视角看，OpenClaw 的核心技术栈——TypeScript ESM、pnpm monorepo、230 条 Plugin SDK 导出路径、24 个消息渠道接入——构成了一个在单一代码仓库中管理 AI Agent 平台全生命周期的案例研究。无论是 Rust/Go 工具替代传统 JS 工具链（oxfmt 替代 Prettier、oxlint 替代 ESLint、tsdown 替代 webpack）、TypeScript 原生 Go 编译器预览集成、还是 Docker 多阶段构建中对 QEMU 交叉编译的优雅降级处理——这些工程细节比功能特性更能说明项目的技术深度。</p>
<p>项目采用 <span style="background-color: #c0c0c0;">MIT 协议</span>，赞助商包括 OpenAI、NVIDIA、Vercel、Blacksmith 和 Convex——一家云端 AI 公司赞助一个本地优先的开源竞争者，这本身就是值得深思的战略信号。本文基于 OpenClaw 官方仓库（<a href="https://github.com/openclaw/openclaw">github.com/openclaw/openclaw</a>）v2026.4.1 的实际代码，从仓库结构、插件架构、渠道系统、Agent 运行时、内存系统、安全模型到多端原生应用，进行完整的技术剖析。所有代码引用均来自一手数据，不依赖二手社区资料。</p>
<div class="blog_h2"><span class="graybg">四层架构概览</span></div>
<p>在深入代码之前，先建立全局视角。OpenClaw 的整体系统可以压缩为四层：</p>
<ul>
<li><span style="background-color: #c0c0c0;">Gateway（控制平面）</span>：以 WebSocket 服务（默认 ws://127.0.0.1:18789）承载会话管理、配置下发、Cron 调度、Webhook 和健康检查，同时托管 Control UI（Lit 3 + Vite）和 Canvas 宿主（A2UI）。</li>
<li><span style="background-color: #c0c0c0;">Agent / Pi Runtime（智能体运行时）</span>：基于 @mariozechner/pi-agent-core@0.64.0，以 RPC 模式运行，支持工具流（Tool Streaming）和块流（Block Streaming），接入 25+ 模型提供者，并具备 Auth 轮换与 Failover 能力。</li>
<li><span style="background-color: #c0c0c0;">Channels + Skills（渠道与技能层）</span>：覆盖 24 个消息平台，通过 Plugin SDK 的 230 条契约路径与核心交互；ClawHub 市场、before_install 安全钩子，以及 Browser、Canvas、Nodes、Cron、Sessions 等一等工具都位于这一层。</li>
<li><span style="background-color: #c0c0c0;">Memory（记忆层）</span>：由 memory-core 的 13 个子模块组成，以本地 Markdown 文件持久化、sqlite-vec 向量搜索和 LanceDB 为存储后端，承载用户可编辑偏好与长期上下文。</li>
</ul>
<p>Gateway 是唯一的控制平面入口，所有客户端（CLI、Web UI、macOS App、iOS/Android 节点）都通过 WebSocket 连接到 Gateway。Agent 运行时以 RPC 模式挂载在 Gateway 下，接收来自各渠道的消息，调用模型和工具，将结果路由回发送者所在的渠道。Skills 和 Channels 通过 Plugin SDK 的 230 条导出路径与核心交互，Memory 层则为 Agent 提供跨会话的长期记忆能力。</p>
<p>这四层的每一层都有严格的边界约束。Gateway 层通过 src/gateway/protocol/schema.ts 定义类型化 WebSocket 协议，Agent 层通过 Pi 的 RPC 接口暴露能力，Plugin SDK 是扩展与核心之间唯一合法的导入面，Memory 层通过 13 个细粒度子模块避免单体耦合。下文将逐层展开。</p>
<div class="blog_h1"><span class="graybg">项目历史与版本</span></div>
<div class="blog_h2"><span class="graybg">MoltBot → ClawdBot → OpenClaw 的命名演变</span></div>
<p>OpenClaw 并非从零开始。在当前命名之前，它先后经历了 <span style="background-color: #c0c0c0;">MoltBot</span> 和 <span style="background-color: #c0c0c0;">ClawdBot</span> 两个阶段。这段历史在代码库中留有痕迹：package.json 的 scripts 字段至今保留着 "moltbot:rpc" 命令，指向与 "openclaw:rpc" 完全相同的实现。文档域名 docs.molt.bot 也仍以 301 重定向的方式指向当前的 docs.openclaw.ai。</p>
<p>项目由奥地利开发者 <span style="background-color: #c0c0c0;">Peter Steinberger</span>（GitHub: @steipete）主导，其在仓库中拥有 14,756 个提交，远超第二贡献者（1,690 个）。Steinberger 此前以 iOS SDK 生态的贡献著称，转型为 AI Agent 平台开发者，并将高频迭代、激进重构的风格延续到了 OpenClaw 的开发中。</p>
<div class="blog_h2"><span class="graybg">日历版本号与三频道发布策略</span></div>
<p>OpenClaw 采用日历版本（Calendar Versioning）而非语义版本（Semantic Versioning）。版本格式为 vYYYY.M.D（例如 v2026.4.1），直接反映发布日期。当同一天有多个发布时，追加补丁后缀 vYYYY.M.D-N。</p>
<p>发布频道分为三层，通过 npm dist-tag 映射：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">频道</td>
<td style="text-align: center;">npm dist-tag</td>
<td style="text-align: center;">Tag 格式</td>
<td style="text-align: center;">适用场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>stable</td>
<td>latest</td>
<td>vYYYY.M.D</td>
<td>生产环境，默认安装</td>
</tr>
<tr>
<td>beta</td>
<td>beta</td>
<td>vYYYY.M.D-beta.N</td>
<td>预发布验证，macOS App 可能缺席</td>
</tr>
<tr>
<td>dev</td>
<td>dev</td>
<td>main 分支头</td>
<td>开发调试，按需发布</td>
</tr>
</tbody>
</table>
<p>切换频道使用 openclaw update --channel stable|beta|dev。Beta 发布时，npm 版本号必须携带 -beta.N 后缀，不能使用不带后缀的版本号配合 --tag beta 发布，否则版本名会被消耗掉——这是仓库 AGENTS.md 中明确记录的发布守则。</p>
<div class="blog_h2"><span class="graybg">v2026.3.31：一次密集的破坏性变更</span></div>
<p>截至本文写作时，最新的 stable 版本为 v2026.3.31（2026-03-31 发布）。该版本包含 <strong>六项破坏性变更（Breaking Changes）</strong>，密度之高反映了 OpenClaw 激进的迭代风格：</p>
<ol>
<li><strong>Nodes/exec 重构</strong>：移除了 CLI 和 Agent nodes 工具中重复的 nodes.run shell 封装器，所有节点 shell 执行统一走 exec host=node 路径。节点特有能力保留在 nodes invoke 和专用的 media/location/notify 操作上。</li>
<li><strong>Plugin SDK 遗留路径弃用</strong>：弃用旧版 provider 兼容子路径以及旧版 bundled provider setup 和 channel-runtime 兼容垫片（compatibility shims），发出迁移警告。当前文档化的 openclaw/plugin-sdk/* 入口加上本地 api.ts / runtime-api.ts 桶文件是唯一的前进路径。</li>
<li><strong>插件安装安全收紧</strong>：内建的危险代码（dangerous-code）critical 级发现和安装时扫描失败现在<strong>默认拒绝</strong>（fail closed）。此前能成功安装的部分插件现在需要显式指定 --dangerously-force-unsafe-install 标志才能继续。</li>
<li><strong>Gateway 认证收紧</strong>：trusted-proxy 模式拒绝混合共享令牌配置；本地直连回退（local-direct fallback）要求使用配置的令牌，不再隐式认证同主机调用者。</li>
<li><strong>节点命令门控</strong>：节点命令在节点配对获批（node pairing approved）之前保持禁用状态。仅完成设备配对不再足以暴露声明的节点命令。</li>
<li><strong>节点事件信任面缩减</strong>：节点发起的运行（node-originated runs）现在在缩减的受信表面（reduced trusted surface）上执行，依赖更广泛 host/session 工具访问权限的通知驱动或节点触发流程可能需要调整。</li>
</ol>
<p>这种"每个版本都可能有 Breaking Changes"的策略与日历版本号的选择相呼应——既然不提供语义化的兼容性承诺，那就用发布日期明确标识每一次快照。对于使用者，官方建议锁定到特定版本号并在升级前阅读 CHANGELOG。</p>
<div class="blog_h2"><span class="graybg">v2026.3.28：xAI 深度整合与安全强化</span></div>
<p>前一个重要版本 v2026.3.28（2026-03-29 发布）同样包含多项重要变更：</p>
<ul>
<li><strong>xAI 深度整合</strong>：bundled xAI provider 迁移至 Responses API，新增原生 x_search（Grok 网页搜索工具），并在 openclaw onboard 中集成可选的 x_search 配置步骤。</li>
<li><strong>MiniMax 图像生成</strong>：新增 MiniMax image-01 模型的图像生成和图生图编辑能力，支持宽高比控制。</li>
<li><strong>Qwen 认证迁移</strong>：移除已弃用的 qwen-portal-auth OAuth 集成，迁移至 Model Studio API Key 模式。</li>
<li><strong>Plugins/hooks 审批机制</strong>：before_tool_call 钩子新增异步 requireApproval 能力，插件可以暂停工具执行并通过 Telegram 按钮、Discord 交互、/approve 命令等渠道提示用户审批。</li>
<li><strong>Microsoft Teams 升级</strong>：迁移至官方 Teams SDK，支持 1:1 对话的流式回复和 AI 标注。</li>
<li><strong>Gateway OpenAI 兼容性</strong>：新增 /v1/models 和 /v1/embeddings 端点，使 Gateway 可以被 OpenAI 兼容的第三方工具直接调用。</li>
</ul>
<div class="blog_h1"><span class="graybg">仓库结构</span></div>
<div class="blog_h2"><span class="graybg">顶层目录布局</span></div>
<p>OpenClaw 是一个 pnpm workspace monorepo，根目录的核心布局如下：</p>
<pre class="crayon-plain-tag">openclaw/
├── src/           # 核心源码
│   ├── cli/       # CLI 命令入口与进度条
│   ├── commands/  # 各子命令实现
│   ├── gateway/   # Gateway 控制平面（含 protocol/ 子目录）
│   ├── channels/  # 核心渠道实现
│   ├── routing/   # 消息路由
│   ├── plugins/   # 插件发现、加载、注册
│   ├── plugin-sdk/# 公开插件契约（唯一合法导入面）
│   ├── infra/     # 基础设施（SQLite、文件锁等）
│   └── media/     # 媒体处理管道
├── apps/
│   ├── macos/     # SwiftUI + AppKit 菜单栏应用
│   ├── ios/       # Xcode + SwiftUI
│   └── android/   # Kotlin + Gradle
├── extensions/    # 内部扩展（bundled plugin workspace tree）
├── packages/      # 共享包
├── skills/        # 内置 Skills（随 npm 包分发）
├── ui/            # Web Control UI（Lit 3 + Vite）
├── docs/          # Mintlify 文档
├── test/          # E2E 测试
└── scripts/       # 构建/发布/检查脚本（60+个）</pre>
<p>这棵目录树揭示了 OpenClaw 的工程哲学：<span style="background-color: #c0c0c0;">核心尽量薄，边界尽量硬</span>。src/ 存放所有 TypeScript 核心代码，extensions/ 存放内建扩展（bundled plugin workspace tree），apps/ 存放三个原生客户端。三者之间的导入关系是单向的：extensions/ 只能通过 openclaw/plugin-sdk/* 调用核心能力，apps/ 通过 Gateway WebSocket 协议与核心通信。任何反向依赖都会被 CI 的架构守卫脚本拦截。</p>
<div class="blog_h2"><span class="graybg">extensions/ 与 packages/ 的区分</span></div>
<p>extensions/ 是 <span style="background-color: #c0c0c0;">bundled plugin workspace tree</span>——即随 npm 包一起发布的内建扩展。Matrix、Zalo、ZaloUser、Voice Call 等渠道插件以及诊断遥测（diagnostics-otel）都存放在此。每个扩展是一个独立的 pnpm workspace 包，拥有自己的 package.json 和 openclaw.plugin.json 清单文件。扩展的运行时依赖必须声明在自身的 dependencies 中，不能添加到根 package.json（除非核心也使用了同一依赖）。而 workspace:* 在 dependencies 中是被禁止的（因为 npm install 无法解析 workspace 协议），openclaw 本身应放入 devDependencies 或 peerDependencies，运行时通过 jiti 别名解析 openclaw/plugin-sdk。</p>
<p>packages/ 存放的则是纯粹的共享库包，不含插件清单，不走插件加载管线。它们提供跨包复用的工具函数和类型定义。</p>
<div class="blog_h2"><span class="graybg">skills/ 与 docs/ 的角色</span></div>
<p>skills/ 目录存放<span style="background-color: #c0c0c0;">内置 Skills（Bundled Skills）</span>——它们随 npm 包一起分发，安装后即可使用。与 ClawHub 上的第三方 Skill 不同，内置 Skill 不需要 clawhub install，也不走 before_install 安全检查管线。每个 Skill 由一个 SKILL.md 文件描述，该文件在 Agent 运行时被注入到系统提示中。</p>
<p>docs/ 使用 <span style="background-color: #c0c0c0;">Mintlify</span> 框架构建，部署在 docs.openclaw.ai。文档内部链接采用根相对路径（如 [Config](/configuration)），不带 .md 扩展名。文档支持中文翻译，中文版位于 docs/zh-CN/，由 scripts/docs-i18n 脚本自动生成，辅以术语表 docs/.i18n/glossary.zh-CN.json 和翻译记忆 docs/.i18n/zh-CN.tm.jsonl 保证术语一致性。</p>
<div class="blog_h2"><span class="graybg">scripts/ — 60+ 个构建与运维脚本</span></div>
<p>scripts/ 目录包含 60 多个独立脚本文件，加上 package.json 中的 198 个 npm scripts 入口，构成了 OpenClaw 极其精细的构建自动化体系。脚本按用途可分为以下几类：</p>
<ul>
<li><strong>构建脚本</strong>：tsdown-build.mjs（主构建入口）、runtime-postbuild.mjs（构建后处理）、bundle-a2ui.sh（Canvas A2UI 打包）、ui.js（Web UI 构建）</li>
<li><strong>代码检查脚本</strong>：check-extension-plugin-sdk-boundary.mjs（扩展导入边界检查，三种模式）、check-plugin-extension-import-boundary.mjs（核心不得反向导入扩展内部）、check-no-pairing-store-group-auth.mjs（安全认证审计）</li>
<li><strong>发布脚本</strong>：openclaw-npm-release-check.ts（发布前校验）、plugin-npm-release-plan.ts（插件发布计划）、openclaw-npm-postpublish-verify.ts（发布后验证）</li>
<li><strong>平台脚本</strong>：package-mac-app.sh（macOS 打包）、ios-configure-signing.sh（iOS 签名）、build-release-aab.ts（Android AAB 构建）</li>
<li><strong>测试脚本</strong>：test-parallel.mjs（并行测试编排器）、test-live.mjs（真实 API Key 测试）、8 个 e2e/*.sh Docker E2E 测试场景</li>
<li><strong>运维脚本</strong>：committer（原子提交工具，取代手动 git add/commit）、restart-mac.sh（macOS Gateway 重启）、clawlog.sh（macOS 统一日志查询）</li>
</ul>
<div class="blog_h2"><span class="graybg">依赖版本：47 个运行时 + 22 个开发时</span></div>
<p>OpenClaw 的依赖控制极为精简。根 package.json 仅声明 <span style="background-color: #c0c0c0;">47 个运行时依赖</span>和 <span style="background-color: #c0c0c0;">22 个开发时依赖</span>。关键依赖的版本锁定如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">依赖</td>
<td style="text-align: center;">版本</td>
<td style="text-align: center;">用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>@mariozechner/pi-agent-core</td>
<td>0.64.0</td>
<td>Agent 运行时核心</td>
</tr>
<tr>
<td>@agentclientprotocol/sdk</td>
<td>0.17.1</td>
<td>ACP 协议 SDK</td>
</tr>
<tr>
<td>@modelcontextprotocol/sdk</td>
<td>1.29.0</td>
<td>MCP 协议 SDK</td>
</tr>
<tr>
<td>matrix-js-sdk</td>
<td>41.3.0-rc.0</td>
<td>Matrix 渠道</td>
</tr>
<tr>
<td>playwright-core</td>
<td>1.58.2</td>
<td>浏览器控制</td>
</tr>
<tr>
<td>sqlite-vec</td>
<td>0.1.9</td>
<td>向量存储</td>
</tr>
<tr>
<td>sharp</td>
<td>^0.34.5</td>
<td>图像处理</td>
</tr>
<tr>
<td>hono</td>
<td>4.12.9</td>
<td>HTTP 框架</td>
</tr>
<tr>
<td>express</td>
<td>^5.2.1</td>
<td>兼容层</td>
</tr>
<tr>
<td>zod</td>
<td>^4.3.6</td>
<td>运行时校验</td>
</tr>
<tr>
<td>ws</td>
<td>^8.20.0</td>
<td>WebSocket</td>
</tr>
<tr>
<td>undici</td>
<td>^7.24.6</td>
<td>HTTP 客户端</td>
</tr>
</tbody>
</table>
<p>开发依赖中最值得关注的是 @typescript/native-preview@7.0.0-dev.20260331.1——这是 TypeScript 官方的 Go 语言重写版预览，OpenClaw 已将其集成为 pnpm tsgo 命令。vitest@4.1.2 搭配 @vitest/coverage-v8 提供覆盖率检测，tsdown@0.21.7 取代 webpack/rollup 作为打包器，oxfmt@0.43.0 和 oxlint@1.58.0 则分别替代 Prettier 和 ESLint。这套工具链的选型思路清晰：用 Rust/Go 编写的原生工具替换 JavaScript 编写的传统方案，以获得数量级的性能提升。</p>
<p>所有带有 pnpm.patchedDependencies 的依赖必须使用精确版本号（不允许 ^ 或 ~ 前缀），且依赖补丁需要显式审批。此外，仓库明确规定"永远不要更新 Carbon 依赖"——这是一条写入 AGENTS.md 的硬性规则。</p>
<div class="blog_h1"><span class="graybg">核心目录</span></div>
<p>上一章给出了 src/ 的一级目录骨架。本章逐一展开每个子目录的内部设计，以代码结构和依赖关系为主线，解释 OpenClaw 核心源码的工程分层。</p>
<div class="blog_h2"><span class="graybg">src/cli/ — CLI 命令入口与进度渲染</span></div>
<p>src/cli/ 是整个 OpenClaw 命令行工具的入口层。它不包含任何业务逻辑，只负责两件事：解析命令行参数并路由到 src/commands/ 中的具体实现，以及在终端渲染结构化进度反馈。</p>
<p>进度反馈的核心位于 src/cli/progress.ts。该模块同时使用两套机制：</p>
<p>第一套是 <span style="background-color: #c0c0c0;">OSC 进度序列</span>（Operating System Command Progress Sequences）。这是一组终端转义码，允许在支持 ConPTY 的 Windows Terminal、iTerm2 和部分 Linux 终端中直接在标题栏或标签页上显示百分比进度条。progress.ts 通过向 stdout 写入 \x1b]9;4;1;{percent}\x07 序列驱动操作系统级别的进度指示器，这使得即使终端窗口被最小化，用户仍可在任务栏中看到安装进度。</p>
<p>第二套是 <span style="background-color: #c0c0c0;">@clack/prompts</span>，一个轻量级的交互式终端 UI 库。OpenClaw 用它实现 onboard 向导中的步骤指示器、多选菜单和确认提示。@clack/prompts 的 spinner 与 OSC 进度可以并行工作——spinner 渲染在 stdout 的当前行，OSC 序列渲染在终端标题栏，两者互不干扰。</p>
<pre class="crayon-plain-tag">// src/cli/progress.ts 核心逻辑简化示意
import { spinner } from '@clack/prompts';

export function emitOscProgress(percent: number): void {
  process.stdout.write(`\x1b]9;4;1;${Math.round(percent)}\x07`);
}

export function clearOscProgress(): void {
  process.stdout.write(`\x1b]9;4;0;\x07`);
}

export async function withProgress(
  label: string,
  task: (update: (pct: number) =&gt; void) =&gt; Promise
): Promise {
  const s = spinner();
  s.start(label);
  const result = await task((pct) =&gt; {
    emitOscProgress(pct);
    s.message(`${label} (${pct}%)`);
  });
  clearOscProgress();
  s.stop(`${label} ✔`);
  return result;
}</pre>
<div class="blog_h2"><span class="graybg">src/commands/ — 子命令实现与 Onboard 向导</span></div>
<p>src/commands/ 中的每个文件对应一个顶级 CLI 子命令。文件命名遵循 {command}.ts 模式，如 start.ts、stop.ts、update.ts、onboard.ts、config.ts、plugin.ts。</p>
<p>其中最复杂的是 onboard.ts，即首次运行向导。Onboard 向导的执行流程为：检测系统环境（Node.js 版本、平台、包管理器）→ 选择消息渠道（Telegram/Discord/Slack 等）→ 输入渠道凭据（Bot Token 等）→ 选择 AI Provider（OpenAI/Anthropic/Ollama 等）→ 输入 Provider API Key → 写入配置文件 ~/.openclaw/config.yaml → 执行首次 npm install --omit=dev 安装所选渠道的扩展依赖。整个流程由 @clack/prompts 驱动，每个步骤都有 spinner 和进度条反馈。</p>
<div class="blog_h2"><span class="graybg">src/gateway/ — Gateway 控制平面</span></div>
<p>src/gateway/ 是 OpenClaw 的中枢。它在本地启动一个 WebSocket 服务（默认监听 ws://127.0.0.1:18789），充当所有渠道、插件、原生客户端和 Control UI 之间的 <span style="background-color: #c0c0c0;">单一控制平面</span>（Single Control Plane）。</p>
<p>目录结构大致如下：</p>
<pre class="crayon-plain-tag">src/gateway/
├── server.ts          # WebSocket 服务器生命周期
├── router.ts          # 协议消息分发
├── session.ts         # 会话管理
├── presence.ts        # 在线状态
├── config.ts          # 运行时配置 Hot-reload
├── cron.ts            # 定时任务调度
├── webhooks.ts        # 外部 webhook 接入
├── auth.ts            # 认证模型
├── health.ts          # /healthz, /readyz 端点
├── openai-compat.ts   # /v1/models, /v1/embeddings 兼容层
└── protocol/
    ├── schema.ts      # 协议 Schema 聚合入口
    └── schema/        # 按领域拆分的 Schema 定义文件
        ├── sessions.ts
        ├── nodes.ts
        ├── channels.ts
        └── ...</pre>
<p>protocol/ 子目录是 Gateway 的类型层。所有 WebSocket 消息都经由 protocol/schema.ts 聚合导出的 TypeScript 类型定义进行序列化和反序列化。schema/ 内的文件按领域组织（sessions、nodes、channels 等），每个文件导出请求/响应的 Zod Schema 或 TypeScript interface。这套 Schema 同时被用于 Swift codegen——macOS/iOS 原生应用中的 Gateway 客户端代码由构建脚本从这些 TypeScript 类型自动生成对应的 Swift struct。</p>
<p>Session 管理（session.ts）维护所有活跃会话的内存状态，包括会话 ID、关联渠道、关联 Agent、消息队列深度、最后活跃时间等。Presence（presence.ts）跟踪所有已连接客户端的在线状态，支持原生应用和 Web UI 实时显示哪些渠道处于在线。Cron（cron.ts）提供基于 cron 表达式的定时任务调度，用于周期性检查渠道连接状态和执行清理任务。Webhooks（webhooks.ts）为 Telegram webhook 模式和 Slack Events API 等需要 HTTP 回调的渠道提供端点注册。</p>
<div class="blog_h2"><span class="graybg">src/channels/ — 核心渠道实现</span></div>
<p>src/channels/ 并非一个单一目录——OpenClaw 将核心渠道的代码分散在 src/ 下的多个一级目录中。具体映射关系为：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>渠道</td>
<td>源码位置</td>
<td>底层依赖</td>
</tr>
</thead>
<tbody>
<tr>
<td>Telegram</td>
<td>src/telegram/</td>
<td>grammY</td>
</tr>
<tr>
<td>Discord</td>
<td>src/discord/</td>
<td>discord.js</td>
</tr>
<tr>
<td>Slack</td>
<td>src/slack/</td>
<td>@slack/bolt</td>
</tr>
<tr>
<td>Signal</td>
<td>src/signal/</td>
<td>signal-cli (Java 子进程)</td>
</tr>
<tr>
<td>iMessage</td>
<td>src/imessage/</td>
<td>BlueBubbles HTTP API / 原生 imsg</td>
</tr>
<tr>
<td>WhatsApp</td>
<td>src/web/</td>
<td>Baileys (WhatsApp Web 协议)</td>
</tr>
</tbody>
</table>
<p>src/channels/ 本身作为聚合层存在，定义了所有渠道必须实现的 <span style="background-color: #c0c0c0;">统一消息抽象</span>接口和路由表。每个渠道目录内部的文件结构大致对称：一个 adapter 文件负责将平台 SDK 的事件映射为统一的 inbound 消息格式，一个 sender 文件负责将统一的 outbound 格式转换回平台特定的 API 调用。</p>
<div class="blog_h2"><span class="graybg">src/routing/ — 消息路由引擎</span></div>
<p>消息路由引擎（src/routing/）是渠道系统和 Agent 运行时之间的中间层。它根据配置文件中的路由规则，将 inbound 消息分发到正确的 Agent 实例。路由维度包括：渠道类型、账号 ID、发送者 peer ID、群组 ID、消息内容匹配模式。多 Agent 场景下，路由引擎负责将不同渠道/账号/群组的消息隔离到不同 Agent 的会话中。</p>
<div class="blog_h2"><span class="graybg">src/plugins/ — 插件发现、加载与注册</span></div>
<p>src/plugins/ 是插件系统的运行时宿主，不是插件本身。它包含四个核心模块：</p>
<p><strong>Discovery</strong>：扫描 extensions/ workspace 和 ~/.openclaw/plugins/ 中已安装的 npm 包，查找包含 openclaw.plugin.json 清单文件的包。</p>
<p><strong>Manifest Validation</strong>：用 Zod Schema 严格校验 openclaw.plugin.json 的结构。清单文件中的 id、channel.id、install.npmSpec 等字段必须符合预定义的格式。</p>
<p><strong>Loader</strong>：对通过校验的插件执行动态 import()，加载其入口模块并调用约定的生命周期钩子。</p>
<p><strong>Registry</strong>：维护全局插件注册表，记录每个已加载插件的类型、状态、能力声明。Registry 支持运行时热插拔——新安装的插件可以在不重启 Gateway 的情况下被 discovery → validate → load → register。</p>
<p><strong>Contract Enforcement</strong>：通过 ESLint 规则在构建时确保插件只通过 openclaw/plugin-sdk/* 导入公开 API。任何直接引用 src/ 内部模块的插件都会在 CI 中被拦截。</p>
<div class="blog_h2"><span class="graybg">src/plugin-sdk/ — 唯一合法的插件导入面</span></div>
<p>src/plugin-sdk/ 是 OpenClaw 面向所有外部扩展的 <span style="background-color: #c0c0c0;">唯一公开 API 面</span>。package.json 的 exports 字段精确地声明了 230 条命名导出子路径，每一条都是一个稳定的契约。这 230 条子路径是插件开发的全部合法导入来源——没有例外。该目录的详细分析见下一章。</p>
<div class="blog_h2"><span class="graybg">src/infra/ — 基础设施层</span></div>
<p>src/infra/ 封装与操作系统交互的底层能力。核心组件包括：基于 <span style="background-color: #c0c0c0;">better-sqlite3</span> 的本地持久化层（存储会话历史、插件状态、用户配置等），以及基于 proper-lockfile 的文件锁机制——确保同一台机器上不会出现两个 OpenClaw Gateway 实例同时操作同一个数据目录。SQLite 数据库文件默认位于 ~/.openclaw/data/openclaw.db。</p>
<div class="blog_h2"><span class="graybg">src/media/ — 媒体处理管道</span></div>
<p>src/media/ 实现了统一的媒体处理管道（Media Processing Pipeline）。当渠道接收到图片、音频、视频或文件消息时，该管道负责：下载原始媒体 → 格式检测 → 必要时进行转码（如 Opus → WAV 用于语音转文字）→ 存储到本地缓存 → 生成引用 URL 供 Agent 使用。管道设计为可插拔的，media plugin 可以注册自定义的 processor 处理特定 MIME 类型。</p>
<div class="blog_h1"><span class="graybg">Plugin SDK</span></div>
<p>OpenClaw 的插件系统以 src/plugin-sdk/ 为核心，通过 package.json 的 exports 字段向外部暴露了 230 条精确的命名子路径。这是一个经过严格设计的 <span style="background-color: #c0c0c0;">契约体系</span>（Contract System）——它同时定义了插件能做什么和不能做什么。</p>
<div class="blog_h2"><span class="graybg">230 条导出子路径的分类与结构</span></div>
<p>package.json 的 exports 字段格式如下：</p>
<pre class="crayon-plain-tag">{
  "exports": {
    "./plugin-sdk/channel-types": "./src/plugin-sdk/channel-types.ts",
    "./plugin-sdk/channel-inbound": "./src/plugin-sdk/channel-inbound.ts",
    "./plugin-sdk/channel-reply-pipeline": "./src/plugin-sdk/channel-reply-pipeline.ts",
    "./plugin-sdk/channel-send-result": "./src/plugin-sdk/channel-send-result.ts",
    "./plugin-sdk/channel-dm-security": "./src/plugin-sdk/channel-dm-security.ts",
    "./plugin-sdk/provider-types": "./src/plugin-sdk/provider-types.ts",
    "./plugin-sdk/provider-registry": "./src/plugin-sdk/provider-registry.ts",
    "./plugin-sdk/memory-core-types": "./src/plugin-sdk/memory-core-types.ts",
    "./plugin-sdk/memory-core-store": "./src/plugin-sdk/memory-core-store.ts",
    "./plugin-sdk/plugin-manifest": "./src/plugin-sdk/plugin-manifest.ts",
    "./plugin-sdk/plugin-lifecycle": "./src/plugin-sdk/plugin-lifecycle.ts",
    "./plugin-sdk/runtime-config": "./src/plugin-sdk/runtime-config.ts",
    "./plugin-sdk/runtime-events": "./src/plugin-sdk/runtime-events.ts",
    "./plugin-sdk/media-types": "./src/plugin-sdk/media-types.ts",
    "./plugin-sdk/media-processor": "./src/plugin-sdk/media-processor.ts",
    "./plugin-sdk/speech-types": "./src/plugin-sdk/speech-types.ts",
    "./plugin-sdk/speech-engine": "./src/plugin-sdk/speech-engine.ts"
    // ... 共 230 条
  }
}</pre>
<p>这 230 条子路径按前缀可划分为以下类别：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>前缀</td>
<td>数量（约）</td>
<td>职责</td>
</tr>
</thead>
<tbody>
<tr>
<td>channel-*</td>
<td>~45</td>
<td>渠道类型定义、inbound/outbound 消息、DM 安全策略、群组行为、分块策略</td>
</tr>
<tr>
<td>provider-*</td>
<td>~35</td>
<td>AI Provider 接口、模型注册、能力声明、流式响应协议</td>
</tr>
<tr>
<td>memory-core-*</td>
<td>~20</td>
<td>内存系统核心类型、存储接口、向量索引</td>
</tr>
<tr>
<td>plugin-*</td>
<td>~25</td>
<td>插件清单格式、生命周期钩子、能力声明</td>
</tr>
<tr>
<td>runtime-*</td>
<td>~40</td>
<td>运行时配置、事件总线、日志、错误类型、会话上下文</td>
</tr>
<tr>
<td>media-*</td>
<td>~15</td>
<td>媒体类型、处理器接口、转码管道</td>
</tr>
<tr>
<td>speech-*</td>
<td>~10</td>
<td>语音识别/合成引擎接口</td>
</tr>
<tr>
<td>其他 (tool-*, skill-*, util-* 等)</td>
<td>~40</td>
<td>工具/技能插件接口、通用工具类型</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">导入边界规则：唯一合法导入面</span></div>
<p>OpenClaw 的核心架构约束是：所有外部扩展（extensions/ workspace 中的包以及第三方 npm 包）<strong>只能从 openclaw/plugin-sdk/* 导入</strong>。不允许直接引用 src/ 内部模块，不允许使用相对路径跨越包边界，不允许引用未在 exports 中声明的路径。</p>
<p>这条规则通过四条自定义 ESLint 规则在 CI 中强制执行：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>Lint 规则</td>
<td>作用</td>
</tr>
</thead>
<tbody>
<tr>
<td>lint:extensions:no-plugin-sdk-internal</td>
<td>禁止 extensions/ 中的代码导入 plugin-sdk 的内部实现文件（非 exports 声明路径）</td>
</tr>
<tr>
<td>lint:extensions:no-relative-outside-package</td>
<td>禁止 extensions/ 中的代码使用相对路径引用包外部的文件</td>
</tr>
<tr>
<td>lint:extensions:no-src-outside-plugin-sdk</td>
<td>禁止 extensions/ 中的代码直接引用 src/ 下非 plugin-sdk 的任何模块</td>
</tr>
<tr>
<td>lint:plugins:no-extension-imports</td>
<td>禁止 src/ 核心代码反向引用 extensions/ 中的模块（防止反向依赖）</td>
</tr>
</tbody>
</table>
<p>这四条规则共同构成了一个严格的 <span style="background-color: #c0c0c0;">依赖防火墙</span>：核心代码和扩展代码之间的边界是单向的、受控的、可审计的。</p>
<div class="blog_h2"><span class="graybg">五种插件类型</span></div>
<p>OpenClaw 定义了五种插件类型，每种对应 plugin-sdk 中的一组子路径：</p>
<p><strong>Channel Plugin（渠道插件）</strong>：实现一个新的消息平台适配器。必须提供 channel-inbound 和 channel-reply-pipeline 的完整实现。清单文件中必须声明 channel.id。</p>
<p><strong>Provider Plugin（Provider 插件）</strong>：接入一个新的 AI 模型提供商。需实现 provider-types 中定义的接口，包括模型列举、Chat Completion 流、Embedding 等。</p>
<p><strong>Tool Plugin（工具插件）</strong>：为 Agent 添加新的可调用工具。通过 tool-* 子路径注册工具定义，包括 JSON Schema 参数描述和执行函数。</p>
<p><strong>Skill Plugin（技能插件）</strong>：预打包的复合能力（如 "搜索网页并总结"），可包含多个 tool 的编排逻辑。</p>
<p><strong>Media Plugin（媒体插件）</strong>：注册自定义的媒体处理器，处理特定 MIME 类型的文件。</p>
<div class="blog_h2"><span class="graybg">插件清单：openclaw.plugin.json</span></div>
<p>每个插件的元数据由包根目录下的 openclaw.plugin.json 声明：</p>
<pre class="crayon-plain-tag">{
  "id": "openclaw-channel-matrix",
  "version": "2026.4.1",
  "type": "channel",
  "channel": {
    "id": "matrix",
    "displayName": "Matrix",
    "supportsGroups": true,
    "supportsDM": true
  },
  "install": {
    "npmSpec": "@openclaw/channel-matrix@latest"
  },
  "minCoreVersion": "2026.3.1",
  "entrypoint": "./dist/index.js"
}</pre>
<p>关键字段：id 是全局唯一标识符；channel.id 在 type 为 channel 时必须提供，用于路由表匹配；install.npmSpec 指定安装时使用的 npm 包标识符；minCoreVersion 声明兼容的最低 OpenClaw 核心版本。</p>
<div class="blog_h2"><span class="graybg">本地桶文件：api.ts 与 runtime-api.ts</span></div>
<p>src/plugin-sdk/ 内部有两个重要的 <span style="background-color: #c0c0c0;">桶文件</span>（Barrel File）：api.ts 和 runtime-api.ts。</p>
<p>api.ts 聚合所有纯类型导出——接口定义、类型别名、枚举等。它是编译时依赖，不包含任何运行时代码。runtime-api.ts 聚合需要运行时实现的模块——工厂函数、注册器、事件发射器等。两者的分离确保了：如果插件只需要类型信息（如纯粹的 TypeScript 类型守卫），可以仅依赖 api.ts，不会引入任何运行时代码，保持 tree-shaking 友好。</p>
<div class="blog_h2"><span class="graybg">插件安装与依赖约束</span></div>
<p>插件安装通过 npm install --omit=dev 执行，只安装生产依赖。关键约束：插件的 package.json 中禁止使用 workspace:* 协议作为 dependencies——这是因为第三方插件安装在用户机器上时不处于 monorepo workspace 上下文中，workspace:* 会解析失败。CI 中有专门的检查脚本拦截此类违规。</p>
<div class="blog_h2"><span class="graybg">遗留 Provider 兼容子路径的弃用</span></div>
<p>v2026.3.31 是一个 <span style="background-color: #c0c0c0;">Breaking Change</span> 版本。此前，plugin-sdk 中保留了一组以 provider-compat-* 为前缀的遗留子路径，用于向后兼容早期 Provider 接口。v2026.3.31 正式移除了这些路径。依赖旧接口的第三方 Provider 插件必须迁移到新的 provider-* 子路径。迁移指南位于 docs/migration/v2026.3.31-provider-compat.md。</p>
<div class="blog_h1"><span class="graybg">Gateway 架构</span></div>
<p>Gateway 是 OpenClaw 的核心运行时进程。它不是一个可选组件——所有渠道消息、Agent 调度、插件通信、原生客户端交互都经由 Gateway 路由。理解 Gateway 就是理解 OpenClaw 的运行时全貌。</p>
<div class="blog_h2"><span class="graybg">单一本地控制平面</span></div>
<p>Gateway 的设计哲学是 <span style="background-color: #c0c0c0;">Single Local Control Plane</span>——本地机器上只有一个 Gateway 实例运行，它是所有组件的通信枢纽。启动命令 openclaw start 实际上就是启动 Gateway 进程。Gateway 在 ws://127.0.0.1:18789（默认端口）上监听 WebSocket 连接，同时在同一端口提供 HTTP 端点。</p>
<p>所有组件都是 Gateway 的客户端：渠道适配器（Telegram bot、Discord bot 等）在内部通过 WebSocket 向 Gateway 报告 inbound 消息；Agent 运行时从 Gateway 接收任务并返回响应；原生应用（macOS、iOS、Android）通过 WebSocket 连接 Gateway 获取实时状态；Control UI（Web 界面）同样是一个 WebSocket 客户端。</p>
<div class="blog_h2"><span class="graybg">类型化协议：protocol/schema</span></div>
<p>Gateway 的 WebSocket 协议是完全类型化的。协议定义位于 src/gateway/protocol/schema.ts，它从 src/gateway/protocol/schema/ 目录中聚合导出所有子模块。每个子模块对应一个协议领域：</p>
<pre class="crayon-plain-tag">// src/gateway/protocol/schema/sessions.ts
import { z } from 'zod';

export const SessionPatchRequest = z.object({
  method: z.literal('sessions.patch'),
  params: z.object({
    sessionId: z.string(),
    patch: z.object({
      thinkingLevel: z.enum(['off','minimal','low','medium','high','xhigh']).optional(),
      activeAgent: z.string().optional(),
      queueMode: z.enum(['sequential','parallel']).optional(),
    }),
  }),
});

export const SessionPatchResponse = z.object({
  result: z.object({
    sessionId: z.string(),
    applied: z.record(z.unknown()),
  }),
});

// src/gateway/protocol/schema/nodes.ts
export const NodeListRequest = z.object({
  method: z.literal('node.list'),
  params: z.object({
    filter: z.object({
      type: z.enum(['channel','agent','plugin','tool']).optional(),
      status: z.enum(['online','offline','error']).optional(),
    }).optional(),
  }),
});

export const NodeDescribeRequest = z.object({
  method: z.literal('node.describe'),
  params: z.object({ nodeId: z.string() }),
});

export const NodeInvokeRequest = z.object({
  method: z.literal('node.invoke'),
  params: z.object({
    nodeId: z.string(),
    action: z.string(),
    payload: z.unknown(),
  }),
});</pre>
<p>协议采用类 JSON-RPC 的请求/响应模式。核心方法包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>方法</td>
<td>用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>sessions.patch</td>
<td>修改会话参数（thinking level、活跃 agent、队列模式等）</td>
</tr>
<tr>
<td>sessions.list</td>
<td>列举所有活跃会话及其状态</td>
</tr>
<tr>
<td>node.list</td>
<td>列举所有已注册节点（渠道、Agent、插件、工具）</td>
</tr>
<tr>
<td>node.describe</td>
<td>获取指定节点的详细信息和能力声明</td>
</tr>
<tr>
<td>node.invoke</td>
<td>向指定节点发送操作指令（如要求渠道发送消息、要求 Agent 执行任务）</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Swift Codegen</span></div>
<p>macOS/iOS 原生应用需要与 Gateway 通信。为保证 TypeScript 协议定义与 Swift 客户端代码的一致性，OpenClaw 在构建流程中包含一个 <span style="background-color: #c0c0c0;">Swift codegen</span> 步骤。构建脚本解析 src/gateway/protocol/schema/ 中的 Zod Schema，自动生成对应的 Swift Codable struct 和 enum。生成的代码位于 apps/macos/Generated/ 和 apps/ios/Generated/。这意味着协议变更只需修改 TypeScript Schema，Swift 侧自动同步，不存在手动同步遗漏的风险。</p>
<div class="blog_h2"><span class="graybg">认证模型</span></div>
<p>Gateway 支持三种认证模式，按优先级排列：</p>
<p><strong>trusted-proxy</strong>：Gateway 信任来自特定代理（如 Nginx、Cloudflare Tunnel）的请求，依据代理注入的 HTTP 头进行身份识别。这是生产环境推荐模式。</p>
<p><strong>local-direct</strong>：当 WebSocket 连接来自 127.0.0.1 时，跳过认证直接授权。这是本地开发和单机部署的默认行为。</p>
<p><strong>gateway token</strong>：通过配置文件设置的静态 Token，客户端在 WebSocket 握手时通过 Authorization: Bearer 头携带。用于远程访问场景。</p>
<p>v2026.3.31 引入了一个重要的安全变更：<span style="background-color: #c0c0c0;">trusted-proxy 模式下，如果检测到多个客户端使用同一个 shared-token，Gateway 将拒绝连接</span>。此前这种 "多人共享一个 token" 的配置虽然不推荐但能工作，新版本将其升级为硬性错误。这是因为 shared-token 场景下无法区分不同用户的会话，会导致消息路由混乱。</p>
<div class="blog_h2"><span class="graybg">OpenAI 兼容端点与健康检查</span></div>
<p>Gateway 在 HTTP 层暴露了一组 <span style="background-color: #c0c0c0;">OpenAI 兼容端点</span>：</p>
<p>/v1/models：返回当前配置的所有可用模型列表，格式兼容 OpenAI List Models API。这使得任何兼容 OpenAI API 的客户端（如 Cursor、Continue 等）可以直接将 OpenClaw Gateway 作为模型提供方。</p>
<p>/v1/embeddings：提供文本向量化接口，格式兼容 OpenAI Embeddings API。后端可路由到实际配置的 Embedding Provider（OpenAI、Ollama 本地模型等）。</p>
<p>健康检查端点遵循 Kubernetes 惯例：</p>
<p>/healthz：存活探针（Liveness Probe），只要 Gateway 进程在运行就返回 200。</p>
<p>/readyz：就绪探针（Readiness Probe），只有当至少一个渠道连接成功且 Agent 运行时已初始化时才返回 200。适用于负载均衡器判断节点是否可以接收流量。</p>
<div class="blog_h2"><span class="graybg">Control UI 与 Bridge Protocol</span></div>
<p>Gateway 直接serve一个 Web 管理界面——<span style="background-color: #c0c0c0;">Control UI</span>。该 UI 使用 <span style="background-color: #c0c0c0;">Lit 3</span>（Web Components 框架）+ <span style="background-color: #c0c0c0;">Vite</span>（构建工具）开发，源码位于 ui/ 目录。构建产物在发布时嵌入 Gateway 的静态资源中，通过 HTTP 直接访问（默认 http://127.0.0.1:18789）。Control UI 本身也是一个 WebSocket 客户端，与 Gateway 保持长连接以实现实时状态更新。</p>
<p>Bridge Protocol（桥接协议）的规范文档位于 docs/gateway/bridge-protocol.md，它定义了原生应用与 Gateway 之间的通信约定——包括消息编码格式、心跳机制、重连策略、事件订阅模型。这份文档是原生应用开发者的核心参考。</p>
<div class="blog_h1"><span class="graybg">渠道系统</span></div>
<p>OpenClaw 在 v2026.4.1 中支持 24 个消息渠道。渠道系统的核心工程挑战在于：如何将 24 个各具特色、API 风格迥异的消息平台，抽象为一套统一的 inbound/outbound 消息模型，同时保留每个平台的独特能力。</p>
<div class="blog_h2"><span class="graybg">24 渠道全景</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>渠道</td>
<td>底层实现</td>
<td>类型</td>
</tr>
</thead>
<tbody>
<tr>
<td>WhatsApp</td>
<td>Baileys (WhatsApp Web 逆向协议)</td>
<td>核心渠道 (src/web/)</td>
</tr>
<tr>
<td>Telegram</td>
<td>grammY</td>
<td>核心渠道 (src/telegram/)</td>
</tr>
<tr>
<td>Slack</td>
<td>@slack/bolt</td>
<td>核心渠道 (src/slack/)</td>
</tr>
<tr>
<td>Discord</td>
<td>discord.js</td>
<td>核心渠道 (src/discord/)</td>
</tr>
<tr>
<td>Signal</td>
<td>signal-cli (Java 子进程)</td>
<td>核心渠道 (src/signal/)</td>
</tr>
<tr>
<td>BlueBubbles (iMessage)</td>
<td>BlueBubbles HTTP API</td>
<td>核心渠道 (src/imessage/)，推荐方式</td>
</tr>
<tr>
<td>iMessage (legacy imsg)</td>
<td>原生 AppleScript/osascript</td>
<td>核心渠道，已标记为遗留</td>
</tr>
<tr>
<td>Google Chat</td>
<td>Google Chat API</td>
<td>内置扩展</td>
</tr>
<tr>
<td>IRC</td>
<td>irc-framework</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Microsoft Teams</td>
<td>Teams SDK (v2026.3.28 升级后版本)</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Matrix</td>
<td>matrix-js-sdk + @matrix-org/crypto-wasm</td>
<td>内置扩展 (extensions/)</td>
</tr>
<tr>
<td>Feishu (飞书)</td>
<td>Feishu Open API</td>
<td>内置扩展</td>
</tr>
<tr>
<td>LINE</td>
<td>@line/bot-sdk</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Mattermost</td>
<td>Mattermost REST API + WebSocket</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Nextcloud Talk</td>
<td>Nextcloud Talk API</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Nostr</td>
<td>nostr-tools</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Synology Chat</td>
<td>Synology Chat Webhook</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Tlon</td>
<td>Tlon API</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Twitch</td>
<td>tmi.js</td>
<td>内置扩展</td>
</tr>
<tr>
<td>Zalo</td>
<td>Zalo Official Account API</td>
<td>内置扩展 (extensions/)</td>
</tr>
<tr>
<td>Zalo Personal</td>
<td>Zalo Personal API (ZaloUser)</td>
<td>内置扩展 (extensions/)</td>
</tr>
<tr>
<td>Voice Call</td>
<td>VoIP/SIP 集成</td>
<td>内置扩展 (extensions/)</td>
</tr>
<tr>
<td>WeChat (微信)</td>
<td>@tencent-weixin/openclaw-weixin (iLink Bot API)</td>
<td>官方合作插件</td>
</tr>
<tr>
<td>WebChat</td>
<td>Gateway 内置 WebSocket 聊天</td>
<td>核心渠道</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">渠道契约类型</span></div>
<p>渠道系统的类型契约由三个核心文件定义：</p>
<p>types.plugin.ts：面向插件开发者的公开类型。渠道插件必须实现的接口在此定义，包括 ChannelAdapter（渠道适配器）、ChannelSender（消息发送器）、ChannelConfig（渠道配置 Schema）。</p>
<p>types.core.ts：核心内部类型，不通过 plugin-sdk 导出。包含路由表条目、会话绑定关系、内部消息信封（Envelope）格式。</p>
<p>types.adapters.ts：适配器辅助类型，定义各平台 SDK 事件到统一 inbound 格式的映射接口。</p>
<div class="blog_h2"><span class="graybg">统一消息抽象</span></div>
<p>统一消息抽象是渠道系统的核心设计。它由三个 plugin-sdk 子路径定义：</p>
<p>channel-inbound：定义所有渠道 inbound 消息的统一结构。无论消息来自 WhatsApp、Telegram 还是 Discord，经过渠道适配器处理后都被转换为相同的 InboundMessage 类型。该类型包含：channelId、peerId（发送者标识）、groupId（群组标识，DM 时为 null）、content（文本/媒体/混合内容）、replyTo（引用消息 ID）、timestamp、rawEvent（平台原始事件，用于渠道特定逻辑）。</p>
<p>channel-reply-pipeline：定义 Agent 响应经过的处理管道。管道阶段包括：内容格式化（Markdown → 平台特定格式）→ 长消息分块（per-channel chunking）→ 媒体附件处理 → 平台 API 调用。</p>
<p>channel-send-result：定义消息发送结果的统一结构，包括平台返回的消息 ID、发送状态（成功/失败/部分成功）、错误信息。</p>
<div class="blog_h2"><span class="graybg">群组路由：@提及门控与回复标签</span></div>
<p>在群组场景中，Agent 默认不响应所有消息——这会导致群组中的噪声。OpenClaw 实现了 <span style="background-color: #c0c0c0;">@提及门控</span>（Mention Gating）：只有当消息中包含对 Bot 的 @mention 时，Agent 才会处理该消息。这一行为可通过配置按渠道/群组覆盖为 always 模式（响应所有消息）。</p>
<p>回复标签（Reply Tags）解决了另一个群组问题：当多条消息同时进入时，Agent 的回复需要标记其对应的原始消息。在 Telegram 中这通过 reply_to_message_id 实现，在 Discord 中通过 Message Reference 实现，在 Slack 中通过 Thread TS 实现。渠道适配器负责将统一的 replyTo 字段映射为平台特定的回复机制。</p>
<p>长消息分块（Per-channel Chunking）是另一个平台差异处理点。Telegram 单条消息上限 4096 字符，Discord 为 2000 字符，WhatsApp 约 65536 字符。channel-reply-pipeline 中的分块阶段根据目标渠道的限制，将超长 Agent 响应拆分为多条消息，同时确保代码块、Markdown 列表等结构不被截断在中间。</p>
<div class="blog_h2"><span class="graybg">DM 安全策略</span></div>
<p>私聊（DM）场景有独立的安全模型，由 channel-dm-security 子路径定义。核心是 dmPolicy 配置项，支持三种模式：</p>
<p>pairing：用户必须先发送配对码（Pairing Code）才能激活 DM 对话。配对码由 openclaw pair 命令生成，一次性使用。这是最安全的模式。</p>
<p>allowlist：只有 allowFrom 配置中列出的用户 ID/手机号才能发起 DM 对话。</p>
<p>open：任何人都可以直接发起 DM 对话。仅建议在可控环境（如内网部署）中使用。</p>
<div class="blog_h2"><span class="graybg">渠道特定亮点</span></div>
<p><strong>WhatsApp</strong>：基于 Baileys 库，使用 WhatsApp Web 协议。首次连接需要扫描 QR 码完成登录，QR 码在终端中以 ASCII art 形式渲染，同时在 Control UI 中以图片形式展示。会话凭证持久化到本地文件系统，后续启动自动恢复。</p>
<p><strong>Telegram</strong>：支持两种运行模式——长轮询（Long Polling，默认）和 <span style="background-color: #c0c0c0;">Webhook 模式</span>。Webhook 模式下，Gateway 注册一个公开 HTTPS 端点接收 Telegram 推送，延迟更低但需要公网可达的地址（通常通过 Cloudflare Tunnel 或 ngrok 实现）。grammY 框架提供了完整的 Bot API 类型安全封装。</p>
<p><strong>Discord</strong>：支持原生 Slash Commands（/ask、/image 等）和纯文本命令两种交互模式。discord.js 提供了丰富的事件模型，OpenClaw 利用其 Message Component 能力实现了交互式按钮和选择菜单。</p>
<p><strong>Microsoft Teams</strong>：v2026.3.28 版本对 Teams 集成进行了重大升级，迁移到新版 Teams SDK。新版本支持流式回复（Streaming Replies），Agent 的响应可以实时逐字显示在 Teams 对话中，并带有 AI 注释标签（AI Annotation），让用户明确知道当前回复来自 AI。</p>
<p><strong>WeChat（微信）</strong>：通过官方合作渠道实现，使用 @tencent-weixin/openclaw-weixin 包，底层接入 iLink Bot API。当前仅支持私聊消息，不支持群聊。v2.x 版本要求 OpenClaw 核心版本 ≥ 2026.3.22。</p>
<div class="blog_h1"><span class="graybg">Agent 运行时</span></div>
<p>OpenClaw 的 Agent 运行时构建在 <span style="background-color: #c0c0c0;">Pi Agent</span> 之上。这不是一个自研的 Agent 框架，而是对外部库 @mariozechner/pi-agent-core@0.64.0 和 @mariozechner/pi-ai@0.64.0 的深度集成。Pi 生态还包括 pi-coding-agent（代码生成专用 Agent）和 pi-tui（终端 UI）。</p>
<div class="blog_h2"><span class="graybg">RPC 模式：工具流与块流</span></div>
<p>Pi Agent 以 <span style="background-color: #c0c0c0;">RPC 模式</span>运行时，支持两种流式输出协议：</p>
<p><strong>Tool Streaming（工具流）</strong>：Agent 调用工具时，工具的执行过程和中间结果以流式方式返回。例如，Agent 调用搜索工具时，搜索结果的每一条都作为一个流事件推送，而不是等待所有结果返回后才一次性输出。</p>
<p><strong>Block Streaming（块流）</strong>：Agent 的文本响应以块为单位流式输出。一个 "块" 可以是一个段落、一个代码块或一个列表。块流比逐 token 流更适合消息渠道场景——渠道适配器可以在每个块完成时立即发送，而不是积累整个响应后发送，也避免了逐 token 发送导致的频繁 API 调用。</p>
<div class="blog_h2"><span class="graybg">Session 模型</span></div>
<p>OpenClaw 的 Session 模型是理解消息路由的关键。每个 Agent 维护多个独立的会话（Session），会话之间完全隔离：</p>
<p><strong>DM Session</strong>：与每个私聊用户的对话构成一个独立会话。会话由 (agentId, channelId, peerId) 三元组唯一标识。</p>
<p><strong>Group Session</strong>：每个群组一个独立会话，由 (agentId, channelId, groupId) 三元组标识。群组会话与 DM 会话完全隔离——Agent 在群组中看不到同一用户的私聊历史，反之亦然。</p>
<p>会话的激活模式（Activation Mode）控制 Agent 何时响应：mention 模式下，只有 @mention 才触发响应；always 模式下，所有消息都触发响应。DM 会话默认为 always，群组会话默认为 mention。</p>
<p>队列模式（Queue Mode）控制并发消息的处理策略：sequential 模式下，消息严格按接收顺序逐条处理；parallel 模式下，多条消息可并行处理（适用于无状态的工具调用场景）。</p>
<p>Reply-back 路由确保 Agent 的响应被发送到正确的渠道和对话。当 Agent 通过工具调用触发了跨渠道操作时（如在 Telegram 对话中要求 Agent 向 Slack 频道发送消息），reply-back 路由负责将操作结果路由回发起请求的 Telegram 对话。</p>
<div class="blog_h2"><span class="graybg">Session 工具：Agent 间协调</span></div>
<p>三个内置工具使 Agent 具备了 <span style="background-color: #c0c0c0;">跨会话/跨 Agent 协调能力</span>：</p>
<pre class="crayon-plain-tag">// sessions_list: 列举当前所有活跃会话
{
  name: 'sessions_list',
  description: 'List all active sessions with their channel, peer, and status',
  parameters: {
    filter: { type: 'object', properties: {
      channelId: { type: 'string' },
      status: { enum: ['active', 'idle', 'archived'] }
    }}
  }
}

// sessions_history: 读取指定会话的历史消息
{
  name: 'sessions_history',
  description: 'Read message history from a specific session',
  parameters: {
    sessionId: { type: 'string' },
    limit: { type: 'number', default: 50 }
  }
}

// sessions_send: 向指定会话发送消息（实现 Agent-to-Agent 通信）
{
  name: 'sessions_send',
  description: 'Send a message to a specific session (enables agent-to-agent coordination)',
  parameters: {
    sessionId: { type: 'string' },
    content: { type: 'string' }
  }
}</pre>
<p>sessions_send 是多 Agent 协调的关键。Agent A 可以通过 sessions_list 发现 Agent B 的会话，通过 sessions_send 向 Agent B 发送指令或查询，Agent B 的响应会通过 reply-back 路由返回 Agent A 的会话上下文。</p>
<div class="blog_h2"><span class="graybg">多 Agent 路由</span></div>
<p>OpenClaw 支持在同一实例中运行多个 Agent，每个 Agent 有独立的配置和会话空间。路由规则在配置文件中定义，支持按渠道、账号、peer 三个维度将 inbound 消息分发到不同 Agent：</p>
<pre class="crayon-plain-tag"># config.yaml 多 Agent 路由示例
agents:
  - id: general-assistant
    provider: openai
    model: gpt-4o
    routes:
      - channel: telegram
        account: "@mybot"
      - channel: discord
        account: "bot-token-1"

  - id: coding-helper
    provider: anthropic
    model: claude-sonnet-4-20250514
    routes:
      - channel: slack
        account: "workspace-1"
        peers: ["U12345678"]  # 仅特定用户的消息路由到此 Agent</pre>
<p>每个 Agent 拥有独立的 workspace 和 session 存储，实现完全隔离。</p>
<div class="blog_h2"><span class="graybg">Agent Workspace 与注入文件</span></div>
<p>每个 Agent 的运行时上下文由 ~/.openclaw/workspace/ 目录提供。该目录下的三个特殊文件会被自动注入到 Agent 的系统提示词中：</p>
<p>AGENTS.md：定义 Agent 的角色、行为准则和约束。这是 Agent 人格的核心定义文件。</p>
<p>SOUL.md：更细粒度的人格描述——语气、对话风格、知识领域偏好等。</p>
<p>TOOLS.md：工具使用指南，告诉 Agent 每个可用工具的使用场景和最佳实践。</p>
<p>这三个文件均为 Markdown 格式，用户可以自由编辑。修改后无需重启——Gateway 在每次会话消息处理前会检查文件的 mtime，如有变更则重新加载。</p>
<div class="blog_h2"><span class="graybg">Session 持久化、修剪与压缩</span></div>
<p>会话历史以 JSONL（JSON Lines）格式持久化到 ~/.openclaw/agents//sessions/*.jsonl。每个会话一个文件，每行一条消息记录。JSONL 格式的选择是经过考量的：它支持 append-only 写入（崩溃安全），支持按行增量读取（内存效率），且可以直接用标准文本工具检查。</p>
<p>长时间运行的会话会积累大量历史，导致 context window 溢出和延迟增加。OpenClaw 提供了两种应对机制：</p>
<p><strong>Session Pruning（会话修剪）</strong>：自动删除超过配置时间窗口（默认 7 天）的旧消息。修剪操作在会话被激活时触发，是懒惰式的。</p>
<p><strong>Session Compaction（会话压缩）</strong>：通过 /compact 命令手动触发。压缩过程调用 AI 模型将长历史总结为精简的上下文摘要，替换原始的逐条消息记录。压缩后的会话文件体积可缩减 80% 以上，同时保留关键上下文信息。</p>
<div class="blog_h2"><span class="graybg">Thinking Levels 与 Idle-stream Timeout</span></div>
<p>OpenClaw 暴露了对 AI 模型 "思考深度" 的精细控制。thinkingLevel 参数支持六个级别：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>级别</td>
<td>行为</td>
</tr>
</thead>
<tbody>
<tr>
<td>off</td>
<td>禁用扩展思考（Extended Thinking），直接生成响应</td>
</tr>
<tr>
<td>minimal</td>
<td>最小思考预算</td>
</tr>
<tr>
<td>low</td>
<td>低思考预算，适合简单任务</td>
</tr>
<tr>
<td>medium</td>
<td>中等思考预算，默认值</td>
</tr>
<tr>
<td>high</td>
<td>高思考预算，适合复杂推理</td>
</tr>
<tr>
<td>xhigh</td>
<td>极高思考预算，用于需要深度推理的场景</td>
</tr>
</tbody>
</table>
<p>Thinking Level 可以在会话级别通过 sessions.patch 协议方法动态调整，也可以在配置文件中设置全局默认值。支持扩展思考的 Provider（如 Anthropic Claude）会根据级别调整思考 token 的预算上限。</p>
<p>v2026.3.31 引入的 <span style="background-color: #c0c0c0;">idle-stream timeout</span> 解决了一个实际运维问题：当模型流（Model Stream）长时间无新 token 输出时（例如模型服务端卡住或网络中断），Agent 会一直等待而不释放会话锁，导致该会话的后续消息全部堆积。idle-stream timeout 允许配置一个超时时间（默认 120 秒），当流在指定时间内无新数据时，Agent 会主动中断流并返回部分响应或错误消息。此超时时间可在配置文件中按 Provider 调整——使用本地 Ollama 模型时可能需要更长的超时。</p>
<div class="blog_h1"><span class="graybg">内存系统</span></div>
<p>AI 助手的个性化能力取决于记忆系统的深度。OpenClaw 的内存子系统 <span style="background-color: #c0c0c0;">memory-core</span> 是整个项目中模块拆分最细致的部分，由 13 个子模块组成，全部通过 plugin-sdk 导出。设计目标明确：所有记忆数据以本地 Markdown 文件和 SQLite 数据库的形式持久化，用户可直接编辑、可 Git 版本控制、可离线运行。</p>
<div class="blog_h2"><span class="graybg">13 个子模块的职责划分</span></div>
<p>plugin-sdk 中与记忆相关的导出共 13 个路径，每个路径对应一个独立的编译单元：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">导出路径</td>
<td style="text-align: center;">职责</td>
</tr>
</thead>
<tbody>
<tr>
<td>memory-core</td>
<td>根模块，定义 MemoryStore 接口、MemoryEntry 类型、TTL 策略和序列化契约</td>
</tr>
<tr>
<td>memory-core-engine-runtime</td>
<td>引擎运行时，将内存操作绑定到当前 Agent 运行时生命周期</td>
</tr>
<tr>
<td>memory-core-host-engine-embeddings</td>
<td>嵌入引擎宿主：调度 Embedding 模型计算向量，管理批量嵌入队列</td>
</tr>
<tr>
<td>memory-core-host-engine-foundation</td>
<td>基础引擎宿主：提供 tokenizer 绑定、向量维度协商、距离度量选择</td>
</tr>
<tr>
<td>memory-core-host-engine-qmd</td>
<td>QMD（Query-Memory-Document）引擎：将用户查询与记忆文档进行语义匹配</td>
</tr>
<tr>
<td>memory-core-host-engine-storage</td>
<td>存储引擎宿主：抽象底层存储后端（SQLite、LanceDB），提供统一 CRUD</td>
</tr>
<tr>
<td>memory-core-host-multimodal</td>
<td>多模态记忆：处理图片、音频等非文本记忆条目的索引与检索</td>
</tr>
<tr>
<td>memory-core-host-query</td>
<td>查询宿主：构建语义搜索查询，合并关键词过滤与向量相似度</td>
</tr>
<tr>
<td>memory-core-host-runtime-cli</td>
<td>CLI 运行时宿主：暴露 openclaw memory search 等终端命令</td>
</tr>
<tr>
<td>memory-core-host-runtime-core</td>
<td>核心运行时宿主：记忆系统的初始化、迁移和生命周期管理</td>
</tr>
<tr>
<td>memory-core-host-runtime-files</td>
<td>文件运行时宿主：监控 Markdown 记忆文件的变更并触发重新索引</td>
</tr>
<tr>
<td>memory-core-host-secret</td>
<td>密钥宿主：管理记忆存储的加密密钥与 SecretRef 解析</td>
</tr>
<tr>
<td>memory-core-host-status</td>
<td>状态宿主：报告索引进度、向量数量、最近查询延迟等运行指标</td>
</tr>
</tbody>
</table>
<p>这种拆分方式遵循 OpenClaw 的插件架构原则：每个子模块可以被独立替换或禁用，核心系统只依赖 memory-core 根模块定义的接口，而不直接依赖任何具体存储后端。</p>
<div class="blog_h2"><span class="graybg">本地 Markdown 文件持久化</span></div>
<p>OpenClaw 的记忆系统将用户偏好和长期上下文存储为本地 Markdown 文件，默认位于 ~/.openclaw/memory/ 目录。每个记忆文件是标准 Markdown，带有 YAML front-matter 元数据：</p>
<pre class="crayon-plain-tag">---
type: preference
created: 2026-03-15T08:22:00Z
updated: 2026-04-01T14:30:00Z
tags: [coding-style, language]
---

# Coding Preferences

- Preferred language: TypeScript with strict mode
- Tab width: 2 spaces
- Always use explicit return types
- Prefer functional composition over class inheritance</pre>
<p>这种设计的核心优势在于三点：第一，用户可以直接用任何文本编辑器修改记忆内容，无需进入 OpenClaw 的界面；第二，记忆文件可以纳入 Git 版本控制，团队成员可以共享和同步偏好配置；第三，记忆内容完全离线可用，不依赖任何云端服务。memory-core-host-runtime-files 模块通过文件系统监听（fs.watch）检测 Markdown 文件的变更，自动触发重新索引流程——解析 front-matter、提取正文、计算嵌入向量、更新向量存储。</p>
<div class="blog_h2"><span class="graybg">向量存储：sqlite-vec 与 LanceDB 双后端</span></div>
<p>语义搜索依赖向量存储。OpenClaw 提供两个后端选项：</p>
<p><span style="background-color: #c0c0c0;">sqlite-vec</span>（版本 0.1.9）是默认后端。它是 SQLite 的向量搜索扩展，以 npm 依赖 sqlite-vec@0.1.9 的形式声明在 package.json 中。sqlite-vec 将向量存储为 SQLite 表中的 BLOB 列，支持精确最近邻（Exact KNN）和基于量化的近似最近邻（ANN）搜索。对于个人使用场景——通常记忆条目在数百到数千量级——sqlite-vec 的精确 KNN 已经足够高效，查询延迟在亚毫秒级别。sqlite-vec 的优势与 OpenClaw 的本地优先哲学完全一致：单文件数据库，零外部依赖，可直接备份和迁移。</p>
<p><span style="background-color: #c0c0c0;">memory-lancedb</span> 是第二个后端，同样通过 plugin-sdk 导出。LanceDB 是一个嵌入式向量数据库，底层使用 Lance 列式格式，支持 IVF-PQ 索引，适合记忆条目达到十万量级的场景。memory-core-host-engine-storage 模块通过统一的存储抽象层隔离这两个后端，上层代码无需感知底层实现差异：</p>
<pre class="crayon-plain-tag">// memory-core-host-engine-storage 抽象接口
export interface VectorStorageBackend {
  insert(entries: MemoryEntry[]): Promise;
  search(query: Float32Array, topK: number, filter?: MemoryFilter): Promise&lt;ScoredEntry[]&gt;;
  delete(ids: string[]): Promise;
  count(): Promise;
  vacuum(): Promise;
}</pre>
<div class="blog_h2"><span class="graybg">嵌入管道与语义搜索</span></div>
<p>memory-core-host-engine-embeddings 管理嵌入计算的完整管道。当记忆文件被创建或修改时，该模块执行以下流程：</p>
<ol>
<li>解析 Markdown 文件，将正文按段落分块（chunking），每块控制在 512 token 以内</li>
<li>调用当前配置的嵌入模型（Embedding Model）计算向量，默认使用提供者插件中配置的嵌入端点</li>
<li>将向量与元数据（来源文件路径、chunk 偏移、时间戳、标签）一起写入向量存储</li>
<li>维护一个增量索引：仅对变更的块重新计算嵌入，未修改的块保留原有向量</li>
</ol>
<p>memory-core-host-engine-qmd（QMD 引擎）负责查询时的语义匹配。QMD 的全称是 Query-Memory-Document，它实现一个三阶段检索流程：先对用户查询计算嵌入向量，然后在向量存储中执行近似最近邻搜索获取候选集，最后用 BM25 关键词评分对候选集重排序（Re-ranking）。memory-core-host-query 模块负责构建查询对象，将语义相似度阈值、标签过滤、时间范围等条件组合为统一的查询描述符。</p>
<p>记忆系统是 OpenClaw 个性化能力的基石。Agent 运行时在处理每一轮对话时，都会通过 memory-core-engine-runtime 检索相关记忆并注入到系统提示词中。这个过程对用户透明，但直接影响 Agent 回复的个性化程度——它知道用户偏好的编程语言、代码风格、常用工具链，甚至过去对话中建立的项目上下文。</p>
<div class="blog_h1"><span class="graybg">模型提供者</span></div>
<p>OpenClaw 的模型提供者（Model Provider）系统是其多模型支持能力的核心。通过 plugin-sdk 导出的提供者插件超过 25 个，覆盖主流商业 API、开源推理引擎和云平台网关。每个提供者是一个独立插件，遵循统一的注册、认证和模型目录协议。</p>
<div class="blog_h2"><span class="graybg">提供者插件架构</span></div>
<p>每个提供者插件由四个核心文件组成：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">文件</td>
<td style="text-align: center;">职责</td>
</tr>
</thead>
<tbody>
<tr>
<td>provider-entry.ts</td>
<td>插件入口点，注册提供者到插件注册表，声明支持的功能特性（Feature Flags）</td>
</tr>
<tr>
<td>provider-auth.ts</td>
<td>认证逻辑，实现 API Key 或 OAuth 流程</td>
</tr>
<tr>
<td>provider-catalog-shared.ts</td>
<td>模型目录，列出该提供者支持的所有模型及其能力标记（文本/图像/代码等）</td>
</tr>
<tr>
<td>provider-model-shared.ts</td>
<td>模型共享配置，定义 token 限制、定价信息、上下文窗口大小等元数据</td>
</tr>
</tbody>
</table>
<p>提供者插件通过 plugin-sdk 的导出路径注册。以 OpenAI 提供者为例，导出路径为 plugin-sdk/provider-openai，Anthropic 为 plugin-sdk/provider-anthropic，以此类推。</p>
<div class="blog_h2"><span class="graybg">完整提供者清单</span></div>
<p>截至 v2026.4.1，plugin-sdk 导出以下提供者：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">提供者</td>
<td style="text-align: center;">模型举例</td>
<td style="text-align: center;">认证方式</td>
</tr>
</thead>
<tbody>
<tr>
<td>OpenAI</td>
<td>GPT-4o, o3, o4-mini, Codex</td>
<td>API Key / OAuth</td>
</tr>
<tr>
<td>Anthropic (Claude)</td>
<td>Claude Sonnet 4, Opus 4</td>
<td>API Key / OAuth</td>
</tr>
<tr>
<td>Google (Gemini)</td>
<td>Gemini 2.5 Pro, Flash</td>
<td>API Key</td>
</tr>
<tr>
<td>DeepSeek</td>
<td>DeepSeek-V3, DeepSeek-R1</td>
<td>API Key</td>
</tr>
<tr>
<td>xAI (Grok)</td>
<td>Grok-3, Grok-3-mini</td>
<td>API Key</td>
</tr>
<tr>
<td>Ollama</td>
<td>本地部署任意 GGUF 模型</td>
<td>无（本地）</td>
</tr>
<tr>
<td>Mistral</td>
<td>Mistral Large, Codestral</td>
<td>API Key</td>
</tr>
<tr>
<td>MiniMax</td>
<td>MiniMax-Text-01, image-01</td>
<td>API Key</td>
</tr>
<tr>
<td>Moonshot（月之暗面）</td>
<td>Kimi</td>
<td>API Key</td>
</tr>
<tr>
<td>ModelStudio（通义千问）</td>
<td>Qwen-Max, Qwen-Plus</td>
<td>API Key</td>
</tr>
<tr>
<td>Qianfan（百度文心）</td>
<td>ERNIE-4.0, ERNIE-Speed</td>
<td>API Key</td>
</tr>
<tr>
<td>NVIDIA</td>
<td>Nemotron, Llama 3 NVIDIA</td>
<td>API Key</td>
</tr>
<tr>
<td>HuggingFace</td>
<td>Inference API 托管模型</td>
<td>API Token</td>
</tr>
<tr>
<td>Together</td>
<td>Llama, Mixtral 等开源模型</td>
<td>API Key</td>
</tr>
<tr>
<td>Venice</td>
<td>隐私优先推理</td>
<td>API Key</td>
</tr>
<tr>
<td>vLLM</td>
<td>自托管 vLLM 实例</td>
<td>自定义</td>
</tr>
<tr>
<td>SGLang</td>
<td>自托管 SGLang 实例</td>
<td>自定义</td>
</tr>
<tr>
<td>BytePlus（火山引擎）</td>
<td>豆包大模型</td>
<td>API Key</td>
</tr>
<tr>
<td>Cloudflare AI Gateway</td>
<td>Workers AI 代理</td>
<td>API Token</td>
</tr>
<tr>
<td>Amazon Bedrock</td>
<td>Claude on Bedrock, Titan</td>
<td>AWS IAM</td>
</tr>
<tr>
<td>Anthropic Vertex</td>
<td>Claude on Vertex AI</td>
<td>GCP Service Account</td>
</tr>
<tr>
<td>Chutes</td>
<td>GPU 推理市场</td>
<td>API Key</td>
</tr>
<tr>
<td>KiloCode</td>
<td>KiloCode 模型</td>
<td>API Key</td>
</tr>
<tr>
<td>Kimi Coding</td>
<td>Kimi 代码模型</td>
<td>API Key</td>
</tr>
<tr>
<td>OpenCode / OpenCode Go</td>
<td>开源代码推理</td>
<td>API Key</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">认证系统：双模认证与密钥管理</span></div>
<p>认证子系统由四个模块组成：provider-auth-api-key（API Key 认证）、provider-auth-login（OAuth 登录认证）、provider-auth-result（认证结果封装）和 provider-auth-runtime（运行时认证状态管理）。</p>
<p>大多数提供者支持 API Key 单一模式，但 OpenAI 和 Anthropic 等主流提供者同时支持 <span style="background-color: #c0c0c0;">OAuth 登录</span>和 API Key 两种方式。OAuth 模式下，用户通过浏览器完成授权后，OpenClaw 获取 access token 并自动管理刷新流程。这种双模设计（Auth Rotation）允许用户在免费额度用完后无缝切换到自有 API Key，反之亦然。</p>
<p><span style="background-color: #c0c0c0;">合成认证（Synthetic Auth）</span>通过 resolveSyntheticAuth 函数实现。当多个提供者共享同一底层认证（例如 Anthropic Vertex 使用 GCP 凭据，而不是 Anthropic 原生 API Key）时，合成认证将底层凭据转换为提供者期望的格式。实现位于认证运行时模块中：</p>
<pre class="crayon-plain-tag">// provider-auth-runtime 中的合成认证解析
export async function resolveSyntheticAuth(
  provider: ProviderId,
  secretStore: SecretStore
): Promise {
  const secretRef = getProviderSecretRef(provider);
  const rawCredential = await secretStore.resolve(secretRef);

  // 根据提供者类型转换凭据格式
  switch (provider) {
    case 'anthropic-vertex':
      return synthesizeVertexAuth(rawCredential as GCPServiceAccount);
    case 'amazon-bedrock':
      return synthesizeBedrockAuth(rawCredential as AWSCredentials);
    default:
      return { type: 'api-key', key: rawCredential as string };
  }
}</pre>
<p><span style="background-color: #c0c0c0;">SecretRef</span> 是 OpenClaw 的凭据引用语义。凭据不以明文存储在配置文件中，而是通过 SecretRef 引用操作系统的密钥链（macOS Keychain、Windows Credential Manager、Linux Secret Service）。SecretRef 的格式为 secretref:</p>
<p>:，运行时由 memory-core-host-secret 模块解析为实际凭据值。</p>
<div class="blog_h2"><span class="graybg">模型故障转移</span></div>
<p>模型故障转移（Model Failover）是 OpenClaw 应对 API 速率限制和服务中断的核心机制（详见 <a href="https://docs.openclaw.ai/concepts/model-failover">docs.openclaw.ai/concepts/model-failover</a>）。当主模型返回 429（Rate Limited）或 5xx 错误时，系统自动将请求路由到预配置的备选模型。故障转移配置在用户的 settings 文件中定义：</p>
<pre class="crayon-plain-tag">{
  "models": {
    "primary": "anthropic:claude-sonnet-4-20260514",
    "fallback": [
      "openai:gpt-4o",
      "google:gemini-2.5-pro"
    ],
    "failover": {
      "maxRetries": 2,
      "retryDelayMs": 1000,
      "fallbackOnRateLimit": true,
      "fallbackOnServerError": true
    }
  }
}</pre>
<p>故障转移逻辑在路由层（src/routing/）实现，对上层 Agent 运行时透明。路由层维护每个提供者的健康状态和速率限制窗口，在主模型不可用时按 fallback 列表顺序尝试备选模型。</p>
<div class="blog_h2"><span class="graybg">v2026.3.28 新增特性</span></div>
<p>v2026.3.28 版本对提供者系统引入了三项重要变更：</p>
<p><strong>xAI 迁移至 Responses API</strong>：xAI 提供者从传统的 Chat Completions API 迁移到 Responses API 格式，同时启用 x_search 原生网页搜索功能。Grok 模型可直接在对话中调用 xAI 的搜索基础设施，无需额外的工具调用层。</p>
<p><strong>MiniMax 图像生成</strong>：MiniMax 提供者新增 image-01 模型支持，通过 MiniMax 的图像生成 API 实现文生图能力。该功能作为提供者拥有的工具（Provider-owned Tool）注册，遵循 OpenClaw 的设计原则——提供者特有的工具和设置归属于提供者插件，而非核心系统。</p>
<p><strong>通义千问认证变更</strong>：Qwen 的 portal auth 模式被移除，统一切换为 Model Studio API Key 认证。这是一次破坏性变更，已有的 portal auth 用户需要手动迁移到 API Key 模式。</p>
<div class="blog_h2"><span class="graybg">GitHub Copilot 登录支持</span></div>
<p>OpenClaw 通过 plugin-sdk/github-copilot-login 和 plugin-sdk/github-copilot-token 两个导出模块支持 GitHub Copilot 账户登录。拥有 Copilot 订阅的用户可以直接使用 GitHub 账户认证，通过 Copilot 的基础设施访问底层模型（GPT-4o、Claude 等），无需单独配置每个提供者的 API Key。认证流程复用 GitHub 的 Device Flow OAuth，获取 Copilot token 后由 github-copilot-token 模块管理令牌刷新。</p>
<div class="blog_h1"><span class="graybg">ACP 协议</span></div>
<p><span style="background-color: #c0c0c0;">Agent Client Protocol（ACP）</span>是 OpenClaw 定义的有状态 Agent 会话协议。ACP 的核心思想是将 AI Agent 的交互从特定的聊天界面中解耦出来，使其可以通过任意通信渠道（Discord、iMessage、终端等）启动和管理有状态的 Agent 工作会话。项目依赖 @agentclientprotocol/sdk@0.17.1 提供协议的核心类型和客户端实现。</p>
<div class="blog_h2"><span class="graybg">ACPX：无头 CLI 工具</span></div>
<p><span style="background-color: #c0c0c0;">ACPX</span>（仓库 openclaw/acpx，1,834 stars）是 OpenClaw 的无头（Headless）ACP CLI 客户端。它允许用户从命令行创建、管理和交互 ACP 会话，无需图形界面。ACPX 的典型使用场景包括 CI/CD 管道中的 Agent 自动化、服务器端部署和脚本编排。</p>
<div class="blog_h2"><span class="graybg">ACP 渠道绑定</span></div>
<p>ACP 会话可以绑定到任意聊天渠道。通过 /acp spawn codex --bind here 命令，用户可以在当前渠道上下文中创建一个 ACP 会话。目前支持的绑定包括：</p>
<ul>
<li><strong>Discord</strong>：通过 Discord Bot 渠道绑定，ACP 会话映射到 Discord 线程</li>
<li><strong>BlueBubbles</strong>：macOS 上的 iMessage 桥接，ACP 会话通过 BlueBubbles API 接入 iMessage</li>
<li><strong>iMessage</strong>：直接 iMessage 绑定（仅 macOS/iOS 平台）</li>
</ul>
<p>ACP 的核心分层需要明确区分三个概念：<span style="background-color: #c0c0c0;">聊天表面（Chat Surface）</span>是用户交互的 UI 层，可以是 Discord 频道、终端窗口或 Web 界面；<span style="background-color: #c0c0c0;">ACP 会话（ACP Session）</span>是有状态的 Agent 交互上下文，维护对话历史、工作区状态和工具授权；<span style="background-color: #c0c0c0;">运行时工作区（Runtime Workspace）</span>是 Agent 实际执行操作的文件系统沙箱。一个聊天表面可以关联多个 ACP 会话，而每个 ACP 会话绑定到唯一的运行时工作区。</p>
<div class="blog_h2"><span class="graybg">MCP 集成与工具桥接</span></div>
<p>OpenClaw 集成了 <span style="background-color: #c0c0c0;">Model Context Protocol（MCP）</span>，依赖 @modelcontextprotocol/sdk@1.29.0。MCP 定义了 AI 模型与外部工具之间的标准通信协议，OpenClaw 通过 MCP 桥接层将外部 MCP 工具服务器暴露给 Agent 运行时。</p>
<p>v2026.3.31 版本引入了 <span style="background-color: #c0c0c0;">ACPX plugin-tools MCP 桥接</span>的关键安全变更：MCP 工具默认关闭（explicit default-off），必须在配置中显式启用。这一变更源于信任边界加固（Trust Boundary Hardening）的安全考量——外部 MCP 工具服务器可能执行任意代码，默认启用会扩大攻击面。启用配置示例：</p>
<pre class="crayon-plain-tag">{
  "mcp": {
    "servers": {
      "filesystem": {
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"],
        "enabled": true
      }
    },
    "trustPolicy": "prompt-per-tool"
  }
}</pre>
<p>trustPolicy 支持三个级别：prompt-per-tool（每次工具调用需用户确认）、prompt-once（首次确认后自动信任）和 trust-all（完全信任，仅建议在受控环境中使用）。</p>
<div class="blog_h2"><span class="graybg">OpenAI apply_patch 默认启用</span></div>
<p>对于 OpenAI 和 Codex 系列模型，OpenClaw 默认启用 apply_patch 工具。这是 OpenAI Codex 模型原生支持的代码编辑工具，直接通过 API 返回结构化的补丁指令（patch instruction），由 OpenClaw 的运行时执行文件修改。相较于让模型输出完整文件内容再做 diff，apply_patch 减少了输出 token 消耗，降低了大文件编辑时的错误率。apply_patch 的沙箱权限与 write 权限对齐——在非主会话的 Docker 沙箱中，apply_patch 的写入范围受到与普通文件写入相同的约束。</p>
<div class="blog_h2"><span class="graybg">CLI 后端插件：Claude CLI / Codex CLI / Gemini CLI</span></div>
<p>v2026.3.31 将三个主要 CLI 后端——<span style="background-color: #c0c0c0;">Claude CLI</span>、<span style="background-color: #c0c0c0;">Codex CLI</span> 和 <span style="background-color: #c0c0c0;">Gemini CLI</span>——的推理默认行为迁移到各自的 bundled plugin 中。通过 Plugin SDK 的 cli-backend 和 cli-runtime 导出路径，CLI 后端可以注册自定义的推理流、工具暴露和会话管理策略。这一迁移的意义在于解耦——核心不再硬编码 CLI 后端的行为，第三方插件可以通过相同的接口注册自定义 CLI 后端。</p>
<div class="blog_h2"><span class="graybg">ACP 与 Agent-to-Agent 通信</span></div>
<p>ACP 的深层价值体现在 <span style="background-color: #c0c0c0;">Agent-to-Agent（A2A）通信</span>能力上。OpenClaw 的 Session 工具集——sessions_list、sessions_history、sessions_send——允许一个 Agent 会话发现、查询和向另一个 Agent 会话发送消息。sessions_send 支持可选的 reply-back 模式（ping-pong 通信）和 announce 步骤，允许 Agent 之间进行结构化的协调对话。</p>
<p>在多 Agent 部署场景下（例如一个 Agent 负责客户对话，另一个 Agent 负责后端任务执行），A2A 通信避免了传统架构中需要外部消息队列的复杂性。所有通信都通过 Gateway 的 WebSocket 控制平面路由，Agent 之间共享同一个运行时基础设施但拥有隔离的会话上下文和工作区。</p>
<p>v2026.3.31 新增的 ACP 渠道绑定进一步扩展了这一能力：/acp spawn codex --bind here 可以将当前聊天表面直接绑定为 Codex 驱动的工作区，无需创建子线程。通过这种方式，用户可以在 Discord 频道中直接启动一个代码编写 Agent，Agent 的输出直接出现在对话流中。</p>
<div class="blog_h1"><span class="graybg">媒体管道</span></div>
<p>OpenClaw 的媒体处理管道位于 src/media/ 目录，负责所有非文本内容的预处理、理解和生命周期管理。通过 plugin-sdk 导出三个核心模块：media-runtime（运行时管道调度）、media-understanding（媒体内容理解接口）和 media-understanding-runtime（理解模块的运行时绑定）。另有 web-media 导出处理 Web 渠道的媒体特化逻辑。</p>
<div class="blog_h2"><span class="graybg">图像处理：sharp 管道</span></div>
<p>图像处理依赖 sharp@0.34.5——Node.js 生态中性能最高的图像处理库，底层使用 libvips。OpenClaw 使用 sharp 执行以下处理：</p>
<ul>
<li>缩放（Resize）：将用户上传的图像缩放到模型支持的最大分辨率，避免浪费 token 或超出 API 限制</li>
<li>格式转换：将 BMP、TIFF、WebP 等格式统一转换为 JPEG 或 PNG，确保所有提供者都能接收</li>
<li>元数据剥离：移除 EXIF 信息中的地理位置、设备信息等隐私数据</li>
<li>缩略图生成：为 UI 显示生成低分辨率预览</li>
</ul>
<p>文件类型检测使用 file-type@22.0.0，通过魔数（Magic Number）而非文件扩展名判断文件类型，防止恶意文件伪装。</p>
<div class="blog_h2"><span class="graybg">PDF 处理</span></div>
<p>PDF 处理依赖 pdfjs-dist@5.6.205（Mozilla PDF.js 的 npm 发行版）。处理流程包括文本提取、页面渲染为图像（用于多模态模型的视觉理解）和结构化内容解析。对于大型 PDF，OpenClaw 实现分页处理策略——仅提取与当前对话上下文相关的页面范围，而非一次性加载整个文档。</p>
<div class="blog_h2"><span class="graybg">音频与视频处理</span></div>
<p>音频和视频处理管道处理用户上传的多媒体文件或通过语音输入采集的音频流。音频处理包括格式转换（统一为 WAV/MP3）、采样率标准化和静音检测。转录（Transcription）钩子将音频输入转换为文本，集成到 Agent 的对话流程中——语音消息被自动转录后作为文本消息处理，Agent 可以选择性地以语音或文本方式回复。</p>
<p>视频处理采用关键帧提取策略：从视频中按固定间隔或场景变化检测提取关键帧，将其作为图像序列发送给多模态模型进行理解，避免处理完整视频流的高昂计算成本。</p>
<div class="blog_h2"><span class="graybg">大小限制与临时文件生命周期</span></div>
<p>每个渠道（Channel）可独立配置媒体文件的大小上限。例如 Discord 渠道的配置：</p>
<pre class="crayon-plain-tag">{
  "channels": {
    "discord": {
      "mediaMaxMb": 25
    },
    "web": {
      "mediaMaxMb": 100
    },
    "cli": {
      "mediaMaxMb": 500
    }
  }
}</pre>
<p>超过限制的文件在预处理阶段即被拒绝，不进入管道后续环节。临时文件（处理过程中的中间产物）遵循严格的生命周期管理：每个媒体处理任务创建独立的临时目录，处理完成后无论成功还是失败都会清理。media-runtime 模块维护一个临时文件注册表，进程退出时执行兜底清理（cleanup-on-exit），防止磁盘泄漏。</p>
<div class="blog_h2"><span class="graybg">媒体理解与多模态接入</span></div>
<p>media-understanding 和 media-understanding-runtime 两个 SDK 导出路径定义了媒体内容理解的接口和运行时实现。媒体理解不仅仅是格式转换——它将图像、文档和音频转化为模型可以消费的结构化输入。对于图像，理解管道提取图中的文字（OCR）、识别对象和场景；对于 PDF，它生成页面摘要和结构化的段落索引；对于音频，它输出时间戳标注的转录文本。</p>
<p>多模态理解的输出格式遵循各模型提供者的要求。OpenAI 的 GPT-4o 和 Anthropic 的 Claude Sonnet 4 接受 base64 编码的图像嵌入消息体；Google Gemini 支持更大的媒体文件通过 File API 上传后引用。media-understanding-runtime 的职责是根据当前活跃的模型提供者，选择最优的编码和传输策略。</p>
<div class="blog_h2"><span class="graybg">媒体管道的可读性阅读器</span></div>
<p>OpenClaw 集成了 @mozilla/readability@0.6.0（Mozilla 的可读性提取库）和 linkedom@0.18.12（轻量 DOM 实现），用于从网页内容中提取正文。当 Agent 使用浏览器工具访问网页时，原始 HTML 经 linkedom 解析后，Readability 算法提取核心正文内容，剥除导航栏、广告、侧边栏等噪声元素。提取后的纯文本进入 Agent 的上下文窗口，相比注入原始 HTML 大幅减少 token 消耗。</p>
<p>Markdown 渲染由 markdown-it@14.1.1 处理。Agent 输出的 Markdown 格式回复在发送到各渠道前，根据目标渠道的能力进行格式适配：Discord 原生支持 Markdown，Telegram 支持部分 Markdown 子集，WhatsApp 使用 WhatsApp 风格的文本格式化，而 SMS/iMessage 则退化为纯文本。</p>
<div class="blog_h1"><span class="graybg">语音系统</span></div>
<p>OpenClaw 的语音系统覆盖了从语音唤醒到语音合成的完整链路。plugin-sdk 导出三个语音模块：speech（公共接口）、speech-core（核心实现）和 speech-runtime（运行时绑定）。语音功能根据平台和交互模式分为四种形态。</p>
<div class="blog_h2"><span class="graybg">Voice Wake：macOS/iOS 唤醒词</span></div>
<p><span style="background-color: #c0c0c0;">Voice Wake</span>（详见 <a href="https://docs.openclaw.ai/nodes/voicewake">docs.openclaw.ai/nodes/voicewake</a>）是 macOS 和 iOS 平台的唤醒词功能。设备持续监听环境音频，检测到预设唤醒词后激活 Agent 会话。唤醒词检测在设备端本地运行，不向云端发送音频流——这与 OpenClaw 的本地优先原则一致。</p>
<p>唤醒后的消息转发通过 <span style="background-color: #c0c0c0;">VoiceWakeForwarder</span> 实现。用户语音经本地语音识别转为文本后，VoiceWakeForwarder 调用 OpenClaw 的 CLI 接口将文本传递给 Agent：</p>
<pre class="crayon-plain-tag">openclaw-mac agent --message "${text}" --thinking low</pre>
<p>VoiceWakeForwarder 的实现中需要特别处理 Shell 转义（Shell Escaping）：用户语音转录文本可能包含引号、美元符号、反引号等 Shell 特殊字符，直接拼接到命令行会导致注入风险或解析错误。转发器对文本进行严格的 Shell 转义后再传递。--thinking low 参数指示 Agent 使用低延迟思考模式，优先响应速度而非推理深度，适配语音交互对实时性的要求。</p>
<div class="blog_h2"><span class="graybg">Talk Mode：Android 持续语音</span></div>
<p><span style="background-color: #c0c0c0;">Talk Mode</span>（详见 <a href="https://docs.openclaw.ai/nodes/talk">docs.openclaw.ai/nodes/talk</a>）是 Android 平台的持续语音对话模式。与 Voice Wake 的"唤醒→单次交互"模式不同，Talk Mode 维持一个持续开放的语音通道——用户和 Agent 可以进行多轮语音对话，无需每轮重新唤醒。Talk Mode 使用 VAD（Voice Activity Detection，语音活动检测）自动判断用户发言的起止，实现自然的对话节奏。</p>
<div class="blog_h2"><span class="graybg">Push-to-Talk：macOS 覆盖层</span></div>
<p>macOS 平台还提供 <span style="background-color: #c0c0c0;">Push-to-Talk</span> 模式，以系统级覆盖层（Overlay）的形式运行。用户通过长按快捷键激活麦克风输入，松开后结束录音并发送。这种模式适合在桌面工作流中快速提问，无需切换到 OpenClaw 的窗口。覆盖层使用 AppKit 的 NSPanel 实现，设置为浮动在所有窗口之上。</p>
<div class="blog_h2"><span class="graybg">TTS：ElevenLabs 与系统回退</span></div>
<p>语音合成（TTS，Text-to-Speech）采用双层策略。首选方案是 <span style="background-color: #c0c0c0;">ElevenLabs</span> 的 API，提供高质量、低延迟、多语言的语音合成。当 ElevenLabs 不可用（网络离线或未配置 API Key）时，系统自动回退到平台原生 TTS：macOS 使用 AVSpeechSynthesizer，iOS 使用 AVSpeechSynthesizer（同一框架），Android 使用 android.speech.tts.TextToSpeech。</p>
<p>此外，OpenClaw 还集成了 node-edge-tts@1.2.10 作为第三层 TTS 后端。Edge TTS 调用 Microsoft Edge 浏览器的在线 TTS 服务，免费且支持多语言多音色，在无 ElevenLabs 订阅但有网络连接的场景下是实用的中间选项。</p>
<div class="blog_h2"><span class="graybg">Voice Call 插件与闭环测试</span></div>
<p>Voice Call 插件打包在 extensions/ 目录中，作为内置扩展随 OpenClaw 分发。它实现了完整的语音通话功能——用户可以像打电话一样与 Agent 进行实时语音对话，双向音频流通过 WebRTC 或平台原生音频框架传输。</p>
<p>语音通话的质量保证依赖 <span style="background-color: #c0c0c0;">闭环测试（Closed-Loop Testing）</span>。测试脚本通过 test:voicecall:closedloop npm script 执行，流程如下：自动生成测试文本 → TTS 合成为音频 → 音频作为输入馈送给语音通话管道 → Agent 处理并生成回复 → TTS 合成回复音频 → 转录回复音频为文本 → 对比原始文本与回复内容的语义一致性。这种端到端闭环消除了人工测试的不确定性，确保语音管道中每个环节（ASR → 推理 → TTS）都正常工作。</p>
<pre class="crayon-plain-tag"># 执行语音通话闭环测试
pnpm test:voicecall:closedloop

# 测试流程：
# 1. 生成测试 prompt
# 2. TTS 合成输入音频
# 3. 注入音频到 voice call 管道
# 4. 等待 Agent 响应
# 5. 捕获 TTS 输出音频
# 6. ASR 转录输出
# 7. 断言：输出文本与预期语义匹配</pre>
<p>整个语音系统体现了 OpenClaw 的多端一致性追求：同一个 Agent 可以通过唤醒词、持续语音、按键说话或语音通话四种方式接收语音输入，通过 ElevenLabs、Edge TTS 或系统原生 TTS 三种方式输出语音回复，所有组合在各个平台上的行为保持一致。语音能力不是一个附加功能，而是与文本渠道同等地位的一等交互模式。</p>
<p><!-- Chapter 13-17: OpenClaw 深度技术解析 --></p>
<div class="blog_h1"><span class="graybg">原生多端应用</span></div>
<p>OpenClaw 的多端战略并非简单的 WebView 包装。macOS、iOS、Android 三个原生客户端各自承担着差异化的职责：macOS 应用是开发者的本地控制台与调试中心，iOS 应用是移动端的轻量节点（Node），Android 应用则面向最广泛的设备指令族群。三者通过 Gateway WebSocket 协议统一通信，实现跨平台的节点注册、指令派发与画布同步。</p>
<div class="blog_h2"><span class="graybg">13.1 macOS 应用：菜单栏中枢</span></div>
<p>macOS 应用的源码位于 apps/macos/，采用 SwiftUI + AppKit 混合架构，以菜单栏常驻图标为交互入口。在 OpenClaw 内部词汇表中，macOS 应用的代号是 <span style="background-color: #c0c0c0;">makeup</span>（即 "mac app" 的谐音缩写）。</p>
<p>应用的核心功能涵盖以下几个层面：</p>
<p><span style="background-color: #c0c0c0;">Gateway 健康监控</span>：菜单栏图标实时反映 Gateway 进程状态，包括连接数、内存占用与心跳延迟。点击图标弹出的面板提供一键重启入口。Gateway 的重启必须通过 OpenClaw Mac 应用本身或 scripts/restart-mac.sh 脚本执行，而非手动在 tmux 中操作——后者会绕过进程监控链，导致状态不一致。</p>
<p><span style="background-color: #c0c0c0;">语音唤醒与 Push-to-Talk 悬浮层</span>：Voice Wake 持续监听唤醒词，PTT（Push-to-Talk）覆盖层以半透明悬浮窗的形式驻留桌面。两者共同构成语音交互的 macOS 原生入口。</p>
<p><span style="background-color: #c0c0c0;">WebChat 嵌入与调试工具</span>：内嵌 WebChat 视图支持与 Gateway 的实时对话，同时暴露调试面板用于查看消息流、工具调用日志与 token 消耗。</p>
<p><span style="background-color: #c0c0c0;">SSH 隧道远程控制</span>：macOS 应用可通过 SSH 隧道连接远程部署的 Gateway 实例，在本地菜单栏操控云端服务。</p>
<div class="blog_h3"><span class="graybg">13.1.1 SwiftUI 状态管理：Observation 框架</span></div>
<p>macOS 应用的状态管理已全面迁移至 Swift 5.9 引入的 <span style="background-color: #c0c0c0;">Observation 框架</span>，使用 @Observable 宏标记可观察类型，以 @Bindable 实现属性级的双向绑定。旧版 ObservableObject / @StateObject / @Published 模式已被显式弃用 — 任何残留的 legacy 用法都应迁移至新框架。Observation 框架的优势在于更细粒度的依赖追踪：SwiftUI 仅在被实际读取的属性发生变化时重新渲染视图，而非 ObservableObject 的整体通知模式。</p>
<pre class="crayon-plain-tag">// 正确：Observation 框架
@Observable
final class GatewayMonitor {
    var isConnected = false
    var latencyMs: Int = 0
    var sessionCount: Int = 0
}

// 错误：已弃用的旧模式，不要使用
// class GatewayMonitor: ObservableObject {
//     @Published var isConnected = false
// }</pre>
<div class="blog_h3"><span class="graybg">13.1.2 签名构建与 TCC 权限</span></div>
<p>macOS 应用需要签名构建才能使系统权限在重新编译后持久化。未签名的开发构建在每次 rebuild 后都会触发 TCC（Transparency, Consent, and Control）权限重置弹窗。打包脚本位于 scripts/package-mac-app.sh，负责代码签名、公证（Notarization）与 DMG 封装。</p>
<p>macOS 节点模式（Node Mode）暴露的系统能力通过 TCC 权限映射管控：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">节点命令</td>
<td style="text-align: center;">功能</td>
<td style="text-align: center;">TCC 权限</td>
</tr>
</thead>
<tbody>
<tr>
<td>system.run</td>
<td>执行本地命令，返回 stdout/stderr/exit code</td>
<td>needsScreenRecording 标志位</td>
</tr>
<tr>
<td>system.notify</td>
<td>发送用户通知</td>
<td>notifications</td>
</tr>
<tr>
<td>canvas.*</td>
<td>画布操作路由</td>
<td>screen-recording</td>
</tr>
<tr>
<td>camera.*</td>
<td>摄像头抓取</td>
<td>camera</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">13.1.3 统一日志系统</span></div>
<p>macOS 应用的日志通过 scripts/clawlog.sh 脚本统一查询，底层使用 macOS 的 Unified Logging 系统，支持按子系统（subsystem）过滤。常见操作：</p>
<pre class="crayon-plain-tag"># 实时跟踪所有 OpenClaw 子系统日志
./scripts/clawlog.sh --follow

# 按类别过滤
./scripts/clawlog.sh --category networking --tail 100

# 查看特定子系统
./scripts/clawlog.sh --subsystem ai.openclaw.gateway</pre>
<div class="blog_h2"><span class="graybg">13.2 iOS 应用：移动节点</span></div>
<p>iOS 应用源码位于 apps/ios/，是一个标准 Xcode 项目 + SwiftUI 工程。与 macOS 应用不同，iOS 应用的定位是作为 Gateway 的远程节点运行，通过 <span style="background-color: #c0c0c0;">Bonjour 设备发现</span>（Device Discovery）机制自动配对局域网内的 Gateway 实例，并通过 Gateway WebSocket 建立持久连接。</p>
<p>iOS 节点提供的核心能力：</p>
<p><span style="background-color: #c0c0c0;">Canvas Surface</span>：在 iOS 设备上渲染 Agent 驱动的画布内容，支持触摸交互。</p>
<p><span style="background-color: #c0c0c0;">Voice Wake 转发</span>：iOS 端的语音唤醒检测结果通过 WebSocket 转发至 Gateway，实现移动端的免触语音激活。</p>
<p><span style="background-color: #c0c0c0;">Talk Mode</span>：长按说话的语音交互模式，音频流直接传输至 Gateway 进行识别与处理。</p>
<p><span style="background-color: #c0c0c0;">Camera Snap/Clip</span>：支持拍照快照和短视频片段采集，供 Agent 的视觉能力使用。</p>
<p><span style="background-color: #c0c0c0;">Screen Recording</span>：通过 ReplayKit 进行屏幕录制，将录制内容作为上下文发送给 Agent。</p>
<p>iOS 应用的版本号维护在两个位置：apps/ios/Sources/Info.plist 和 apps/ios/Tests/Info.plist，关键字段为 CFBundleShortVersionString（展示版本号）和 CFBundleVersion（构建号）。发版时两个文件必须同步更新。</p>
<div class="blog_h2"><span class="graybg">13.3 Android 应用：全能设备节点</span></div>
<p>Android 应用位于 apps/android/，使用 Kotlin + Gradle 构建。与 iOS 应用相比，Android 节点暴露了更丰富的设备指令族群（Device Command Families），充分利用 Android 平台的开放性。</p>
<p>应用 UI 组织为三个主要标签页：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">标签页</td>
<td style="text-align: center;">功能</td>
</tr>
</thead>
<tbody>
<tr>
<td>Connect</td>
<td>设备配对入口，支持设置码（Setup Code）和手动输入两种方式</td>
</tr>
<tr>
<td>Chat Sessions</td>
<td>会话列表与聊天界面</td>
</tr>
<tr>
<td>Voice</td>
<td>语音交互控制面板</td>
</tr>
</tbody>
</table>
<p>Android 节点支持的设备指令族群是三端中最丰富的：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">指令族</td>
<td style="text-align: center;">能力</td>
</tr>
</thead>
<tbody>
<tr>
<td>notifications</td>
<td>读取/发送系统通知</td>
</tr>
<tr>
<td>location</td>
<td>GPS 定位与地理围栏</td>
</tr>
<tr>
<td>SMS</td>
<td>短信读取与发送</td>
</tr>
<tr>
<td>photos</td>
<td>相册访问与照片上传</td>
</tr>
<tr>
<td>contacts</td>
<td>通讯录读写</td>
</tr>
<tr>
<td>calendar</td>
<td>日历事件管理</td>
</tr>
<tr>
<td>motion</td>
<td>加速度计、陀螺仪等传感器数据</td>
</tr>
<tr>
<td>app update</td>
<td>应用自更新管理</td>
</tr>
</tbody>
</table>
<p>此外，Android 端同样支持 Canvas 渲染、摄像头采集与屏幕录制能力。</p>
<div class="blog_h3"><span class="graybg">13.3.1 Android 构建与测试命令</span></div>
<pre class="crayon-plain-tag"># 单元测试（Play Debug 变体）
./gradlew :app:testPlayDebugUnitTest

# 第三方集成测试
./gradlew :app:testThirdPartyDebugUnitTest

# Kotlin 代码风格检查
./gradlew :app:ktlintCheck :benchmark:ktlintCheck

# 发布 AAB 构建
bun apps/android/scripts/build-release-aab.ts</pre>
<p>版本信息定义在 apps/android/app/build.gradle.kts 中的 versionName（展示版本）和 versionCode（数字递增版本）。</p>
<div class="blog_h2"><span class="graybg">13.4 跨平台节点协议</span></div>
<p>三个原生应用通过统一的 <span style="background-color: #c0c0c0;">Gateway WebSocket 协议</span>与 Gateway 通信。节点相关的核心命令包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">命令</td>
<td style="text-align: center;">方向</td>
<td style="text-align: center;">功能</td>
</tr>
</thead>
<tbody>
<tr>
<td>node.list</td>
<td>Gateway → 客户端</td>
<td>枚举已连接的所有节点及其能力声明</td>
</tr>
<tr>
<td>node.describe</td>
<td>Gateway → 节点</td>
<td>查询指定节点的详细能力描述与参数 schema</td>
</tr>
<tr>
<td>node.invoke</td>
<td>Gateway → 节点</td>
<td>在指定节点上执行命令并返回结果</td>
</tr>
</tbody>
</table>
<p>macOS 平台的系统权限通过 TCC 框架管控，涵盖 screen-recording、notifications、camera、location 四项。每项权限与特定的节点命令能力绑定，应用会在首次调用时请求授权。</p>
<p>会话级别的权限提升通过 /elevated on|off 命令控制。开启后，当前会话获得完整的 bash 访问权限；关闭时回退到受限执行面。该命令是每个会话独立的，不影响其他并发会话。</p>
<div class="blog_h3"><span class="graybg">13.4.1 v2026.3.31 安全强化</span></div>
<p><span style="background-color: #c0c0c0;">v2026.3.31</span> 引入了两项与节点安全相关的破坏性变更。第一，节点命令在设备配对（Device Pairing）完成后不再自动启用 — 必须经过显式的<span style="background-color: #c0c0c0;">节点配对审批</span>（Node Pairing Approval）后，节点命令才会暴露给 Agent。设备配对仅建立了 WebSocket 连接通道，而节点配对审批确认了用户对该设备能力暴露的授权意图。</p>
<p>第二，由节点端发起的运行（Node-originated Runs）被限制在缩减的可信执行面（Reduced Trusted Surface）上。即使节点本身拥有完整能力，从节点侧主动触发的执行流只能使用预定义的安全工具子集。</p>
<p><!-- Chapter 14 --></p>
<div class="blog_h1"><span class="graybg">Live Canvas</span></div>
<p><span style="background-color: #c0c0c0;">Live Canvas</span> 是 OpenClaw Gateway 托管的 Agent 驱动可视化工作区。与传统的静态输出不同，Live Canvas 是持久性的交互画面 — Agent 可以在其上推送内容、重置状态、执行脚本、捕获快照。Canvas 的跨端渲染由原生应用（macOS、iOS SwiftUI、Android）各自实现，而画布操控逻辑通过 <span style="background-color: #c0c0c0;">A2UI</span>（Agent to UI）协议统一抽象。</p>
<div class="blog_h2"><span class="graybg">14.1 A2UI 协议与构建</span></div>
<p>A2UI 定义了 Agent 向 UI 层发送控制指令的协议规范。Canvas 宿主的 A2UI 实现位于 src/canvas-host/a2ui/ 目录。该实现会被打包为一个独立的 bundle，由 Gateway 在运行时加载并注入到 Canvas 宿主容器中。</p>
<p>bundle 的构建产物通过哈希文件 src/canvas-host/a2ui/.bundle.hash 进行版本追踪（自动生成，不应手动编辑）。构建命令有两种等价形式：</p>
<pre class="crayon-plain-tag"># 通过 pnpm script
pnpm canvas:a2ui:bundle

# 通过 shell 脚本
scripts/bundle-a2ui.sh</pre>
<p>A2UI bundle 的构建是整体 pnpm build 流水线的第一步。完整的构建管线为：</p>
<pre class="crayon-plain-tag">pnpm build
# 等价于：
# 1. pnpm canvas:a2ui:bundle
# 2. tsdown-build.mjs
# 3. runtime-postbuild.mjs</pre>
<p>A2UI 的 vendor 源码维护在 vendor/a2ui 目录，原生端的共享封装层位于 apps/shared/OpenClawKit/Tools/CanvasA2UI。</p>
<div class="blog_h3"><span class="graybg">14.1.1 跨编译注意事项</span></div>
<p>A2UI bundle 的构建在交叉编译环境下可能失败。典型场景是在 Apple Silicon 上通过 QEMU 构建 amd64 目标 — 这种情况下 A2UI 的构建步骤可能因 QEMU 对某些指令集的模拟不完整而崩溃。Dockerfile 中已对此做了防护处理：当 A2UI bundle 构建失败时，会创建一个存根（stub）文件代替，确保 Docker 镜像的整体构建不会中断。这意味着 QEMU 交叉编译产出的镜像可能不包含完整的 Canvas 功能。</p>
<div class="blog_h2"><span class="graybg">14.2 Canvas 操作原语</span></div>
<p>Canvas 的操作模型由四个核心原语组成：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">操作</td>
<td style="text-align: center;">语义</td>
<td style="text-align: center;">典型用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>canvas.push</td>
<td>向 Canvas 追加内容（HTML/JS/CSS 片段）</td>
<td>增量构建 UI 界面</td>
</tr>
<tr>
<td>canvas.reset</td>
<td>清空 Canvas 并重新初始化</td>
<td>切换上下文或重置状态</td>
</tr>
<tr>
<td>canvas.eval</td>
<td>在 Canvas 上下文中执行任意 JavaScript</td>
<td>动态交互逻辑、数据可视化</td>
</tr>
<tr>
<td>canvas.snapshot</td>
<td>捕获当前 Canvas 的视觉快照</td>
<td>记录状态、生成截图反馈</td>
</tr>
</tbody>
</table>
<p>其中 canvas.eval 的安全定位需要特别说明。它被归类为 <span style="background-color: #c0c0c0;">Operator 控制面</span>（Operator Control Surface）— 意味着其安全性由部署运营方（Operator）负责，而非 OpenClaw 平台本身。Agent 通过 canvas.eval 可以执行任意 JavaScript 代码，这赋予了极大的灵活性，但也意味着 Operator 必须在自己的部署环境中建立相应的安全防线。</p>
<div class="blog_h2"><span class="graybg">14.3 Canvas 与节点模式</span></div>
<p>在多端架构中，Canvas 操作通过节点模式暴露。所有 canvas.* 调用都被路由为 node.invoke 指令发送到对应的端侧节点执行。这意味着 Agent 可以指定在特定设备（例如用户的 iPad 或 Android 手机）上渲染画布内容，实现跨设备的视觉工作流编排。</p>
<p>三个平台对 Canvas 的渲染实现各有差异：macOS 使用 WebKit 视图，iOS 使用 SwiftUI 原生视图层结合 WebKit 渲染，Android 使用 Android WebView。但上层的 A2UI 协议保证了 Agent 无需关心底层渲染差异。</p>
<div class="blog_h2"><span class="graybg">A2UI 构建管线与交叉编译</span></div>
<p>A2UI 的 bundle 文件位于 src/canvas-host/a2ui/a2ui.bundle.js，其哈希值记录在 src/canvas-host/a2ui/.bundle.hash（自动生成，不应手动编辑）。构建命令为 pnpm canvas:a2ui:bundle 或 scripts/bundle-a2ui.sh。在 pnpm build 的完整构建管线中，A2UI bundle 作为第一步执行。</p>
<p>交叉编译是一个已知痛点。在 Apple Silicon 上构建 amd64 镜像时，A2UI bundle 可能因 QEMU 模拟环境的限制而失败。Dockerfile 对此做了优雅降级处理：bundle 失败时创建一个 stub 文件（包含注释 /* A2UI bundle unavailable in this build */），并同时清理 vendor/a2ui 和 apps/shared/OpenClawKit/Tools/CanvasA2UI 目录，确保构建不会因 A2UI 不可用而中断。CI 构建在原生架构上执行，因此不受此影响。</p>
<div class="blog_h2"><span class="graybg">Canvas 的安全定位</span></div>
<p>Canvas 的安全定位是理解其设计边界的关键。canvas.eval 允许 Agent 在 Canvas 中执行任意 JavaScript 代码，这在功能上等同于一个浏览器端的 eval()。OpenClaw 将此明确归类为 <span style="background-color: #c0c0c0;">Operator 控制面</span>——与浏览器自动化工具中的脚本执行类似，安全责任在于部署者而非平台。这种定位与 OpenClaw 的单用户、本地优先架构一致：在用户自己的设备上，Agent 本就拥有与用户相当的权限。但在多租户或公开部署场景中，Operator 必须评估 Canvas eval 带来的风险并做出适当限制。</p>
<p><!-- Chapter 15 --></p>
<div class="blog_h1"><span class="graybg">Lobster</span></div>
<p><span style="background-color: #c0c0c0;">Lobster</span> 是 OpenClaw 生态中的工作流编排 Shell，独立仓库位于 openclaw/lobster（992 stars）。其定位口号是 "OpenClaw native workflow Shell" — 一个为 OpenClaw 原生设计的工作流执行环境。</p>
<div class="blog_h2"><span class="graybg">15.1 类型化 JSON 管线</span></div>
<p>Lobster 的核心抽象是<span style="background-color: #c0c0c0;">类型化 JSON 管线</span>（Typed JSON Pipelines）。与 Unix shell 的文本管道不同，Lobster 管线中流动的数据是带有类型约束的 JSON 结构。每个管线步骤声明其输入 schema 和输出 schema，Lobster 在管线组装阶段即可进行类型检查，而非等到运行时才暴露类型不匹配的问题。</p>
<p>管线架构是可组合的（Composable Pipeline Architecture）：开发者可以将 OpenClaw 的 Skills 和 Tools 链接为多步骤工作流。每个步骤可以是一个 Skill 调用、一个 Tool 执行、一段自定义逻辑，或者一个嵌套的子管线。</p>
<p>关键的流控机制是<span style="background-color: #c0c0c0;">审批门</span>（Approval Gates）。管线中可以在任意步骤之间插入审批点，执行会暂停并等待指定审批人（人类或其他 Agent）的确认后才继续推进。这对于涉及敏感操作的自动化流程至关重要 — 例如在部署管线中，代码编译步骤自动执行，但推送至生产环境之前需要人工审批。</p>
<div class="blog_h2"><span class="graybg">15.2 终端色彩调色板</span></div>
<p>Lobster 对终端输出的视觉一致性有严格要求。色彩定义集中在 src/terminal/palette.ts 模块，该模块导出一套共享的终端颜色调色板（Terminal Color Palette）。所有面向终端的输出 — 包括 onboarding 引导流程、配置提示（config prompts）与 TTY UI 输出 — 都必须引用调色板中定义的颜色常量，严禁在代码中硬编码颜色值。</p>
<pre class="crayon-plain-tag">// src/terminal/palette.ts
export const palette = {
  primary: chalk.hex('#5B8DEF'),
  success: chalk.hex('#6BCB77'),
  warning: chalk.hex('#FFD93D'),
  error:   chalk.hex('#FF6B6B'),
  muted:   chalk.gray,
  highlight: chalk.bold.white,
  // ... 更多颜色定义
} as const;</pre>
<p>这一设计确保了 Lobster 在不同终端模拟器和配色方案下的视觉一致性，同时简化了主题定制。</p>
<div class="blog_h2"><span class="graybg">15.3 Caclawphony：自主执行编排</span></div>
<p><span style="background-color: #c0c0c0;">Caclawphony</span>（仓库 openclaw/caclawphony，34 stars）是构建在 Lobster 之上的 Symphony 系统。它的核心能力是将项目级任务分解为相互隔离的自主执行单元（Isolated Autonomous Execution Runs）。每个执行单元拥有独立的上下文、工具集与沙箱环境，多个单元可以并行运行。</p>
<p>Caclawphony 适用于大规模项目重构、批量代码迁移等需要将工作分而治之的场景。项目经理（人类或 Agent）在顶层定义任务分解策略，Caclawphony 负责将其转化为可并行执行的 Lobster 管线集合。</p>
<p>Caclawphony 与 OpenClaw 主仓库的 Session 工具形成互补：sessions_send 提供 Agent 间的点对点通信，而 Caclawphony 提供的是任务级的编排框架——它关心的是"哪些工作单元需要并行/串行执行"，而非"Agent A 如何向 Agent B 发消息"。两者结合，构成了从单次对话到复杂项目执行的完整 Agent 协作栈。</p>
<div class="blog_h2"><span class="graybg">Lobster 的设计哲学</span></div>
<p>Lobster 的名称选择（龙虾）并非随意。在 OpenClaw 的概念体系中，龙虾象征着两个工程理念：其一，龙虾的螯足（claw）代表工具——Lobster 管线中的每个步骤都是一个可独立运行的工具调用；其二，龙虾的蜕壳（molt）代表版本演进——管线可以在保持外部接口不变的情况下替换内部实现。</p>
<p>Lobster 管线与 Unix 管道的另一个关键区别在于<strong>错误处理语义</strong>。Unix 管道中，上游命令的非零退出码可以被下游忽略（除非设置了 set -o pipefail）。Lobster 管线的每个步骤都必须显式声明其错误处理策略：fail-fast（任何错误立即终止整条管线）、retry（按指数退避重试）、skip（记录错误但继续执行）或 fallback（切换到备选步骤）。这种显式的错误处理语义使得 Lobster 管线在可靠性上远超 Shell 脚本。</p>
<div class="blog_h2"><span class="graybg">15.4 Lobster 与 Cron 的关系</span></div>
<p>Lobster 管线可以与 OpenClaw 的 Cron 调度系统结合，实现定时自动化。Cron 负责触发时机，Lobster 负责执行逻辑。典型应用包括：每日凌晨执行代码质量扫描管线、每周生成项目状态报告管线、在特定事件触发后延迟执行清理管线等。Cron 触发器将 Lobster 管线 ID 作为执行载荷传递，Gateway 的调度器负责在指定时间实例化管线并开始执行。</p>
<div class="blog_h1"><span class="graybg">Web UI 与浏览器控制</span></div>
<div class="blog_h2"><span class="graybg">Control UI：Lit 3 + Vite</span></div>
<p>OpenClaw 的 Web 管理界面——<span style="background-color: #c0c0c0;">Control UI</span>——直接由 Gateway 进程托管和分发，不需要独立的前端服务器。UI 源码位于 ui/ 目录，使用 <span style="background-color: #c0c0c0;">Lit 3</span>（Google 的 Web Components 库）构建，以 <span style="background-color: #c0c0c0;">Vite</span> 作为开发服务器和构建工具。构建命令为 pnpm ui:build，产出物嵌入到 Gateway 的静态资源路径中。</p>
<p>选择 Lit 而非 React/Vue/Svelte 体现了 OpenClaw 的工程偏好：Lit 基于 Web Components 标准，无需虚拟 DOM 运行时，产出的 bundle 体积极小，且与 Gateway 的原生 HTTP 服务天然兼容。Control UI 的功能覆盖会话管理、渠道状态监控、配置编辑、Skills 管理和 Agent 交互。UI 构建系统支持信号（Signals）响应式模式，通过 @lit-labs/signals@0.2.0 和 signal-utils@0.21.1 实现细粒度的 UI 更新。</p>
<p>UI 还有独立的测试流水线 pnpm test:ui，以及专门的 lint 规则 lint:ui:no-raw-window-open 防止在 UI 代码中使用原始的 window.open()（应使用框架提供的安全包装）。</p>
<div class="blog_h2"><span class="graybg">WebChat：基于 Gateway WebSocket 的对话界面</span></div>
<p><span style="background-color: #c0c0c0;">WebChat</span>（详见 <a href="https://docs.openclaw.ai/web/webchat">docs.openclaw.ai/web/webchat</a>）是 Control UI 中内嵌的对话界面，直接使用 Gateway 的 WebSocket 连接——无需独立的 WebChat 端口或额外配置。安装完 Gateway 后，用户在浏览器中访问 http://localhost:18789 即可开始与 Agent 对话。</p>
<p>WebChat 同时也是 macOS App 的内嵌 Web 视图，通过 macOS 的 WebKit 视图直接加载。这种架构复用确保了 Web 端和 macOS 端的对话体验一致。</p>
<div class="blog_h2"><span class="graybg">浏览器控制工具：Playwright + 专属 Chromium</span></div>
<p>OpenClaw 的浏览器控制工具（详见 <a href="https://docs.openclaw.ai/tools/browser">docs.openclaw.ai/tools/browser</a>）是核心工具体系中最复杂的模块之一。它使用 playwright-core@1.58.2 通过 CDP（Chrome DevTools Protocol）控制一个专属的 Chromium 实例——不是用户的日常浏览器，而是 OpenClaw 管理的独立实例，拥有独立的浏览器配置文件（Profile）。</p>
<p>浏览器控制的核心能力包括：</p>
<ul>
<li><strong>页面快照（Snapshots）</strong>：捕获页面的 DOM 状态和视觉渲染，供 Agent 分析页面内容</li>
<li><strong>结构化操作（Actions）</strong>：点击、填写表单、滚动、导航——Agent 通过结构化指令驱动浏览器，而非注入自由 JavaScript</li>
<li><strong>文件上传</strong>：Agent 可以指示浏览器在文件选择器中上传指定文件</li>
<li><strong>多 Profile 隔离</strong>：不同任务可以使用不同的浏览器配置文件，保持 Cookie 和登录状态的隔离</li>
</ul>
<p>浏览器工具的配置通过 JSON 声明：</p>
<pre class="crayon-plain-tag">{
  "browser": {
    "enabled": true,
    "color": "#FF4500"
  }
}</pre>
<p>color 参数控制浏览器窗口的标题栏颜色——这是一个细节设计，当 Agent 控制的浏览器窗口出现在屏幕上时，用户可以通过颜色快速区分它与自己的日常浏览器。</p>
<p>Docker 镜像构建时，可以通过 --build-arg OPENCLAW_INSTALL_BROWSER=1 预安装 Chromium 和 Xvfb（X Virtual Frame Buffer），增加约 300MB 镜像体积，但省去了每次容器启动时 60-90 秒的 Playwright 安装时间。这对 CI/CD 场景尤其重要。</p>
<div class="blog_h2"><span class="graybg">工具体系概览</span></div>
<p>OpenClaw 的<span style="background-color: #c0c0c0;">一等工具（First-class Tools）</span>是平台核心能力的直接延伸，区别于通过 Skills 或 MCP 接入的第三方工具。一等工具直接集成在 Gateway 和 Agent 运行时中，享有完整的安全策略和沙箱支持：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">工具</td>
<td style="text-align: center;">能力</td>
<td style="text-align: center;">文档</td>
</tr>
</thead>
<tbody>
<tr>
<td><span style="background-color: #c0c0c0;">Browser</span></td>
<td>专属 Chromium 控制、CDP 快照、结构化操作、文件上传</td>
<td><a href="https://docs.openclaw.ai/tools/browser">docs.openclaw.ai/tools/browser</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Canvas</span></td>
<td>A2UI 驱动的可视化工作区（push/reset/eval/snapshot）</td>
<td><a href="https://docs.openclaw.ai/platforms/mac/canvas">docs.openclaw.ai/platforms/mac/canvas</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Nodes</span></td>
<td>设备端操作：camera snap/clip、screen record、location.get、notifications</td>
<td><a href="https://docs.openclaw.ai/nodes">docs.openclaw.ai/nodes</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Cron</span></td>
<td>定时任务调度与自动化触发</td>
<td><a href="https://docs.openclaw.ai/automation/cron-jobs">docs.openclaw.ai/automation/cron-jobs</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Sessions</span></td>
<td>sessions_list / sessions_history / sessions_send（Agent 间通信）</td>
<td><a href="https://docs.openclaw.ai/concepts/session-tool">docs.openclaw.ai/concepts/session-tool</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Webhooks</span></td>
<td>接收外部 HTTP 回调并触发 Agent 处理</td>
<td><a href="https://docs.openclaw.ai/automation/webhook">docs.openclaw.ai/automation/webhook</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Gmail Pub/Sub</span></td>
<td>Gmail 邮件到达事件驱动</td>
<td><a href="https://docs.openclaw.ai/automation/gmail-pubsub">docs.openclaw.ai/automation/gmail-pubsub</a></td>
</tr>
<tr>
<td><span style="background-color: #c0c0c0;">Discord/Slack Actions</span></td>
<td>平台原生交互（斜杠命令、按钮、下拉菜单）</td>
<td>渠道文档内嵌</td>
</tr>
</tbody>
</table>
<p>在沙箱模式下，工具的可用性受到严格约束。非主会话（non-main sessions）的 Docker 沙箱中，默认<strong>允许</strong>的工具包括 bash、process、read、write、edit 以及 sessions 系列；默认<strong>禁止</strong>的工具包括 browser、canvas、nodes、cron、discord、gateway。这种白名单+黑名单的双层控制确保了多租户场景下的安全隔离。</p>
<p><!-- Chapter 16 --></p>
<div class="blog_h1"><span class="graybg">安全模型</span></div>
<p>OpenClaw 的安全模型覆盖了从消息入口到执行环境的完整链路。本章从 DM（Direct Message）配对的接入控制讲起，经过沙箱隔离的执行边界，到安全基础设施与凭证管理，系统性地拆解 OpenClaw 的安全架构。</p>
<div class="blog_h2"><span class="graybg">16.1 DM 配对与接入控制</span></div>
<p>OpenClaw 的默认 DM 安全策略（dmPolicy）设置为 <span style="background-color: #c0c0c0;">"pairing"</span>。在此模式下，任何未知发送者发起的 DM 会话都会收到一个配对码（Pairing Code），用户需要在服务器端通过 CLI 确认才能建立信任关系。</p>
<p>三种 DM 策略模式的对比：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">模式</td>
<td style="text-align: center;">行为</td>
<td style="text-align: center;">安全级别</td>
</tr>
</thead>
<tbody>
<tr>
<td>pairing（默认）</td>
<td>未知发送者收到配对码，需管理员批准</td>
<td>高</td>
</tr>
<tr>
<td>allowlist</td>
<td>仅白名单中的用户可发起 DM</td>
<td>高</td>
</tr>
<tr>
<td>open</td>
<td>接受所有 DM（需同时配置 allowFrom: "*"）</td>
<td>低</td>
</tr>
</tbody>
</table>
<p>配对审批通过 CLI 命令完成：</p>
<pre class="crayon-plain-tag">openclaw pairing approve  <code></pre>
<p>每个渠道的允许列表通过 allowFrom 字段独立配置。例如 channels.telegram.allowFrom、channels.discord.allowFrom 分别控制 Telegram 和 Discord 渠道的准入名单。</p>
<p>公开 DM 访问需要双重显式授权：dmPolicy="open" 和 allowFrom 数组中包含 "*" 通配符。仅设置其中一项不会开放公开访问 — 这是一项有意为之的双重门控设计，防止配置笔误导致意外暴露。</p>
<p>openclaw doctor 会主动检测并告警风险或错误的 DM 策略配置，包括但不限于：open 模式下缺少 allowFrom 通配符、allowlist 模式下白名单为空等异常情况。</p>
<p>历史遗留的配置键名 channels.discord.dm.policy 已迁移为 channels.discord.dmPolicy。旧格式在当前版本仍可被识别，但会触发弃用警告。</p>
<div class="blog_h2"><span class="graybg">16.2 沙箱隔离</span></div>
<p>OpenClaw 的沙箱策略通过 agents.defaults.sandbox.mode 配置。推荐的默认值为 <span style="background-color: #c0c0c0;">"non-main"</span>，意味着非主会话（群组会话、频道会话等）自动进入沙箱隔离环境。</p>
<p>这一设计基于 OpenClaw 的<span style="background-color: #c0c0c0;">单用户设计假设</span>：主会话（Main Session）的操作者是服务的所有者，拥有完整的宿主机访问权限，工具在宿主机上直接执行。而非主会话来自外部用户，每个会话在独立的 Docker 沙箱容器中执行，彼此以及与宿主机之间完全隔离。</p>
<p>沙箱内的工具可用性由白名单和黑名单双重控制：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">类别</td>
<td style="text-align: center;">工具列表</td>
</tr>
</thead>
<tbody>
<tr>
<td>沙箱白名单（允许使用）</td>
<td>bash, process, read, write, edit, sessions_list, sessions_history, sessions_send, sessions_spawn</td>
</tr>
<tr>
<td>沙箱黑名单（禁止使用）</td>
<td>browser, canvas, nodes, cron, discord, gateway</td>
</tr>
</tbody>
</table>
<p>黑名单中的工具若在沙箱会话中被调用，将返回明确的权限拒绝错误而非静默忽略。</p>
<div class="blog_h2"><span class="graybg">16.3 安全基础设施</span></div>
<p>OpenClaw 的安全策略文档维护在独立仓库 openclaw/trust（35 stars），对外发布于 trust.openclaw.ai。该仓库包含完整的威胁模型（Threat Model）文档。安全漏洞报告通过 security@openclaw.ai 接收。</p>
<div class="blog_h3"><span class="graybg">16.3.1 SSRF 防护</span></div>
<p>OpenClaw Plugin SDK 导出 ssrf-runtime 模块，供插件在发起网络请求时使用。该模块会校验目标地址，阻止对内网地址（RFC 1918）、环回地址、链路本地地址以及云元数据端点的访问，从而防范 SSRF（Server-Side Request Forgery，服务端请求伪造）攻击。所有插件的网络调用都应通过此模块路由，而非直接使用 fetch 或 http 模块。</p>
<div class="blog_h3"><span class="graybg">16.3.2 Prompt Injection 的立场</span></div>
<p>OpenClaw 将 Prompt Injection（提示注入）正式声明为<span style="background-color: #c0c0c0;">范围外</span>（Out of Scope）— 不将其视为安全漏洞。这一立场基于现实考量：当前 LLM 架构下不存在可靠的 prompt injection 防御手段，将其纳入漏洞范围只会制造虚假的安全承诺。相应地，canvas.eval 和浏览器脚本执行均被归类为 Operator 控制面，安全边界由部署方自行划定。</p>
<div class="blog_h2"><span class="graybg">16.4 插件安装安全</span></div>
<p>插件安装流程的安全管控在 v2026.3.28 至 v2026.3.31 之间经历了显著强化。</p>
<p>安装流程中的 before_install 钩子为安全扫描器提供了集成点。任何外部安全扫描工具都可以注册为 before_install 处理器，在插件代码落盘前进行检查。</p>
<p><span style="background-color: #c0c0c0;">v2026.3.31 破坏性变更</span>：内置的危险代码检测器现在对 "critical" 级别的发现默认执行<span style="background-color: #c0c0c0;">关闭失败</span>（Fail Closed）策略。此前，critical 级别的发现仅生成警告，管理员可以选择忽略。新策略下，标记为 critical 的发现会直接阻止安装。如需强制安装已标记的插件，必须使用显式的覆盖参数：</p>
<pre class="crayon-plain-tag">openclaw plugin install  --dangerously-force-unsafe-install</pre>
<p>该参数名称的冗长是有意为之 — 让每次使用都足够刻意，避免误操作。Skills 安装和 Plugins 安装都受到相同的扫描门控约束。</p>
<div class="blog_h2"><span class="graybg">16.5 Gateway 认证强化（v2026.3.31）</span></div>
<p>v2026.3.31 对 Gateway 的认证机制进行了多项收紧：</p>
<p><span style="background-color: #c0c0c0;">trusted-proxy 模式</span>拒绝混合共享令牌配置（Mixed Shared-token Configs）。如果检测到多个服务共用同一认证令牌，Gateway 将拒绝启动并报告配置冲突。</p>
<p><span style="background-color: #c0c0c0;">local-direct 回退模式</span>现在要求显式配置令牌。此前，同一主机上的连接可以隐式获得认证（Implicit Same-host Auth），这在多租户部署场景下存在风险。新版本移除了这一隐式信任，所有连接都必须提供有效令牌。</p>
<p>节点配对审批（Node Pairing Approval）成为强制前提 — 节点命令直到配对审批完成后才暴露。节点发起的运行（Node-originated Runs）被限制在缩减的可信执行面上。</p>
<div class="blog_h2"><span class="graybg">16.6 凭证管理</span></div>
<p>OpenClaw 的凭证统一存储于 ~/.openclaw/credentials/ 目录。Web 服务提供商的凭证刷新通过 openclaw login 命令重新执行 OAuth 流程。</p>
<p>Provider 插件中的密钥引用使用 <span style="background-color: #c0c0c0;">SecretRef</span> 语义 — 配置文件中仅存储密钥的引用标识符而非明文值，运行时由凭证管理器解析为实际密钥。这一设计确保配置文件可以安全地纳入版本控制。</p>
<p>关于内容安全的基本原则：永远不要提交真实的电话号码、视频文件或生产环境配置值到代码仓库中。</p>
<p><!-- Chapter 17 --></p>
<div class="blog_h1"><span class="graybg">构建与测试</span></div>
<p>OpenClaw 的构建与测试基础设施是其工程纪律的集中体现。本章逐一拆解构建工具链的选型、198 个 npm 脚本的分类、测试基础设施的架构设计以及代码质量门控的实施细节。</p>
<div class="blog_h2"><span class="graybg">17.1 构建工具链</span></div>
<p>OpenClaw 的构建工具选型刻意回避了主流的 webpack/rollup/esbuild 全家桶模式，转而采用一套更专注的工具组合：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">工具</td>
<td style="text-align: center;">版本</td>
<td style="text-align: center;">职责</td>
</tr>
</thead>
<tbody>
<tr>
<td>tsdown</td>
<td>0.21.7</td>
<td>打包器（bundler），通过 scripts/tsdown-build.mjs 驱动</td>
</tr>
<tr>
<td>TypeScript</td>
<td>6.0.2</td>
<td>类型检查</td>
</tr>
<tr>
<td>@typescript/native-preview</td>
<td>7.0.0-dev.20260331.1</td>
<td>Go 实现的 TypeScript 编译器预览版（pnpm tsgo）</td>
</tr>
<tr>
<td>oxfmt</td>
<td>0.43.0</td>
<td>代码格式化（取代 Prettier）</td>
</tr>
<tr>
<td>oxlint + oxlint-tsgolint</td>
<td>1.58.0 / 0.18.1</td>
<td>代码检查（取代 ESLint）</td>
</tr>
<tr>
<td>Bun</td>
<td>-</td>
<td>开发/测试阶段的 TypeScript 执行器</td>
</tr>
<tr>
<td>Node 22+</td>
<td>-</td>
<td>生产运行时（保持 Node + Bun 双路径兼容）</td>
</tr>
<tr>
<td>tsx</td>
<td>4.21.0</td>
<td>基于 Node 的 TypeScript 执行</td>
</tr>
<tr>
<td>jiti</td>
<td>2.6.1</td>
<td>运行时 ESM 解析（plugin-sdk 别名解析）</td>
</tr>
</tbody>
</table>
<p>几点选型要点值得展开：</p>
<p><span style="background-color: #c0c0c0;">tsdown 而非直接使用 esbuild</span>：tsdown 在 esbuild 之上提供了更高层的打包抽象，其配置文件比直接编写 esbuild 插件更简洁。构建入口是 scripts/tsdown-build.mjs。</p>
<p><span style="background-color: #c0c0c0;">@typescript/native-preview</span>：这是 TypeScript 官方的 Go 语言重写实验版本，通过 pnpm tsgo 调用。其类型检查速度比标准 TypeScript 编译器快一个数量级，OpenClaw 将其用于 CI 中的快速类型检查路径。</p>
<p><span style="background-color: #c0c0c0;">oxfmt / oxlint</span>：基于 Rust 的格式化和检查工具链，替代了传统的 Prettier + ESLint 组合。格式化命令为 pnpm format（检查）和 pnpm format:fix（自动修复），检查命令为 pnpm lint。</p>
<p><span style="background-color: #c0c0c0;">Bun + Node 双运行时</span>：开发和测试阶段使用 Bun 获得更快的启动速度（bun 、bunx ），生产部署使用 Node 22+ 确保兼容性。两条路径必须同时保持可用。</p>
<div class="blog_h3"><span class="graybg">17.1.1 构建管线与变体</span></div>
<p>完整构建管线：</p>
<pre class="crayon-plain-tag">pnpm build
# 展开为：
# 1. pnpm canvas:a2ui:bundle    → A2UI bundle 构建
# 2. scripts/tsdown-build.mjs   → 主体打包
# 3. runtime-postbuild.mjs      → 运行时后处理</pre>
<p>三种构建变体服务于不同场景：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">变体</td>
<td style="text-align: center;">命令</td>
<td style="text-align: center;">用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>完整构建</td>
<td>pnpm build</td>
<td>包含 A2UI bundle + 主体 + 后处理</td>
</tr>
<tr>
<td>Docker 构建</td>
<td>pnpm build:docker</td>
<td>跳过 A2UI bundle（可能在 QEMU 下失败）</td>
</tr>
<tr>
<td>严格冒烟测试</td>
<td>pnpm build:strict-smoke</td>
<td>快速验证构建产物的基本可用性</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">17.2 198 个 npm 脚本分类</span></div>
<p>OpenClaw 的 package.json 包含 <span style="background-color: #c0c0c0;">198 个 npm 脚本</span>。这个数字并非膨胀 — 它反映了一个覆盖多平台、多渠道、多插件的工程体系所需的自动化指令密度。以下按职责分类：</p>
<div class="blog_h3"><span class="graybg">17.2.1 构建类</span></div>
<p>build、build:docker、build:plugin-sdk:dts、build:strict-smoke — 核心构建管线及其变体，加上 Plugin SDK 的类型声明生成。</p>
<div class="blog_h3"><span class="graybg">17.2.2 检查与 Lint 类（30+）</span></div>
<p>以 pnpm check 作为元检查入口，编排 tsgo、lint、format、format:check 及约 20 个特定规则检查脚本。详见 17.6 节的 CI 架构分析。</p>
<div class="blog_h3"><span class="graybg">17.2.3 测试类（40+）</span></div>
<p>测试脚本是数量最多的一组：test、test:fast、test:watch、test:coverage、test:e2e、test:live、test:gateway、test:channels、test:extensions、test:contracts，以及一系列 test:docker:* 和 test:parallels:* 脚本。</p>
<div class="blog_h3"><span class="graybg">17.2.4 发布类</span></div>
<p>release:check、release:openclaw:npm:check、release:plugins:npm:check — 发布前的版本号、changelog、npm registry 一致性校验。</p>
<div class="blog_h3"><span class="graybg">17.2.5 平台类</span></div>
<p>android:*、ios:*、ui:* — 各平台的构建、测试、lint 快捷入口。</p>
<div class="blog_h3"><span class="graybg">17.2.6 文档类</span></div>
<p>docs:check-links（死链检测）、docs:spellcheck（拼写检查）、docs:check-i18n-glossary（国际化术语表一致性）。</p>
<div class="blog_h3"><span class="graybg">17.2.7 协议类</span></div>
<p>protocol:check（协议定义一致性检查）、protocol:gen（生成 TypeScript 类型）、protocol:gen:swift（生成 Swift 类型）。</p>
<div class="blog_h2"><span class="graybg">17.3 测试基础设施</span></div>
<p>OpenClaw 使用 <span style="background-color: #c0c0c0;">Vitest 4.1.2</span> 作为测试框架，搭配 @vitest/coverage-v8 进行 V8 引擎级别的代码覆盖率采集。覆盖率门槛统一设置为 <span style="background-color: #c0c0c0;">70%</span>，覆盖 lines、branches、functions、statements 四个维度。</p>
<p>一条关键的强制规则：Vitest 的并发模式<span style="background-color: #c0c0c0;">只允许使用 forks 池</span>。threads、vmThreads、vmForks 三种模式被显式禁用。这一限制源于 OpenClaw 测试中大量涉及进程级副作用（子进程创建、文件系统操作、网络端口占用等），线程级隔离无法提供足够的隔离保障。</p>
<p>并行测试编排由 test-parallel.mjs 脚本驱动，提供三种执行配置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">配置</td>
<td style="text-align: center;">并行度</td>
<td style="text-align: center;">用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>default</td>
<td>CPU 核心数的 50%</td>
<td>日常开发，平衡速度与系统响应性</td>
</tr>
<tr>
<td>serial</td>
<td>1</td>
<td>调试失败用例，排除并发干扰</td>
</tr>
<tr>
<td>max</td>
<td>CPU 核心数的 100%</td>
<td>CI 环境，最大化吞吐</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">17.3.1 测试层级</span></div>
<p>测试体系覆盖多个层级，每个层级关注不同粒度的验证需求：</p>
<p><span style="background-color: #c0c0c0;">单元测试与集成测试</span>：pnpm test（全量运行）、pnpm test:fast（排除慢速用例）、pnpm test:watch（文件监听模式）、pnpm test:coverage（带覆盖率报告）。</p>
<p><span style="background-color: #c0c0c0;">领域测试</span>：test:channels（渠道集成）、test:extensions（扩展接口）、test:gateway（Gateway 协议）、test:e2e（端到端流程）、test:live（真实 API 对接）。</p>
<p><span style="background-color: #c0c0c0;">契约测试</span>（Contract Tests）：test:contracts:channels 和 test:contracts:plugins 分别强制渠道和插件的接口契约一致性。契约测试确保 Channel 适配器和 Plugin 遵循其声明的接口协议，防止实现漂移。</p>
<p><span style="background-color: #c0c0c0;">Docker E2E 测试</span>（8+ 场景）：在完整的 Docker 容器化环境中执行端到端验证。覆盖场景包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">场景</td>
<td style="text-align: center;">验证范围</td>
</tr>
</thead>
<tbody>
<tr>
<td>onboard</td>
<td>首次启动引导流程</td>
</tr>
<tr>
<td>plugins</td>
<td>插件安装、加载与执行</td>
</tr>
<tr>
<td>MCP channels</td>
<td>MCP 协议渠道连通性</td>
</tr>
<tr>
<td>gateway network</td>
<td>Gateway 网络拓扑与路由</td>
</tr>
<tr>
<td>OpenWebUI</td>
<td>OpenWebUI 集成</td>
</tr>
<tr>
<td>doctor-switch</td>
<td>doctor 诊断与配置切换</td>
</tr>
<tr>
<td>qr-import</td>
<td>QR 码配置导入</td>
</tr>
<tr>
<td>live models</td>
<td>真实模型端点对接</td>
</tr>
</tbody>
</table>
<p><span style="background-color: #c0c0c0;">Parallels 冒烟测试</span>：在 macOS、Windows、Linux 三个虚拟机客户端上执行冒烟测试，验证跨操作系统的基本功能可用性。</p>
<p><span style="background-color: #c0c0c0;">性能测试套件</span>：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">脚本</td>
<td style="text-align: center;">度量维度</td>
</tr>
</thead>
<tbody>
<tr>
<td>test:perf:budget</td>
<td>性能预算检查（运行时间/内存上限）</td>
</tr>
<tr>
<td>test:perf:hotspots</td>
<td>热点函数 profiling</td>
</tr>
<tr>
<td>test:perf:imports</td>
<td>模块导入耗时分析</td>
</tr>
<tr>
<td>test:startup:bench</td>
<td>启动时间基准</td>
</tr>
<tr>
<td>test:startup:memory</td>
<td>启动内存占用</td>
</tr>
</tbody>
</table>
<p><span style="background-color: #c0c0c0;">Live 测试</span>：通过设置环境变量 OPENCLAW_LIVE_TEST=1 启用，执行 pnpm test:live。这些测试使用真实的 API 密钥调用外部服务，因此不在常规 CI 中运行，而是在专用的 live 测试环境中定期执行。</p>
<div class="blog_h2"><span class="graybg">17.4 代码质量门控</span></div>
<p>OpenClaw 对代码质量的门控措施密度极高，以下逐一展开：</p>
<p><span style="background-color: #c0c0c0;">文件行数限制</span>：check:loc 脚本强制执行约 500-700 行的文件行数上限。超过上限的文件会被标记为需要拆分。这是一项软性但有强制检查的编码指南，目标是防止出现难以维护的巨型文件。</p>
<p><span style="background-color: #c0c0c0;">严格类型纪律</span>：禁止使用 @ts-nocheck，避免使用 any 类型，优先使用 unknown。在对外边界（配置文件、Webhook 载荷、CLI 输出、API 响应）处优先使用 zod@4.3.6 进行运行时 schema 校验。</p>
<p><span style="background-color: #c0c0c0;">动态导入防护</span>：构建系统会检测同一模块同时存在静态导入和动态导入的情况，并发出 INEFFECTIVE_DYNAMIC_IMPORT 警告。这种混用模式会导致 tree-shaking 失效 — 模块已经通过静态导入被打包，动态导入不会带来额外的按需加载收益，反而增加了代码理解的复杂度。</p>
<p><span style="background-color: #c0c0c0;">重复代码检测</span>：使用 jscpd@4.0.8 扫描 src/、extensions/、test/、scripts/ 目录下的代码重复。超过阈值的重复块会触发 CI 失败。</p>
<p><span style="background-color: #c0c0c0;">漂移检测</span>：一系列 check 脚本监控各类定义与实现之间的一致性：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">检查</td>
<td style="text-align: center;">检测内容</td>
</tr>
</thead>
<tbody>
<tr>
<td>canon:check</td>
<td>规范代码样式一致性</td>
</tr>
<tr>
<td>plugin-sdk:api:check</td>
<td>Plugin SDK 公开 API 漂移检测</td>
</tr>
<tr>
<td>config:docs:check</td>
<td>配置 schema 与文档的一致性</td>
</tr>
<tr>
<td>lint:plugins:plugin-sdk-subpaths-exported</td>
<td>Plugin SDK 子路径导出完整性</td>
</tr>
</tbody>
</table>
<p>此外还有 8 个以上针对特定扩展（Extension）的边界 lint 规则，确保各扩展模块不会越界访问其他扩展的内部 API。</p>
<div class="blog_h2"><span class="graybg">17.5 CI 架构</span></div>
<p>OpenClaw 的 CI 采用<span style="background-color: #c0c0c0;">两层检查体系</span>（Two-tier Check System），将本地开发门控与 CI 门控分离：</p>
<div class="blog_h3"><span class="graybg">17.5.1 第一层：pnpm check（本地开发门控）</span></div>
<p>pnpm check 是每次提交前必须通过的本地检查。其执行顺序为：</p>
<pre class="crayon-plain-tag"># pnpm check 的执行序列：
# 1. no-conflict-markers     → 检测未解决的合并冲突标记
# 2. host-env-policy:swift   → 验证 Swift 宿主环境策略
# 3. tsgo                    → Go 版 TypeScript 类型检查
# 4. lint                    → oxlint 代码检查
# 5. format                  → oxfmt 格式校验</pre>
<p>该管线为串行执行，任何步骤失败都会终止后续步骤并报告错误位置。</p>
<div class="blog_h3"><span class="graybg">17.5.2 第二层：check-additional（CI 专属门控）</span></div>
<p>check-additional 在 CI 环境中额外执行，包含架构策略和边界策略守卫。这些检查被有意排除在本地开发循环之外 — 它们通常运行较慢且依赖 CI 专有环境（如完整的 git 历史、所有分支的 diff 信息等），放入本地循环会严重拖慢开发节奏。</p>
<div class="blog_h3"><span class="graybg">17.5.3 Pre-commit 钩子与快速通道</span></div>
<p>Pre-commit 钩子由 <span style="background-color: #c0c0c0;">prek</span> 工具管理，钩子的默认行为是执行完整的 pnpm check 管线。</p>
<p>对于需要快速迭代的场景，环境变量 FAST_COMMIT=1 可以跳过 format 和 check 步骤：</p>
<pre class="crayon-plain-tag"># 跳过格式和检查（手动保证代码质量时使用）
FAST_COMMIT=1 git commit -m "wip: experimental changes"</pre>
<p>使用 FAST_COMMIT 意味着开发者自行承担代码质量责任 — CI 仍会执行完整检查，不符合要求的提交将在 CI 阶段被拦截。</p>
<div class="blog_h3"><span class="graybg">17.5.4 主分支准入标准</span></div>
<p>代码合入主分支（main）的<span style="background-color: #c0c0c0;">准入门槛</span>（Landing Bar）为：</p>
<pre class="crayon-plain-tag"># 主分支准入三件套：
pnpm check        # 类型 + lint + 格式
pnpm test          # 全量测试
pnpm build         # 构建验证（当变更涉及构建影响面时）</pre>
<p>第三项 pnpm build 是条件性的：仅在变更涉及构建影响面（build-affecting surfaces）时才要求。构建影响面的定义包括但不限于：tsdown-build.mjs 配置变更、package.json 依赖变更、tsconfig.json 变更、新增或删除模块导出等。纯粹的逻辑变更（函数实现调整、bug 修复等）不触发构建要求，以此平衡 CI 速度与安全性。</p>
<div class="blog_h1"><span class="graybg">部署方案</span></div>
<p>OpenClaw 的部署策略覆盖了从单人开发者到企业团队的全部场景。部署方式按复杂度递增排列：npm 全局安装、Docker 容器化、Ansible 编排、Nix 声明式配置、Windows 系统托盘。每种方式对应不同的运维哲学和安全模型。</p>
<div class="blog_h2"><span class="graybg">npm 全局安装（推荐方式）</span></div>
<p>对于大多数开发者而言，npm 全局安装是进入 OpenClaw 最快的路径：</p>
<pre class="crayon-plain-tag">npm install -g openclaw@latest
openclaw onboard --install-daemon</pre>
<p>第一条命令将 OpenClaw CLI 及其全部依赖安装到 Node.js 的全局 node_modules 目录。第二条命令启动交互式引导向导（Onboarding Wizard），--install-daemon 标志指示向导在流程结束后自动注册系统守护进程（Daemon）。</p>
<p>守护进程的注册方式因操作系统而异。在 macOS 上，OpenClaw 生成一个 <span style="background-color: #c0c0c0;">launchd</span> plist 文件并通过 launchctl load 注册为用户级别的 Launch Agent。plist 的关键配置如下：</p>
<pre class="crayon-plain-tag">Label
ai.openclaw.gateway
ProgramArguments

  /usr/local/bin/node
  /usr/local/lib/node_modules/openclaw/dist/gateway.js

RunAtLoad

KeepAlive</pre>
<p>在 Linux 上，OpenClaw 采用 <span style="background-color: #c0c0c0;">systemd 用户服务（user service）</span>，将单元文件写入 ~/.config/systemd/user/openclaw-gateway.service，并通过 systemctl --user enable --now openclaw-gateway 启动。这一选择的关键在于"用户级别"——无需 root 权限，服务生命周期与用户会话绑定，遵循最小权限原则。配合 loginctl enable-linger 可以实现即使用户未登录也保持运行。</p>
<div class="blog_h2"><span class="graybg">Docker 部署：四阶段构建深度剖析</span></div>
<p>Docker 部署是 OpenClaw 为生产环境和隔离场景提供的首选方案。其 Dockerfile 采用<span style="background-color: #c0c0c0;">多阶段构建（Multi-Stage Build）</span>模式，共分四个阶段，每个阶段都经过精心设计以最小化最终镜像体积。</p>
<div class="blog_h3"><span class="graybg">阶段一：ext-deps — 扩展依赖提取</span></div>
<p>第一阶段的唯一职责是从 extensions/ 目录树中提取所有 package.json 文件，同时保留其目录结构。这是一个纯文件复制阶段，不执行任何安装操作：</p>
<pre class="crayon-plain-tag">FROM node:24-bookworm AS ext-deps
WORKDIR /app
COPY extensions/ extensions/
RUN find extensions -name "package.json" -exec sh -c \
  'mkdir -p /out/$(dirname {}) &amp;&amp; cp {} /out/{}' \;</pre>
<p>这种分离的目的是利用 Docker 的层缓存机制——只有当扩展的 package.json 发生变化时，后续的依赖安装层才会失效。</p>
<div class="blog_h3"><span class="graybg">阶段二：build — 编译构建</span></div>
<p>构建阶段使用 <span style="background-color: #c0c0c0;">Bun</span> 作为 JavaScript 运行时加速依赖安装，同时以 <span style="background-color: #c0c0c0;">pnpm</span> 作为包管理器，<span style="background-color: #c0c0c0;">tsdown</span> 作为 TypeScript 编译工具：</p>
<pre class="crayon-plain-tag">FROM oven/bun:1 AS build
WORKDIR /app
COPY --from=ext-deps /out/extensions ./extensions
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
RUN cd ui &amp;&amp; pnpm run build</pre>
<p>构建产物包括两部分：dist/ 目录下的 TypeScript 编译输出（通过 tsdown 生成），以及 ui/dist/ 目录下的 Web Control UI 静态资源（通过 Vite 构建）。</p>
<div class="blog_h3"><span class="graybg">阶段三：runtime-assets — 运行时资产裁剪</span></div>
<p>第三阶段是整个构建流水线中最关键的体积优化环节：</p>
<pre class="crayon-plain-tag">FROM build AS runtime-assets
RUN pnpm prune --prod
RUN find . -name "*.d.ts" -delete \
 &amp;&amp; find . -name "*.map" -delete \
 &amp;&amp; find . -name "*.ts" ! -name "*.d.ts" -path "*/src/*" -delete</pre>
<p>首先通过 pnpm prune --prod 移除所有 devDependencies，然后逐一清除 TypeScript 声明文件（.d.ts）、Source Map 文件（.map）和源码文件。最终只保留 JavaScript 运行时代码和生产级依赖。</p>
<div class="blog_h3"><span class="graybg">阶段四：runtime — 最终运行镜像</span></div>
<p>最终阶段基于精简的 Node 24 镜像，且所有基础镜像均采用<span style="background-color: #c0c0c0;">固定 SHA256 摘要（Pinned SHA256 Digest）</span>以确保构建可复现：</p>
<pre class="crayon-plain-tag"># 默认变体
FROM node:24-bookworm@sha256:abc123... AS runtime
# 精简变体
# FROM node:24-bookworm-slim@sha256:def456... AS runtime

USER node
WORKDIR /home/node/app
COPY --from=runtime-assets --chown=node:node /app ./

HEALTHCHECK --interval=3m --timeout=10s --start-period=30s \
  CMD curl -f http://localhost:18789/healthz || exit 1

EXPOSE 18789 18790
CMD ["node", "dist/gateway.js"]</pre>
<p>OpenClaw 提供两个镜像变体（Variant），通过构建参数 OPENCLAW_VARIANT 选择：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>变体</td>
<td>基础镜像</td>
<td>特点</td>
<td>适用场景</td>
</tr>
</thead>
<tbody>
<tr>
<td>default</td>
<td>node:24-bookworm</td>
<td>包含完整 Debian 工具链，支持浏览器安装</td>
<td>需要 Playwright/浏览器渠道</td>
</tr>
<tr>
<td>slim</td>
<td>node:24-bookworm-slim</td>
<td>最小化系统库，镜像体积减小约 40%</td>
<td>纯 CLI/API 场景</td>
</tr>
</tbody>
</table>
<p>其余构建参数（Build Args）包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>参数名</td>
<td>默认值</td>
<td>说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>OPENCLAW_EXTENSIONS</td>
<td>all</td>
<td>控制构建哪些扩展，逗号分隔或 all</td>
</tr>
<tr>
<td>OPENCLAW_INSTALL_BROWSER</td>
<td>false</td>
<td>是否在镜像中预装 Chromium（Playwright）</td>
</tr>
<tr>
<td>OPENCLAW_INSTALL_DOCKER_CLI</td>
<td>false</td>
<td>是否安装 Docker CLI（用于沙箱功能）</td>
</tr>
</tbody>
</table>
<p>安全方面，最终镜像以 USER node（uid 1000）运行，杜绝 root 权限。健康检查（Health Check）配置了两个端点：/healthz 用于存活探测（Liveness Probe），/readyz 用于就绪探测（Readiness Probe），检测间隔为 3 分钟，超时 10 秒，启动宽限期 30 秒。</p>
<div class="blog_h3"><span class="graybg">docker-compose.yml：双服务架构</span></div>
<p>官方 docker-compose.yml 定义了两个服务，体现了 OpenClaw 的网关-客户端分离架构：</p>
<pre class="crayon-plain-tag">version: "3.9"
services:
  openclaw-gateway:
    image: ghcr.io/openclaw/openclaw:latest
    ports:
      - "18789:18789"
      - "18790:18790"
    volumes:
      - openclaw-data:/home/node/.openclaw
      - /var/run/docker.sock:/var/run/docker.sock
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - NET_RAW
      - NET_ADMIN
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:18789/healthz"]
      interval: 3m
      timeout: 10s
      start_period: 30s

  openclaw-cli:
    image: ghcr.io/openclaw/openclaw:latest
    command: ["node", "dist/cli.js"]
    depends_on:
      openclaw-gateway:
        condition: service_healthy
    environment:
      - OPENCLAW_GATEWAY_URL=http://openclaw-gateway:18789

volumes:
  openclaw-data:</pre>
<p>安全加固（Security Hardening）体现在三处：no-new-privileges 阻止进程通过 setuid/setgid 提权；cap_drop 丢弃 NET_RAW 和 NET_ADMIN 能力（Capability），防止原始套接字操作和网络配置篡改；Docker Socket 挂载（/var/run/docker.sock）为沙箱集成提供<span style="background-color: #c0c0c0;">Docker-in-Docker（DinD）</span>能力，允许 Agent 在隔离容器中执行命令。</p>
<p>端口映射方面，18789 是 Gateway 的 HTTP 主端口，承载 REST API、WebSocket 连接和 Web Control UI；18790 是 Bridge 端口，供外部渠道插件通过 gRPC 或 WebSocket 桥接到 Gateway。</p>
<div class="blog_h2"><span class="graybg">Ansible 部署：openclaw/openclaw-ansible</span></div>
<p>openclaw/openclaw-ansible（545 stars）提供了一套完整的 Ansible Playbook，将 OpenClaw 的 Docker 部署包装为可重复执行的基础设施代码（Infrastructure as Code）。其核心特性包括：</p>
<p><strong>Tailscale VPN 集成</strong>：Playbook 默认集成 <span style="background-color: #c0c0c0;">Tailscale</span>，Gateway 绑定到 Tailscale 虚拟网络接口而非公网接口。这意味着 OpenClaw 实例只在 Tailnet 内可达，无需暴露任何公网端口，从根本上消除了未授权访问的风险。</p>
<p><strong>UFW 防火墙配置</strong>：自动配置 <span style="background-color: #c0c0c0;">UFW（Uncomplicated Firewall）</span>规则，仅放行 SSH（22）和 Tailscale 所需端口（41641/UDP），其余入站流量全部丢弃。</p>
<p><strong>Docker 隔离</strong>：OpenClaw 运行在独立 Docker 网络中，数据卷映射到宿主机的指定路径，支持通过 Ansible 变量自定义挂载点、环境变量和资源限制。</p>
<div class="blog_h2"><span class="graybg">Nix 部署：openclaw/nix-openclaw</span></div>
<p>openclaw/nix-openclaw（611 stars）提供 Nix Flake 形式的声明式配置。对于 NixOS 用户或使用 home-manager 的开发者，这是最符合其工作流的部署方式：</p>
<pre class="crayon-plain-tag">{
  inputs.openclaw.url = "github:openclaw/nix-openclaw";
  outputs = { self, nixpkgs, openclaw }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        openclaw.nixosModules.default
        {
          services.openclaw = {
            enable = true;
            gateway.port = 18789;
            extensions = [ "discord" "telegram" "whatsapp" ];
          };
        }
      ];
    };
  };
}</pre>
<p>Nix 部署的优势在于完全的可复现性（Reproducibility）——相同的 Flake 输入必然产生相同的系统配置，消除了"在我机器上能跑"的环境差异问题。</p>
<div class="blog_h2"><span class="graybg">Windows 部署：系统托盘与 PowerToys 集成</span></div>
<p>openclaw/openclaw-windows-node（405 stars）为 Windows 提供了原生集成体验。其核心组件包括：</p>
<p><strong>系统托盘伴侣（System Tray Companion）</strong>：一个轻量级的 .NET 应用，驻留在 Windows 系统托盘。它管理 OpenClaw Gateway 进程的生命周期（启动/停止/重启），显示实时状态，并提供快捷菜单访问 Web Control UI 和日志。</p>
<p><strong>PowerToys 命令面板扩展（Command Palette Extension）</strong>：集成 Microsoft PowerToys 的命令面板（Run 插件），用户可以通过 Alt+Space 快捷键直接向 OpenClaw Agent 发送指令，无需切换到浏览器或终端窗口。</p>
<div class="blog_h2"><span class="graybg">远程 Gateway 配置（Linux）</span></div>
<p>在远程 Linux 服务器上部署 Gateway 时，安全暴露服务是核心问题。OpenClaw 提供三种模式：</p>
<p><strong>Tailscale Serve（Tailnet 内部访问）</strong>：通过 tailscale serve 将 Gateway 端口映射到 Tailscale 的 HTTPS 代理，仅 Tailnet 内的设备可访问。配合 Gateway 的 --tailscale serve 模式，自动完成证书配置和端口映射。</p>
<p><strong>Tailscale Funnel（公网 HTTPS 暴露）</strong>：tailscale funnel 模式将服务通过 Tailscale 的全球边缘网络暴露到公网，自动获取 HTTPS 证书。适用于需要外部 Webhook 回调（如 Telegram Bot）的场景。</p>
<p><strong>SSH 隧道</strong>：最传统但最灵活的方式，通过 SSH 端口转发将远程 Gateway 映射到本地：</p>
<pre class="crayon-plain-tag">ssh -L 18789:localhost:18789 -L 18790:localhost:18790 user@remote-host</pre>
<p>Gateway 的绑定模式（Bind Mode）通过 --bind 参数控制：loopback（默认）仅监听 127.0.0.1，lan 监听 0.0.0.0 以接受局域网连接。Tailscale 模式通过 --tailscale 参数设置：off（默认）、serve、funnel。</p>
<div class="blog_h2"><span class="graybg">引导向导：openclaw onboard</span></div>
<p>openclaw onboard 是 OpenClaw 的交互式初始化命令，引导用户完成从零到可用的全部配置。流程按步骤执行：</p>
<p><strong>步骤一：Gateway 配置</strong>——选择绑定模式（loopback/lan）、端口、Tailscale 模式。若检测到已有 Gateway 实例运行，则询问是否复用。</p>
<p><strong>步骤二：工作区配置</strong>——创建默认工作区目录（~/.openclaw/workspace），配置 LLM 提供商密钥（OpenAI API Key、Anthropic API Key 等），设置默认模型。</p>
<p><strong>步骤三：渠道配置</strong>——根据用户选择启用渠道（Discord Bot Token、Telegram Bot Token、WhatsApp 手机号等），验证凭据有效性。</p>
<p><strong>步骤四：Skills 配置</strong>——推荐安装热门 Skills，提示用户浏览 ClawHub 发现更多。</p>
<p>当附带 --install-daemon 标志时，向导结束后自动注册并启动系统守护进程。若出现问题，openclaw doctor 命令执行全面的健康检查：验证 Node.js 版本、检查端口占用、测试 LLM 连接、校验配置文件语法，并输出诊断报告及修复建议。</p>
<div class="blog_h1"><span class="graybg">生态全景</span></div>
<div class="blog_h2"><span class="graybg">ClawHub：官方 Skill 注册中心</span></div>
<p><span style="background-color: #c0c0c0;">ClawHub</span>（openclaw/clawhub，7,214 stars）是 OpenClaw 的官方 Skill 注册中心与分发平台。它的定位类似于 npm 之于 Node.js，或 crates.io 之于 Rust——一个集中式的包注册表，但分发的是 AI Agent 能力模块。</p>
<p>安装 Skill 只需一条命令：</p>
<pre class="crayon-plain-tag">clawhub install weather-forecast
clawhub install code-review --version 2.1.0</pre>
<p>ClawHub 的 Agent 集成能力是其核心差异化特性：当 Agent 在对话中遇到需要但当前未安装的能力时，可以自动搜索 ClawHub 并在用户确认后拉取安装。这一流程的实现路径是 Agent 调用内置的 clawhub_search 工具函数，该函数向 api.clawhub.com/v1/search 发送请求，返回匹配的 Skill 列表及其安全评级。</p>
<p>Web 端的 clawhub.com 提供可视化的市场（Marketplace）界面，支持按类别浏览、按关键词搜索、查看安装统计和社区评分。每个 Skill 页面展示其 SKILL.md 的渲染内容、依赖关系图和版本历史。</p>
<div class="blog_h2"><span class="graybg">Skills 存档与社区列表</span></div>
<p>openclaw/skills（3,622 stars）是所有 Skill 的版本存档仓库，保存了每个 Skill 每个版本的完整快照。这一设计确保即使 Skill 作者删除了某个版本，已部署的实例仍可从存档拉取。</p>
<p>VoltAgent/awesome-openclaw-skills（43,292 stars）是社区维护的精选 Skill 列表，收录超过 5,400 个经过社区验证的 Skill。其 Star 数反映了 OpenClaw Skill 生态的活跃程度——一个"awesome 列表"的 Star 数通常是其生态规模的风向标。</p>
<div class="blog_h2"><span class="graybg">Skills 系统：三层结构</span></div>
<p>OpenClaw 的 Skills 按分发方式分为三个层级：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>层级</td>
<td>存储位置</td>
<td>安装方式</td>
<td>更新策略</td>
</tr>
</thead>
<tbody>
<tr>
<td>内置 Skills（Bundled）</td>
<td>skills/ 目录，随 npm 包分发</td>
<td>自动包含</td>
<td>随 OpenClaw 版本更新</td>
</tr>
<tr>
<td>托管 Skills（Managed）</td>
<td>~/.openclaw/managed/skills/</td>
<td>clawhub install</td>
<td>clawhub update</td>
</tr>
<tr>
<td>工作区 Skills（Workspace）</td>
<td>~/.openclaw/workspace/skills//</td>
<td>手动创建</td>
<td>用户自行管理</td>
</tr>
</tbody>
</table>
<p>加载优先级从上到下递增——工作区 Skills 可以覆盖同名的托管或内置 Skill，提供最大的定制灵活性。</p>
<div class="blog_h2"><span class="graybg">Skill 开发：AgentSkills 规范</span></div>
<p>每个 Skill 由一个 SKILL.md 文件定义，这是 <span style="background-color: #c0c0c0;">AgentSkills 规范</span>的核心载体。SKILL.md 采用带有特殊 YAML Front Matter 的 Markdown 格式：</p>
<pre class="crayon-plain-tag">---
name: code-review
version: 2.1.0
description: Automated code review with multi-language support
triggers:
  - pattern: "review {file_path}"
  - pattern: "check code quality"
permissions:
  - filesystem:read
  - git:read
before_install: scripts/check-deps.sh
---

# Code Review Skill

## Instructions

You are a senior code reviewer. When triggered, analyze the provided
file for bugs, style issues, and potential improvements.

## Tools

### review_file
Analyze a single file and return findings.
- `file_path` (string, required): Path to the file to review
- `severity` (string, optional): Minimum severity to report (info|warn|error)</pre>
<p>其中 before_install 字段指定一个<span style="background-color: #c0c0c0;">安全钩子（Security Hook）</span>脚本，在安装阶段执行。该脚本用于验证系统依赖（如检查 Python 版本、确认特定二进制文件存在），并以非零退出码阻止安装。这一机制的安全意义在于：它为 Skill 作者提供了一个声明式的前置检查点，防止不兼容的 Skill 被安装到不具备运行条件的环境中。</p>
<div class="blog_h2"><span class="graybg">官方子仓库矩阵</span></div>
<p>OpenClaw 组织下维护着一系列功能各异的子仓库：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>仓库</td>
<td>Stars</td>
<td>定位</td>
</tr>
</thead>
<tbody>
<tr>
<td>openclaw/acpx</td>
<td>1,834</td>
<td>无头 ACP CLI（Agent Control Protocol 命令行客户端）</td>
</tr>
<tr>
<td>openclaw/lobster</td>
<td>992</td>
<td>工作流 Shell（Workflow Shell），交互式任务编排</td>
</tr>
<tr>
<td>openclaw/nix-openclaw</td>
<td>611</td>
<td>Nix Flake 声明式部署</td>
</tr>
<tr>
<td>openclaw/openclaw-ansible</td>
<td>545</td>
<td>Ansible Playbook 自动化部署</td>
</tr>
<tr>
<td>openclaw/openclaw-windows-node</td>
<td>405</td>
<td>Windows 系统托盘 + PowerToys 集成</td>
</tr>
<tr>
<td>openclaw/openclaw.ai</td>
<td>250</td>
<td>官方网站源码</td>
</tr>
<tr>
<td>openclaw/community</td>
<td>92</td>
<td>社区治理文档与 Discord 管理策略</td>
</tr>
<tr>
<td>openclaw/trust</td>
<td>35</td>
<td>安全策略、漏洞披露流程、审计报告</td>
</tr>
<tr>
<td>openclaw/caclawphony</td>
<td>34</td>
<td>Symphony 自主运行框架，长时间无人值守 Agent 任务</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">社区竞品与替代方案</span></div>
<p>OpenClaw 并非孤立存在。以下项目在不同维度上与其构成竞争或互补关系：</p>
<p>HKUDS/nanobot（37,216 stars）定位为"超轻量 OpenClaw 替代品"，去除了多渠道和插件系统的复杂性，聚焦于单一 CLI 场景下的极速响应。其核心卖点是亚秒级冷启动和极低内存占用，适合资源受限的边缘设备。</p>
<p>chatgpt-on-wechat/CowAgent（42,673 stars）以微信生态为核心战场，提供企业微信、公众号、小程序等深度集成。在中国市场，微信的渗透率使其成为 AI Agent 的天然入口，CowAgent 在这一垂直领域的覆盖深度超过 OpenClaw。</p>
<p>AstrBot（28,373 stars）是一个 IM 聊天机器人框架，支持 QQ、飞书、钉钉等国内主流即时通讯平台。与 OpenClaw 的平台无关性不同，AstrBot 选择深耕国内生态，提供更贴合国内开发者习惯的 API 设计。</p>
<div class="blog_h2"><span class="graybg">文档与国际化</span></div>
<p>OpenClaw 的文档基于 <span style="background-color: #c0c0c0;">Mintlify</span> 构建，部署在 docs.openclaw.ai。中文本地化版本位于 docs.openclaw.ai/zh-CN。</p>
<p>国际化（i18n）流水线的技术实现值得关注：翻译工作由 scripts/docs-i18n 脚本驱动，该脚本读取 glossary.zh-CN.json 作为术语表（Glossary），确保专有名词翻译一致（例如 "Gateway" 始终翻译为"网关"，"Skill" 保留英文）。翻译记忆（Translation Memory）存储在 zh-CN.tm.jsonl 文件中，采用 JSON Lines 格式，每行一对源文本与译文。该文件的作用类似于传统本地化工具中的 TMX 文件——当源文本发生变化时，i18n 脚本先在翻译记忆中查找完全匹配或模糊匹配，避免重复翻译已有内容。</p>
<p>社区交流的主阵地是 Discord（discord.gg/clawd），这也是获取实时开发进度和与维护者直接交流的主要渠道。</p>
<div class="blog_h1"><span class="graybg">竞品与展望</span></div>
<div class="blog_h2"><span class="graybg">全维度竞品对比</span></div>
<p>以下表格从多个维度对比 OpenClaw 与当前主流 AI Agent 框架：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>维度</td>
<td>OpenClaw</td>
<td>Manus</td>
<td>AutoGen</td>
<td>LangChain</td>
<td>OpenHands</td>
</tr>
</thead>
<tbody>
<tr>
<td>协议</td>
<td>MIT</td>
<td>闭源 SaaS</td>
<td>MIT (CC-BY-4.0 docs)</td>
<td>MIT</td>
<td>MIT</td>
</tr>
<tr>
<td>部署方式</td>
<td>本地优先 + Docker + Ansible</td>
<td>纯云端</td>
<td>本地 / 云端</td>
<td>本地 / 云端</td>
<td>Docker 容器</td>
</tr>
<tr>
<td>主语言</td>
<td>TypeScript</td>
<td>未公开</td>
<td>Python</td>
<td>Python</td>
<td>Python</td>
</tr>
<tr>
<td>多渠道</td>
<td>Discord/Telegram/WhatsApp/Slack/Web/SMS 等 15+</td>
<td>Web 唯一入口</td>
<td>无原生渠道</td>
<td>无原生渠道</td>
<td>Web UI</td>
</tr>
<tr>
<td>语音支持</td>
<td>原生 Realtime API</td>
<td>有</td>
<td>无</td>
<td>社区扩展</td>
<td>无</td>
</tr>
<tr>
<td>插件系统</td>
<td>Skills + 插件 SDK + ClawHub</td>
<td>内置工具</td>
<td>工具注册</td>
<td>工具/链/代理</td>
<td>沙箱工具</td>
</tr>
<tr>
<td>记忆系统</td>
<td>SQLite + 向量 + 知识图谱</td>
<td>云端对话历史</td>
<td>内存状态</td>
<td>多种 Memory 类型</td>
<td>对话历史</td>
</tr>
<tr>
<td>安全模型</td>
<td>三层沙箱 + 权限 DSL + 审计日志</td>
<td>平台托管</td>
<td>无内置沙箱</td>
<td>无内置沙箱</td>
<td>Docker 沙箱</td>
</tr>
<tr>
<td>社区规模</td>
<td>342K stars, 20K+ commits</td>
<td>N/A（闭源）</td>
<td>42K stars</td>
<td>105K stars</td>
<td>55K stars</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">OpenClaw 的差异化定位</span></div>
<p>从对比中可以提炼出 OpenClaw 的三个核心差异化支柱：</p>
<p><strong>本地优先（Local-First）</strong>：所有数据默认存储在用户设备上。Gateway 进程运行在本地或用户控制的服务器上，LLM API 调用从用户设备直接发出，不经过任何第三方中转服务器。这一设计对于受数据合规要求约束的企业用户尤为关键——GDPR、HIPAA 等法规对数据离境有严格限制，本地优先架构天然满足这些要求。</p>
<p><strong>多渠道原生支持（Multi-Channel Native）</strong>：不是通过适配器（Adapter）将单一接口包装成多个渠道，而是在架构层面将渠道（Channel）作为一等公民（First-Class Citizen）。每个渠道有独立的消息格式化器（Formatter）、权限模型和用户身份映射。</p>
<p><strong>MIT 全开放</strong>：不设 Enterprise 版本，不保留核心功能的闭源组件。所有功能对所有用户完全免费。这种策略在商业模式上高度依赖赞助商和社区贡献，但在信任建立上极为有效。</p>
<div class="blog_h2"><span class="graybg">赞助商策略分析</span></div>
<p>OpenAI、NVIDIA 和 Vercel 为何赞助一个本地优先的"竞争者"？这一看似矛盾的赞助关系背后是清晰的战略逻辑：</p>
<p><strong>OpenAI</strong>：OpenClaw 的每一次 Agent 调用都消耗 OpenAI 的 API Token。本地优先≠不用云模型，恰恰相反——OpenClaw 是 OpenAI API 的超级分发渠道。每个 OpenClaw 用户都是潜在的 API 付费用户，且使用频率远高于普通 ChatGPT 用户。赞助 OpenClaw 是一种<span style="background-color: #c0c0c0;">生态锁定（Ecosystem Lock-in）</span>策略：当开发者习惯了 OpenClaw + GPT-4 的工作流，切换到其他模型的成本就会显著上升。</p>
<p><strong>NVIDIA</strong>：本地推理（Local Inference）是 OpenClaw 的长期演进方向之一。当用户开始在本地运行开源 LLM 时，对 GPU 算力的需求直接转化为 NVIDIA 的硬件销售。赞助 OpenClaw 是在培育<span style="background-color: #c0c0c0;">本地推理（Local Inference）</span>的市场需求。</p>
<p><strong>Vercel</strong>：OpenClaw 的 Web Control UI、文档站点、ClawHub 市场均可部署在 Vercel 平台上。赞助开源项目是 Vercel 拓展开发者工具生态的标准动作，与其赞助 Next.js、Turborepo 的逻辑一致。</p>
<div class="blog_h2"><span class="graybg">挑战与局限性</span></div>
<p>客观评估，OpenClaw 面临以下结构性挑战：</p>
<p><strong>16,843 个待处理 Issue</strong>：截至 2026 年 4 月，仓库有超过 16,000 个开放 Issue。这一数字反映了极高的用户参与度，但同时也意味着维护团队面临巨大的分诊压力（Triage Pressure）。大量 Issue 长期无响应，社区信任可能随时间侵蚀。</p>
<p><strong>Node.js 环境门槛</strong>：对于非 JavaScript 开发者，安装和维护 Node.js 环境本身就是一个障碍。Python 生态的 AutoGen 和 LangChain 在这一点上有天然优势——Python 的安装和环境管理对数据科学家和研究人员更为友好。</p>
<p><strong>单一维护者风险（Bus Factor）</strong>：steipete 贡献了 14,756 个提交，占总提交数（20,000+）的约 73%。第二贡献者（vincentkoc，1,690 个提交）的差距巨大。这意味着项目高度依赖单一个人，其 <span style="background-color: #c0c0c0;">巴士因子（Bus Factor）</span> 接近 1——如果核心维护者因任何原因无法继续，项目存续性将受到严重威胁。</p>
<p><strong>激进的重构节奏</strong>：几乎每个版本都包含破坏性变更（Breaking Changes）。插件 API 的频繁变动导致社区 Skills 的维护成本很高——一个 Skill 可能在几周内因上游 API 变化而失效。这种"快速迭代"与"稳定平台"之间的张力是 OpenClaw 当前最大的架构风险。</p>
<div class="blog_h2"><span class="graybg">未来演进方向</span></div>
<p><strong>TypeScript 原生编译器</strong>：OpenClaw 正在跟踪 @typescript/native-preview 7.0.0-dev（基于 Go 语言实现的 TypeScript 原生编译器）。该编译器承诺 10 倍以上的编译速度提升，这对 OpenClaw 的开发体验和 CI/CD 流水线效率将产生显著影响。仓库中已有实验性分支开始适配新编译器的特性。</p>
<p><strong>插件 SDK 稳定化</strong>：当前插件系统存在多个历史遗留路径（Legacy Paths），包括已废弃但尚未删除的旧版导入方式。SDK 稳定化的核心目标是确定一个长期不变的 API 面（API Surface），将所有旧路径标记为 deprecated 并在后续版本中移除。</p>
<p><strong>微信官方集成</strong>：与腾讯的合作将带来微信渠道的官方支持，而非目前依赖第三方库的间接集成。这对中国市场的渗透率至关重要——微信的月活跃用户数超过 13 亿，官方渠道意味着更稳定的 API 和更低的封号风险。</p>
<p><strong>企业级采用路径</strong>：Ansible Playbook + Docker 容器 + 沙箱隔离的组合已为企业部署铺平道路。下一步是完善 RBAC（基于角色的访问控制）、审计日志导出（Audit Log Export）到 SIEM 系统、以及 SSO（单点登录）集成。</p>
<div class="blog_h2"><span class="graybg">终评：本地优先 AI Agent 范式的代表</span></div>
<p>OpenClaw 代表的是一种明确的技术哲学：<strong>AI Agent 应该运行在用户控制的基础设施上，数据不应离开用户的信任边界</strong>。这一立场在当前"一切皆云"的行业趋势中显得逆流而行，但正是这种逆流赋予了它独特的价值。</p>
<p>从技术实现角度看，OpenClaw 在 TypeScript 单体仓库中实现了渠道抽象、插件隔离、三层沙箱、向量记忆和跨平台原生应用等复杂功能，代码质量和架构设计的成熟度远超其仅四个月的年龄。但其单一维护者集中度、激进重构节奏和日益庞大的 Issue 积压也构成了实质性风险。</p>
<p>对于开发者而言，OpenClaw 是目前最完整的开源本地优先 AI Agent 平台，没有之一。对于企业而言，其 Docker + Ansible + 沙箱的组合提供了可审计、可隔离、可复现的部署路径。对于 AI 行业而言，它证明了"本地优先"不是一个妥协方案，而是一个可以在功能完备性上与云端 SaaS 竞品正面对抗的架构范式。</p>
<div class="blog_h1"><span class="graybg">参考资源</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td>资源</td>
<td>链接</td>
</tr>
</thead>
<tbody>
<tr>
<td>GitHub 主仓库</td>
<td><a href="https://github.com/openclaw/openclaw">github.com/openclaw/openclaw</a></td>
</tr>
<tr>
<td>官方网站</td>
<td><a href="https://openclaw.ai">openclaw.ai</a></td>
</tr>
<tr>
<td>英文文档</td>
<td><a href="https://docs.openclaw.ai">docs.openclaw.ai</a></td>
</tr>
<tr>
<td>中文文档</td>
<td><a href="https://docs.openclaw.ai/zh-CN">docs.openclaw.ai/zh-CN</a></td>
</tr>
<tr>
<td>Discord 社区</td>
<td><a href="https://discord.gg/clawd">discord.gg/clawd</a></td>
</tr>
<tr>
<td>ClawHub 市场</td>
<td><a href="https://clawhub.com">clawhub.com</a></td>
</tr>
<tr>
<td>ClawHub 源码</td>
<td><a href="https://github.com/openclaw/clawhub">github.com/openclaw/clawhub</a></td>
</tr>
<tr>
<td>Star 增长曲线</td>
<td><a href="https://star-history.com/#openclaw/openclaw">star-history.com/#openclaw/openclaw</a></td>
</tr>
<tr>
<td>DeepWiki 分析</td>
<td><a href="https://deepwiki.com/openclaw/openclaw">deepwiki.com/openclaw/openclaw</a></td>
</tr>
<tr>
<td>安全信任中心</td>
<td><a href="https://trust.openclaw.ai">trust.openclaw.ai</a></td>
</tr>
<tr>
<td>安全联络邮箱</td>
<td><a href="mailto:security@openclaw.ai">security@openclaw.ai</a></td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">官方子仓库导航</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">仓库</td>
<td style="text-align: center;">Stars</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/openclaw/openclaw">openclaw/openclaw</a></td>
<td>343,696</td>
<td>主仓库：CLI、Gateway、Agent 运行时、Plugin SDK</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/clawhub">openclaw/clawhub</a></td>
<td>7,214</td>
<td>官方 Skill 目录平台</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/skills">openclaw/skills</a></td>
<td>3,622</td>
<td>ClawHub 所有 Skill 版本归档</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/acpx">openclaw/acpx</a></td>
<td>1,834</td>
<td>无头 ACP CLI：有状态 Agent Client Protocol 会话</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/lobster">openclaw/lobster</a></td>
<td>992</td>
<td>Lobster 工作流 Shell：类型化 JSON 管线 + 审批门</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/nix-openclaw">openclaw/nix-openclaw</a></td>
<td>611</td>
<td>Nix 声明式打包支持</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/openclaw-ansible">openclaw/openclaw-ansible</a></td>
<td>545</td>
<td>Ansible 自动化部署（Tailscale + UFW + Docker）</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/openclaw-windows-node">openclaw/openclaw-windows-node</a></td>
<td>405</td>
<td>Windows 系统托盘 + PowerToys 命令面板扩展</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/openclaw.ai">openclaw/openclaw.ai</a></td>
<td>250</td>
<td>官方网站源码</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/trust">openclaw/trust</a></td>
<td>35</td>
<td>安全信任策略与威胁模型</td>
</tr>
<tr>
<td><a href="https://github.com/openclaw/caclawphony">openclaw/caclawphony</a></td>
<td>34</td>
<td>Symphony：项目任务 → 隔离自主执行</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">CLI 命令速查</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">命令</td>
<td style="text-align: center;">用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>openclaw onboard</td>
<td>交互式引导安装（Gateway + 渠道 + Skills）</td>
</tr>
<tr>
<td>openclaw gateway run</td>
<td>启动 Gateway 控制平面</td>
</tr>
<tr>
<td>openclaw agent --message "..."</td>
<td>向 Agent 发送消息</td>
</tr>
<tr>
<td>openclaw channels status --probe</td>
<td>检查所有渠道连接状态</td>
</tr>
<tr>
<td>openclaw channels login</td>
<td>渠道登录（如 WhatsApp QR 扫码）</td>
</tr>
<tr>
<td>openclaw pairing approve <code></code></td>
<td>审批 DM 配对请求</td>
</tr>
<tr>
<td>openclaw doctor</td>
<td>诊断配置问题与安全风险</td>
</tr>
<tr>
<td>openclaw config set</td>
<td>修改配置项</td>
</tr>
<tr>
<td>openclaw update --channel</td>
<td>切换发布频道并更新</td>
</tr>
<tr>
<td>openclaw message send --to</td>
<td>向指定目标发送消息</td>
</tr>
<tr>
<td>openclaw gateway status</td>
<td>查看 Gateway 运行状态</td>
</tr>
<tr>
<td>openclaw nodes list</td>
<td>列出已连接的设备节点</td>
</tr>
<tr>
<td>clawhub install</td>
<td>从 ClawHub 安装 Skill</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">聊天内命令速查</span></div>
<p>以下命令可在 WhatsApp、Telegram、Slack、Discord、Teams、WebChat 等渠道的对话中直接发送：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">命令</td>
<td style="text-align: center;">功能</td>
</tr>
</thead>
<tbody>
<tr>
<td>/status</td>
<td>查看当前会话状态（模型 + token 用量 + 费用）</td>
</tr>
<tr>
<td>/new 或 /reset</td>
<td>重置会话</td>
</tr>
<tr>
<td>/compact</td>
<td>压缩会话上下文（生成摘要）</td>
</tr>
<tr>
<td>/think</td>
<td>设置思考等级：off|minimal|low|medium|high|xhigh</td>
</tr>
<tr>
<td>/verbose on|off</td>
<td>控制详细输出</td>
</tr>
<tr>
<td>/usage off|tokens|full</td>
<td>每条回复后显示用量统计</td>
</tr>
<tr>
<td>/restart</td>
<td>重启 Gateway（群组中仅 owner 可用）</td>
</tr>
<tr>
<td>/activation mention|always</td>
<td>群组激活模式切换</td>
</tr>
<tr>
<td>/elevated on|off</td>
<td>切换提升权限的 bash 访问</td>
</tr>
<tr>
<td>/approve</td>
<td>审批挂起的工具执行或插件操作</td>
</tr>
<tr>
<td>/acp spawn codex --bind here</td>
<td>在当前对话中创建 ACP 工作区</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">数据声明</span></div>
<p>本文所有技术细节均来源于以下一手数据，不依赖任何二手社区资料：</p>
<ul>
<li>GitHub API（api.github.com/repos/openclaw/openclaw）：仓库元数据、Stars/Forks/Issues 统计、贡献者排名、Release 说明</li>
<li>AGENTS.md（仓库根目录，35,263 字节）：架构规范、模块边界、构建指南、测试策略、发布守则</li>
<li>package.json（仓库根目录）：233 个 exports 条目（含 230 个 plugin-sdk 子路径）、47 个运行时依赖、22 个开发依赖、198 个 npm scripts</li>
<li>Dockerfile 与 docker-compose.yml：完整的容器化构建与部署配置</li>
<li>README.md（GitHub API base64 解码）：官方功能列表、渠道支持、安装指南、安全模型</li>
<li>GitHub Releases API（v2026.3.31、v2026.3.28 Release Notes）：破坏性变更与新功能详情</li>
</ul>
<div class="blog_h2"><span class="graybg">最小可用配置</span></div>
<p>完成 openclaw onboard 后，系统会生成一个最小配置文件 ~/.openclaw/openclaw.json。手动配置时，最小可用的 JSON 如下：</p>
<pre class="crayon-plain-tag">{
  "agent": {
    "model": "anthropic/claude-sonnet-4-6"
  }
}</pre>
<p>仅需指定一个模型即可启动 Gateway。Agent 会使用该模型进行所有推理任务。更复杂的配置可以声明多模型故障转移、渠道接入、安全策略、沙箱模式和 Skills：</p>
<pre class="crayon-plain-tag">{
  "agent": {
    "model": "openai/gpt-5.2",
    "fallbackModels": ["anthropic/claude-sonnet-4-6", "google/gemini-2.5-flash"],
    "thinkingLevel": "medium"
  },
  "agents": {
    "defaults": {
      "workspace": "~/.openclaw/workspace",
      "sandbox": {
        "mode": "non-main"
      }
    }
  },
  "channels": {
    "telegram": {
      "botToken": "123456:ABCDEF",
      "dmPolicy": "pairing",
      "allowFrom": []
    },
    "discord": {
      "token": "your-discord-bot-token",
      "dmPolicy": "pairing"
    },
    "whatsapp": {
      "allowFrom": ["+1234567890"]
    }
  },
  "browser": {
    "enabled": true
  },
  "gateway": {
    "mode": "local",
    "auth": {
      "mode": "token"
    }
  }
}</pre>
<p>完整配置参考详见 <a href="https://docs.openclaw.ai/gateway/configuration">docs.openclaw.ai/gateway/configuration</a>。配置文件支持 JSON5 格式（允许注释和尾逗号），这是一个面向人类可读性的设计选择。</p>
<div class="blog_h2"><span class="graybg">微信接入：腾讯官方插件</span></div>
<p>对于中国用户群体，微信接入是一个高优先级需求。OpenClaw 的微信支持通过腾讯官方发布的 npm 包 @tencent-weixin/openclaw-weixin 实现，基于 iLink Bot API。这是一个具有里程碑意义的集成——标志着腾讯以官方身份参与开源 AI Agent 生态。</p>
<p>安装与激活流程：</p>
<pre class="crayon-plain-tag"># 安装微信插件
openclaw plugins install "@tencent-weixin/openclaw-weixin"

# 扫码登录
openclaw channels login --channel openclaw-weixin</pre>
<p>微信集成当前仅支持私聊（Private Chat），不支持群聊。v2.x 版本要求 OpenClaw &gt;=2026.3.22。用户需要在微信客户端（我 → 设置 → 插件）中启用"微信 ClawBot 插件"——该功能由腾讯逐步灰度发布。</p>
<p>这种通过官方 npm 包而非逆向工程实现的微信接入路径，避免了 chatgpt-on-wechat 等项目面临的账号封禁风险，具有更高的稳定性和合规性。但目前的功能受限（仅私聊）也反映了腾讯在开放微信生态时的审慎态度。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/claw">OpenClaw学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/claw/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager</title>
		<link>https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager</link>
		<comments>https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager#comments</comments>
		<pubDate>Mon, 14 Oct 2024 06:45:45 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Cloud]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=40243</guid>
		<description><![CDATA[<p>In this blog post, I will walk through my journey investigating and resolving an issue where my certificate request from ZeroSSL, using <a class="read-more" href="https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager">Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>In this blog post, I will walk through my journey investigating and resolving an issue where my certificate request from ZeroSSL, using Cert-Manager, remained in a "not ready" state for over two days. I'll cover the tools involved, provide background information, and show how I eventually identified and fixed the problem.</p>
<div class="blog_h1"><span class="graybg">Background on ACME</span></div>
<div class="blog_h2"><span class="graybg">ACME</span></div>
<p>ACME (Automatic Certificate Management Environment) is a protocol developed by the Internet Security Research Group (ISRG) to automate the process of obtaining and managing SSL/TLS certificates from Certificate Authorities (CAs). It simplifies the traditionally manual steps involved in certificate issuance by using an automated process. ACME is widely used by services like Let’s Encrypt and ZeroSSL to secure websites with HTTPS.</p>
<p>The ACME protocol automates interactions between a client (such as Cert-Manager or Certbot) and a CA, allowing the client to request, renew, and manage certificates without human intervention. ACME operates through a series of challenges that prove ownership of the domain for which a certificate is requested. Once the ownership is verified, the CA can issue the certificate.</p>
<div class="blog_h3"><span class="graybg">HTTP-01 Challenge</span></div>
<p>In this challenge, the client proves ownership of a domain by hosting a specific file at a designated path (e.g., http://gmem.cc/.well-known/acme-challenge/). The CA attempts to retrieve this file, and if successful, the challenge is validated. This challenge is commonly used for publicly accessible web servers.</p>
<div class="blog_h3"><span class="graybg">DNS-01 Challenge</span></div>
<p>In this challenge, the client proves domain ownership by creating a special DNS TXT record for the domain. The CA checks the DNS record to confirm ownership. DNS-01 is typically used for wildcard certificates or when the server is not publicly accessible because it doesn’t rely on serving files over HTTP.</p>
<div class="blog_h3"><span class="graybg">TLS-ALPN-01 Challenge</span></div>
<p>This challenge requires the client to prove control of a domain by configuring a TLS server with a special certificate during the ACME validation process. The CA then connects to the server via TLS and checks the certificate. This challenge is less common and usually used in specialized environments.</p>
<div class="blog_h1"><span class="graybg">Background on Cert-Manager</span></div>
<p>Cert-Manager is an open-source Kubernetes add-on that automates the management, issuance, and renewal of certificates within Kubernetes clusters. It integrates with various Certificate Authorities (CAs) and protocols, including ACME (used by providers like Let’s Encrypt and ZeroSSL). Cert-Manager is widely used to ensure that certificates remain valid and secure without manual intervention.</p>
<div class="blog_h2"><span class="graybg">Cert-Manager Components</span></div>
<p>When deploying Cert-Manager in Kubernetes, several key components work together to handle certificate management.</p>
<div class="blog_h3"><span class="graybg">cert-manager</span></div>
<p>The core component of the Cert-Manager system, responsible for managing the lifecycle of certificates and interacting with Issuers (such as ACME servers like Let’s Encrypt or ZeroSSL). It runs as a Kubernetes controller and is responsible for:</p>
<ol>
<li>Watching Certificate, CertificateRequest, Issuer, ClusterIssuer, Order, and Challenge resources.</li>
<li>Requesting certificates from CAs.</li>
<li>Automatically renewing certificates before expiration.</li>
<li>Handling the interactions with external CAs (via ACME, Vault, Venafi, etc.).</li>
</ol>
<p>This component performs the actual management of certificates, from creation to renewal, ensuring that the requested certificates are stored securely as Kubernetes Secrets.</p>
<div class="blog_h3"><span class="graybg">cert-manager-cainjector</span></div>
<p>The CA Injector is an additional component that works alongside Cert-Manager to inject CA data into other Kubernetes resources. It primarily operates on Kubernetes ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources, injecting certificates into them automatically. This is necessary for:</p>
<ol>
<li>Mutating admission controllers that require TLS certificates for secure communication.</li>
</ol>
<ol>
<li>Ensuring that Kubernetes components relying on CA certificates have up-to-date CA data.</li>
</ol>
<p>The cainjector is critical in environments where certain Kubernetes components (e.g., webhooks) require their certificates to be signed by a trusted CA.</p>
<div class="blog_h3"><span class="graybg">cert-manager-webhook</span></div>
<p>The Webhook component provides an admission controller that validates Cert-Manager resources like Certificate, Issuer, ClusterIssuer, and CertificateRequest upon creation or update. It ensures that the Cert-Manager resources are correctly configured by:</p>
<ol>
<li>Validating resources before they are accepted into the Kubernetes API (syntax and structure).</li>
</ol>
<ol>
<li>Mutating resources to provide defaults (for example, setting default values in a Certificate resource).</li>
</ol>
<ol>
<li>Providing a layer of security and correctness by ensuring invalid configurations are caught early.</li>
</ol>
<p>The webhook helps catch configuration issues early, improving the reliability of certificate management workflows.</p>
<div class="blog_h3"><span class="graybg">cert-manager-webhook-dnspod</span></div>
<p>This webhook, which is maintained by the <a href="https://github.com/imroc/cert-manager-webhook-dnspod">community</a>, specifically handles DNS-01 challenges for domains hosted in Tencent Cloud’s DNSPod. When Cert-Manager requests a certificate using the DNS-01 challenge, it needs to create a DNS TXT record in the domain's DNS zone. cert-manager-webhook-dnspod facilitates this by interacting with the DNSPod API to manage DNS records.</p>
<p>Cert-Manager invokes this webhook when it needs to solve a DNS-01 challenge using DNSPod as the DNS provider. The webhook receives instructions from Cert-Manager, communicates with the DNSPod API to create or delete DNS TXT records, and reports back to Cert-Manager when the challenge is complete.</p>
<p>In this post the webhook was created with the following manifest:</p>
<pre class="crayon-plain-tag">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager-webhook-dnspod
  namespace: argocd
spec:
  destination:
    namespace: cert-manager
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://github.com/imroc/cert-manager-webhook-dnspod
    targetRevision: master
    path: charts/cert-manager-webhook-dnspod
    helm:
      releaseName: cert-manager-webhook-dnspod
      values: |
        groupName: acme.dnspod.tencent.com
        clusterIssuer:
          name: letsencrypt-issuer
          secretId: DNSPOD_SECRETID
          secretKey: DNSPOD_SECRETKEY
          email: gmem@me.com
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated:
      prune: true
      selfHeal: true
 </pre>
<div class="blog_h2"><span class="graybg">Cert-Manager CRDs</span></div>
<p>Cert-Manager relies on several types of custom resources that work together to manage the certificate lifecycle:</p>
<ol style="list-style-type: undefined;">
<li>Issuer/ClusterIssuer:
<ol>
<li>An Issuer or ClusterIssuer is a Cert-Manager resource that defines how certificates should be requested from a Certificate Authority (CA). The difference between them is that an Issuer is scoped to a single namespace, whereas a ClusterIssuer can be used cluster-wide.</li>
<li>Issuers can be configured to use different CA backends, such as ACME, Vault, or self-signed certificates.</li>
</ol>
</li>
<li>Certificate: The Certificate resource defines which certificates should be issued and managed by Cert-Manager. It specifies details such as the domain names, issuer to use, renewal period, and where to store the certificate (usually in a Kubernetes secret).</li>
<li>CertificateRequest: When a Certificate resource is created, Cert-Manager generates a CertificateRequest. This resource represents the actual request to the Issuer for a certificate. Cert-Manager manages these requests and handles the approval and signing process.</li>
<li>Order: When Cert-Manager requests a certificate from an ACME-based Issuer, it creates an Order resource to track the status of the certificate issuance. The Order keeps track of challenges and interactions with the ACME server.</li>
<li>Challenge: A Challenge resource represents the ACME challenge issued by the CA (such as DNS-01 or HTTP-01). The challenge proves domain ownership by requiring the client (Cert-Manager) to respond to a domain validation request from the CA.</li>
</ol>
<div class="blog_h2"><span class="graybg">Cert-Manager  Challenge Workflows</span></div>
<div class="blog_h3"><span class="graybg">HTTP-01 Challenge<br /></span></div>
<p>The HTTP-01 challenge is used when the domain is publicly accessible over HTTP. The CA validates ownership of the domain by requesting a specific file over HTTP. Cert-Manager sets up the challenge response using a Kubernetes Ingress resource.</p>
<ol>
<li>Create an Issuer or ClusterIssuer: The Issuer defines the connection to the CA (such as Let’s Encrypt or ZeroSSL) and specifies that ACME should be used with the HTTP-01 challenge.</li>
<li>Create a Certificate Resource: A Certificate resource is created that specifies the domain names for which a certificate is needed and the Issuer to use.</li>
<li>Cert-Manager Creates a CertificateRequest: Cert-Manager generates a CertificateRequest based on the Certificate resource.</li>
<li>Cert-Manager Creates an Order: Cert-Manager creates an Order resource to track the status of the certificate request with the ACME server.</li>
<li>Cert-Manager Creates an HTTP-01 Challenge: An HTTP-01 challenge is created, and Cert-Manager configures an Ingress resource to serve the challenge response at the path /.well-known/acme-challenge/.</li>
<li>ACME Server Attempts to Validate: The CA attempts to access the challenge file via the HTTP URL (e.g., http://example.com/.well-known/acme-challenge/&lt;token&gt;). If successful, the challenge is validated.</li>
<li>Certificate Issued: Once the challenge is validated, the CA issues the certificate, and Cert-Manager stores it in the specified Kubernetes Secret.</li>
</ol>
<div class="blog_h3"><span class="graybg">DNS-01 Challenge </span></div>
<p>The DNS-01 challenge is used when domain ownership must be validated via DNS records. This method is often preferred for wildcard certificates or domains that are not publicly accessible over HTTP.</p>
<ol>
<li>Create an Issuer or ClusterIssuer: The Issuer specifies that ACME should be used with the DNS-01 challenge and includes configuration for interacting with the DNS provider (e.g., AWS Route53, Cloudflare, or a custom webhook like dnspod).</li>
<li>Create a Certificate Resource: A Certificate resource is created that defines the domains for which the certificate is needed and the Issuer to use.</li>
<li>Cert-Manager Creates a CertificateRequest: Cert-Manager generates a CertificateRequest based on the Certificate resource.</li>
<li>Cert-Manager Creates an Order: Cert-Manager creates an Order resource to track the status of the certificate request with the ACME server.</li>
<li>Cert-Manager Creates a DNS-01 Challenge: A DNS-01 challenge is created, and Cert-Manager interacts with the configured DNS provider to automatically create a special TXT record for the domain (e.g., _acme-challenge.example.com).</li>
<li>ACME Server Attempts to Validate: The CA checks for the presence of the _acme-challenge.example.com TXT record in the domain’s DNS records. If the correct value is found, the challenge is validated.</li>
<li>Certificate Issued: Once the DNS challenge is validated, the CA issues the certificate, and Cert-Manager stores it in the specified Kubernetes Secret.</li>
</ol>
<div class="blog_h1"><span class="graybg">The Problem, Investigation and Fix<br /></span></div>
<div class="blog_h2"><span class="graybg">Problem Statement </span></div>
<p>I created a ClusterIssuer and a Certificate for the wildcard domain *.gmem.cc. However, after waiting for more than two days, the certificate status still showed as not ready, with the following condition in the certificate's status:</p>
<pre class="crayon-plain-tag">status:
  conditions:
    - lastTransitionTime: "2024-10-14T06:02:10Z"
      message: Issuing certificate as Secret does not exist
      observedGeneration: 1
      reason: DoesNotExist
      status: "False"
      type: Ready</pre>
<p>I checked on DNSPod and found a <pre class="crayon-plain-tag">TXT</pre> record <pre class="crayon-plain-tag">_acme-challenge.gmem.cc</pre> with the TTL set to 600 seconds.</p>
<div class="blog_h2"><span class="graybg">Initial Investigation</span></div>
<p>To diagnose the issue, I checked the status of the related Cert-Manager resources: <pre class="crayon-plain-tag">CertificateRequest</pre>, <pre class="crayon-plain-tag">Order</pre>, and <pre class="crayon-plain-tag">Challenge</pre>. These are the key resources that Cert-Manager uses to interact with ACME and handle certificate issuance. Here’s an overview of my findings:</p>
<p>CertificateRequest:</p>
<pre class="crayon-plain-tag">status:
  conditions:
    - lastTransitionTime: "2024-10-14T06:02:14Z"
      message: Certificate request has been approved by cert-manager.io
      reason: cert-manager.io
      status: "True"
      type: Approved
    - lastTransitionTime: "2024-10-14T06:02:14Z"
      message: 'Waiting on certificate issuance from order istio-system/wildcard-ssl-5pgsr-3353861729: "pending"'
      reason: Pending
      status: "False"
      type: Ready</pre>
<p>The certificate request was approved, but the system was waiting for the certificate issuance to complete.</p>
<p>Order: </p>
<pre class="crayon-plain-tag">status:
  state: pending
  authorizations:
    - challenges:
        - token: iQMwrfsFRmJ_MytUY3N4NW6QehtTn0-IEvJWAmYEw_k
          type: dns-01
          url: https://acme.zerossl.com/v2/DV90/chall/DDRBMBd9jnJo_W4EcQfSWQ
      wildcard: true</pre>
<p>The order was still in a "pending" state, and the DNS-01 challenge had not been completed yet.</p>
<p>Challenge:</p>
<pre class="crayon-plain-tag">status:
  presented: true
  processing: true
  reason: 'Waiting for DNS-01 challenge propagation: DNS record for "gmem.cc" not yet propagated'
  state: pending</pre>
<p>The challenge was waiting for DNS propagation, but apparently the <pre class="crayon-plain-tag">TXT</pre> record I mentioned above had been created two days ago.</p>
<div class="blog_h2"><span class="graybg">Global DNS Propagration Check</span></div>
<p>Mutifarious reasons can cause delay on DNS propagation, we can check whether TXT record  _acme-challenge.gmem.cc is synchronized all over the world using <a href="https://www.whatsmydns.net/">whatsmydns.net</a>:</p>
<p style="padding-left: 30px;">https://www.whatsmydns.net/#TXT/_acme-challenge.gmem.cc</p>
<p>In this case, the check result was that the record had been fully propagated.</p>
<div class="blog_h2"><span class="graybg">Cert-Manager Logs</span></div>
<p>I then checked the logs for the Cert-Manager pod to see if any errors were being reported. The relevant and repeating error message from the logs was:</p>
<p style="padding-left: 30px;">E1014 05:52:58.647839 1 sync.go:190] "cert-manager/challenges: propagation check failed" err="DNS record for \"gmem.cc\" not yet propagated"</p>
<p>At this point, it seemed that Cert-Manager was unable to see the propagated DNS records, even though I had confirmed their existence.</p>
<p>After enabling verbose logging (--v5 log level) in Cert-Manager, I finally discovered the root cause. The detailed logs revealed that Cert-Manager was checking the DNS record at an intermediate CNAME:</p>
<p style="padding-left: 30px;">I1014 06:20:33.919849 1 dns.go:116] "cert-manager/challenges/Check: checking DNS propagation"<br />I1014 06:20:33.921190 1 wait.go:90] Updating FQDN: _acme-challenge.gmem.cc. with its CNAME: lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com.<br />I1014 06:25:35.886683 1 wait.go:298] Searching fqdn "lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com." using seed nameservers [10.231.18.121:53]<br />I1014 06:25:35.886696 1 wait.go:329] Returning cached zone record "sg-tencentclb.com." for fqdn "lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com."<br />I1014 06:20:33.987786 1 wait.go:141] Looking up TXT records for "lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com."</p>
<p>The challenge was failing because the DNS record for  <pre class="crayon-plain-tag">*.gmem.cc</pre> was a  CNAME pointing to another domain, lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com, which caused Cert-Manager to search for a wrong TXT record.</p>
<div class="blog_h2"><span class="graybg">The Fix</span></div>
<p>The solution was to<span style="background-color: #c0c0c0;"> remove the wildcard record *.gmem.cc which was CNAMEed to Tencent Cloud Loadbalancer address.</span> After a few minutes, the DNS cache was invalidated and Cert-Manager finally logged something different:</p>
<p style="padding-left: 30px;">I1014 06:25:45.974354 1 wait.go:298] Searching fqdn "_acme-challenge.gmem.cc." using seed nameservers [10.231.18.121:53]<br />I1014 06:25:46.453434 1 wait.go:383] Returning discovered zone record "gmem.cc." for fqdn "_acme-challenge.gmem.cc."<br />I1014 06:25:46.454660 1 wait.go:316] Returning authoritative nameservers [c.dnspod.com., a.dnspod.com., b.dnspod.com.]<br />I1014 06:25:46.462705 1 wait.go:141] Looking up TXT records for "_acme-challenge.gmem.cc."</p>
<p>indicating that the correct TXT record was found. And from the subsequent logs some working detailed of Cert-Manager was revealed:</p>
<p style="padding-left: 30px;">I1014 06:25:47.050500 1 dns.go:128] "cert-manager/challenges/Check: waiting DNS record TTL to allow the DNS01 record to propagate for domain" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" dnsName="gmem.cc" type="DNS-01" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" domain="gmem.cc" ttl=60 fqdn="_acme-challenge.gmem.cc."</p>
<p style="padding-left: 60px;">This line indicated that after Cert-Manager validated the TXT record locally, it would wait for TTL ( 60 here ) seconds, just in case that Zero SSL server hadn't been able to see te record.</p>
<p style="padding-left: 30px;">014 06:26:47.051650 1 sync.go:359] "cert-manager/challenges/acceptChallenge: accepting challenge with ACME server" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" dnsName="gmem.cc" type="DNS-01"<br />I1014 06:26:47.051665 1 logger.go:81] "cert-manager/acme-middleware: Calling Accept"<br />I1014 06:26:49.708221 1 sync.go:376] "cert-manager/challenges/acceptChallenge: waiting for authorization for domain" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" dnsName="gmem.cc" type="DNS-01"<br />I1014 06:26:49.708250 1 logger.go:99] "cert-manager/acme-middleware: Calling WaitAuthorization"<br />I1014 06:26:50.194610 1 logs.go:199] "cert-manager/controller: Event(v1.ObjectReference{Kind:\"Challenge\", Namespace:\"istio-system\", Name:\"wildcard-ssl-5pgsr-3353861729-3026093647\", UID:\"a4b2f2d7-6185-4072-92f0-b7a89418cdb1\", APIVersion:\"acme.cert-manager.io/v1\", ResourceVersion:\"463570645\", FieldPath:\"\"}): type: 'Normal' reason: 'DomainVerified' Domain \"gmem.cc\" verified with \"DNS-01\" validation"</p>
<p style="padding-left: 60px;">After the wait, Cert-Manager called Zero SSL server for Accept and WaitAuthorization operation and the server verified that we were the owner of the domain name.</p>
<div class="blog_h1"><span class="graybg">Conclusion</span></div>
<p>In this case, the root cause of the certificate issuance failure was a CNAME record interfering with Cert-Manager's DNS-01 challenge. It had nothing to do with the ACME server but was linked to Cert-Manager's internal implementation.</p>
<p>To aviod similar issues in the future, we need to stop using wildcard domain records.</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager">Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>A Comprehensive Study of Kotlin for Java Developers</title>
		<link>https://blog.gmem.cc/comprehensive-study-kotlin-java-developers</link>
		<comments>https://blog.gmem.cc/comprehensive-study-kotlin-java-developers#comments</comments>
		<pubDate>Sun, 29 Sep 2024 12:52:40 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=40127</guid>
		<description><![CDATA[<p>Introduction Purpose of the Study Understanding the Motivations for Learn Kotlin In the rapidly evolving field of software development, staying abreast of <a class="read-more" href="https://blog.gmem.cc/comprehensive-study-kotlin-java-developers">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/comprehensive-study-kotlin-java-developers">A Comprehensive Study of Kotlin for Java Developers</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">Introduction</span></div>
<div class="blog_h2"><span class="graybg">Purpose of the Study</span></div>
<div class="blog_h3"><span class="graybg">Understanding the Motivations for Learn Kotlin</span></div>
<p>In the rapidly evolving field of software development, staying abreast of emerging technologies is essential for maintaining a competitive edge. Kotlin, a statically typed programming language developed by JetBrains, has garnered significant attention since its release. For developers proficient in Java—especially versions 8 and earlier—exploring Kotlin offers an opportunity to enhance coding efficiency, embrace modern programming paradigms, and address some of the limitations inherent in older versions of Java.</p>
<p>The motivations for learning Kotlin are multifaceted:</p>
<ol style="list-style-type: undefined;">
<li>Modern Language Features: Kotlin introduces contemporary features such as null safety, data classes, and coroutines, which streamline coding practices and reduce common programming errors.</li>
<li>Interoperability: Kotlin is fully interoperable with Java, allowing for seamless integration into existing Java projects and the use of established Java libraries.</li>
<li>Industry Adoption: Major companies, including Google, have endorsed Kotlin for Android development, signaling a shift in industry standards and practices.</li>
</ol>
<div class="blog_h3"><span class="graybg">Setting Goals for Mastering Kotlin with a Java Background</span></div>
<p>As a Java developer delving into Kotlin, it's important to set clear, achievable goals to maximize the benefits of this study:</p>
<ol style="list-style-type: undefined;">
<li>Comprehensive Understanding: Gain a thorough grasp of Kotlin's syntax, features, and best practices.</li>
<li>Comparative Analysis: Identify and understand the similarities and differences between Kotlin and Java to leverage existing knowledge effectively.</li>
<li>Practical Application: Apply Kotlin concepts in real-world scenarios, including Android development and server-side applications.</li>
<li>Interoperability Proficiency: Learn how to integrate Kotlin code with Java, enabling the use of both languages within the same project seamlessly.</li>
<li>Code Optimization: Utilize Kotlin's features to write more concise, efficient, and maintainable code compared to traditional Java approaches.</li>
</ol>
<p>By setting these goals, the study aims to provide a structured pathway to not only learn Kotlin but also to enhance overall programming proficiency.</p>
<div class="blog_h2"><span class="graybg">Overview of Java and Kotlin</span></div>
<div class="blog_h3"><span class="graybg">Brief History and Evolution of Java up to Version 21</span></div>
<p>Java has been a cornerstone in the software development industry since its inception by Sun Microsystems in 1995. Over the years, it has undergone significant transformations, introducing features that address the evolving needs of developers and the industry at large. Below is a chronological overview of Java's evolution up to Java 21, the latest version as of October 2023.</p>
<p>Java 1.0 to Java 1.4 (1996 - 2002): Establishing the Foundation</p>
<ol style="list-style-type: undefined;">
<li>Java 1.0 (1996): The initial release provided the basic framework of the language, focusing on portability and network computing.</li>
<li>Java 1.1 to 1.4: These versions introduced inner classes, JDBC, JavaBeans, the Collections Framework, and enhanced performance and security features.</li>
</ol>
<p>Java 5 (2004): Embracing Modern Programming Concepts</p>
<ol style="list-style-type: undefined;">
<li>Generics: Allowed for type-safe collections.</li>
<li>Annotations: Provided metadata that could be processed by the compiler or at runtime.</li>
<li>Enhanced for-loop: Simplified iteration over collections and arrays.</li>
<li>Autoboxing/Unboxing: Automated conversion between primitive types and their corresponding object wrapper classes.</li>
</ol>
<p>Java 6 and Java 7 (2006 - 2011): Performance and Usability Enhancements</p>
<ol style="list-style-type: undefined;">
<li>Java 6: Focused on performance improvements and included updates to the JVM and core libraries.</li>
</ol>
<ol style="list-style-type: undefined;">
<li>Java 7:
<ol>
<li>Diamond Operator (&lt;&gt;): Simplified the use of generics.</li>
<li>Try-with-Resources: Enhanced exception handling and resource management.</li>
<li>Strings in Switch Statements: Allowed strings to be used in switch cases.</li>
</ol>
</li>
</ol>
<p>Java 8 (2014): A Paradigm Shift with Functional Programming</p>
<ol style="list-style-type: undefined;">
<li>Lambda Expressions: Introduced functional programming concepts, enabling more concise code.</li>
<li>Stream API: Provided a powerful way to process collections in a functional style.</li>
<li>Optional Class: Addressed null references by providing a container object which may or may not contain a non-null value.</li>
<li>Date and Time API: Offered a new set of classes under java.time package for date and time manipulation.</li>
</ol>
<p>Java 9 (2017): Modularization and JShell</p>
<ol style="list-style-type: undefined;">
<li>Project Jigsaw (Modules): Introduced the Java Platform Module System, allowing for better encapsulation and modularization of code.</li>
<li>JShell (REPL): Provided an interactive Read-Eval-Print Loop tool for rapid prototyping.</li>
</ol>
<p>Java 10 (2018): Local Variable Type Inference</p>
<ol style="list-style-type: undefined;">
<li>var Keyword: Enabled local variable type inference, allowing the compiler to infer the type of a variable from its initializer.</li>
</ol>
<p>Java 11 (2018): Long-Term Support and New Features</p>
<ol style="list-style-type: undefined;">
<li>Standardized HTTP Client API: Introduced a new HTTP client under java.net.http.</li>
<li>String Methods Enhancements: Added methods like isBlank(), lines(), strip(), repeat().</li>
<li>Removal of JavaFX: Decoupled JavaFX from the JDK.</li>
</ol>
<p>Java 12 to Java 15 (2019 - 2020): Incremental Improvements</p>
<ol style="list-style-type: undefined;">
<li>Java 12: Switch Expressions (Preview): Enhanced switch statements to be used as expressions.</li>
<li>Java 13: Text Blocks (Preview): Simplified the inclusion of multi-line strings.</li>
<li>Java 14:
<ol>
<li>Records (Preview): Introduced a compact syntax for declaring data classes.</li>
<li>Helpful NullPointerExceptions: Improved the detail in NullPointerException messages.</li>
</ol>
</li>
</ol>
<p>Java 15:</p>
<ol style="list-style-type: undefined;">
<li>Sealed Classes (Preview): Restricted which classes can extend or implement a class or interface.</li>
<li>Z Garbage Collector (Product Feature): Low-latency garbage collector moved from experimental to production.</li>
</ol>
<p>Java 16 and Java 17 (2021): Pattern Matching and Sealed Classes</p>
<ol style="list-style-type: undefined;">
<li>Java 16:
<ol>
<li>Pattern Matching for instanceof: Simplified the use of instanceof with pattern variables.</li>
<li>Records: Moved from preview to a standard feature.</li>
</ol>
</li>
<li>Java 17 (Long-Term Support Release):
<ol>
<li>Sealed Classes: Finalized as a standard feature.</li>
<li>Removal of Deprecated Features: Eliminated older features like the Applet API.</li>
<li>Enhanced Pseudorandom Number Generators: Introduced new interfaces and implementations for PRNGs.</li>
</ol>
</li>
</ol>
<p>Java 18 and Java 19 (2022): Incubator and Preview Features</p>
<ol style="list-style-type: undefined;">
<li>Java 18:
<ol>
<li>UTF-8 by Default: Standardized UTF-8 as the default character set.</li>
<li>Simple Web Server: Provided a command-line tool for starting a minimal web server.</li>
</ol>
</li>
<li>Java 19:
<ol>
<li>Virtual Threads (Preview): Part of Project Loom, introduced lightweight threads for concurrent programming.</li>
<li>Structured Concurrency (Incubator): Simplified multithreaded programming by treating multiple tasks running in different threads as a single unit.</li>
</ol>
</li>
</ol>
<p>Java 20 and Java 21 (2023): Advancements in Performance and Productivity</p>
<ol style="list-style-type: undefined;">
<li>Java 20:
<ol>
<li>Scoped Values (Incubator): Allowed for the sharing of immutable data within and across threads.</li>
<li>Record Patterns (Second Preview): Enhanced pattern matching for records.</li>
</ol>
</li>
<li>Java 21 (Latest LTS as of October 2023):
<ol>
<li>Virtual Threads (Standard Feature): Finalized virtual threads for high-throughput concurrent applications.</li>
<li>Sequenced Collections: Introduced interfaces to represent collections with a defined encounter order.</li>
<li>String Templates (Preview): Provided a new way to create and process strings with embedded expressions.</li>
<li>Pattern Matching for Switch (Standard Feature): Finalized pattern matching in switch expressions and statements.</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Brief History of Kotlin</span></div>
<p>In 2010 JetBrains began the development of Kotlin, aiming to create a language that could improve developer productivity and happiness. The primary motivations were:</p>
<ol style="list-style-type: undefined;">
<li>Conciseness: Reduce boilerplate code common in Java.</li>
<li>Safety: Introduce features like null safety to prevent common errors.</li>
<li>Interoperability: Ensure seamless integration with Java code and libraries.</li>
<li>Tooling Support: Leverage JetBrains' expertise in IDE development to provide excellent tooling from the outset.</li>
</ol>
<p>In July 2011, JetBrains publicly announced Kotlin, revealing their plans to create a new language for the JVM.</p>
<p>Kotlin 1.0 (February 2016):</p>
<ol style="list-style-type: undefined;">
<li>First Stable Release: Marked the language as production-ready after years of development and refinement.</li>
<li>Core Features: Included null safety, extension functions, data classes, and higher-order functions.</li>
<li>Interoperability: Ensured 100% compatibility with Java, allowing developers to call Kotlin code from Java and vice versa</li>
<li>Google I/O Announcement: Google declared official support for Kotlin on Android, making it a first-class language for Android app development. Led to a significant surge in Kotlin adoption within the Android community.</li>
</ol>
<p>Kotlin 1.1 (March 2017):</p>
<ol style="list-style-type: undefined;">
<li>Coroutines (Experimental): Introduced coroutines for asynchronous programming, allowing developers to write non-blocking code more easily.</li>
<li>JavaScript Target: Enabled compilation of Kotlin code to JavaScript, facilitating cross-platform development.</li>
</ol>
<p>Kotlin 1.2 (November 2017):</p>
<ol style="list-style-type: undefined;">
<li>Multiplatform Projects (Experimental): Allowed sharing code between JVM and JavaScript platforms, paving the way for true cross-platform applications.</li>
<li>Improved Compilation: Enhanced compiler performance and incremental compilation support.</li>
</ol>
<p>Kotlin 1.3 (October 2018):</p>
<ol style="list-style-type: undefined;">
<li>Coroutines Become Stable: Solidified coroutines as a core feature, providing a powerful tool for asynchronous and concurrent programming.</li>
<li>Kotlin/Native: Enabled compilation to native binaries, expanding Kotlin's reach to platforms like iOS, Windows, Linux, and macOS without the need for a virtual machine.</li>
<li>Contracts: Introduced experimental support for contracts, allowing for more precise code analysis.</li>
</ol>
<p>Kotlin 1.4 (August 2020):</p>
<ol style="list-style-type: undefined;">
<li>Multiplatform Enhancements: Improved the multiplatform project support, making it more stable and easier to use.</li>
<li>Compiler Improvements: Focused on performance, resulting in faster compilation times and better IDE responsiveness.</li>
<li>Standard Library Updates: Added new functions and classes to the standard library, enhancing functionality.</li>
</ol>
<p>Kotlin 1.5 (May 2021):</p>
<ol style="list-style-type: undefined;">
<li>Language Features: Introduced JVM records, sealed interfaces, and inline classes.</li>
<li>Stability: Many experimental features were promoted to stable status.</li>
</ol>
<p>Kotlin 1.6 (November 2021):</p>
<ol style="list-style-type: undefined;">
<li>Standard Library Enhancements: Improved existing APIs and added new ones.</li>
<li>Performance: Continued focus on compiler and runtime performance optimizations.</li>
</ol>
<p>Kotlin 1.7 (June 2022):</p>
<ol style="list-style-type: undefined;">
<li>Context Receivers (Experimental): Added support for context-dependent declarations.</li>
<li>K2 Compiler (Alpha): Began work on a new frontend compiler aimed at performance improvements and better tooling.</li>
</ol>
<p>Kotlin 1.8 (January 2023):</p>
<ol style="list-style-type: undefined;">
<li>Incremental Updates: Brought further enhancements to the language and tooling.</li>
<li>Kotlin Multiplatform Mobile (KMM): Progressed towards stabilizing shared code between Android and iOS.</li>
</ol>
<p>Kotlin 1.9 (July 2023):</p>
<ol style="list-style-type: undefined;">
<li>K2 Compiler Advances: Continued development of the new compiler, improving compilation times and error diagnostics.</li>
<li>Language Features: Added new experimental features and made existing ones more stable</li>
</ol>
<p>Kotlin 2.x (May 2024):</p>
<p>Kotlin 2.0 brings significant improvements and new features that make it a more powerful, expressive, and developer-friendly language. Here are some of the significant features introduced in Kotlin 2.0:</p>
<ol style="list-style-type: undefined;">
<li>K2 Compiler (New Generation Compiler)
<ol>
<li>Improved Performance: The new K2 compiler is designed to be faster and more efficient, significantly reducing compilation times.</li>
<li>Improved Error Reporting: K2 enhances error diagnostics, providing more detailed and user-friendly error messages.</li>
<li>Unified Backend: It unifies the backend of Kotlin/Native, Kotlin/JVM, and Kotlin/JS, allowing developers to work with different platforms more seamlessly.</li>
<li>Modular and Extensible: The new architecture allows for easier integration of third-party tools, opening doors for more customization and extension.</li>
</ol>
</li>
<li>Context Receivers
<ol>
<li>This feature allows adding additional context to functions without explicitly passing it through parameters. It simplifies code that requires multiple receivers, such as in DSLs (Domain Specific Languages) and other multi-receiver scenarios.<br />
<pre class="crayon-plain-tag">fun  withDatabaseContext(block: Database.() -&gt; T): T { 
    return Database().block()
}

val result = withDatabaseContext {
    // Inside this lambda, `this` refers to a `Database` instance.
    query("SELECT * FROM users")
}</pre>
</li>
</ol>
</li>
<li>Builder Inference Improvements: Kotlin 2.0 improves type inference in builder-style APIs, making code more concise and readable. This is particularly useful for libraries like coroutines and UI libraries that leverage builder patterns.</li>
<li>Explicit API Mode for Kotlin Libraries: The explicit API mode helps developers build public libraries with stricter controls. It enforces that every member of a public API must explicitly declare its visibility and type, making the API more predictable and well-documented.</li>
<li>Improved Multiplatform Support
<ol>
<li>Multiplatform Compose: Kotlin 2.0 enhances Kotlin Multiplatform projects, especially for shared UI development. It supports libraries like Compose Multiplatform for building UIs that run on multiple platforms with the same codebase.</li>
<li>Better Gradle Integration: The new version comes with improved Gradle tooling and support for managing multiple targets more easily.</li>
</ol>
</li>
<li>New Sealed Interface Features: Kotlin 2.0 allows sealed interfaces, which, like sealed classes, restrict which classes can implement them. This improves safety in scenarios where specific hierarchies are needed.<br />
<pre class="crayon-plain-tag">sealed interface Operation
class Add(val value: Int) : Operation
class Subtract(val value: Int) : Operation</pre>
</li>
<li>
<p>Collection Literals and Destructuring in Loops: </p>
<ol>
<li>
<p>Kotlin 2.0 introduces collection literals to make collection initialization more concise.</p>
</li>
<li>
<p>Destructuring in loops is improved, allowing better handling of pairs and other data structures in iteration contexts.</p>
</li>
</ol>
</li>
<li>Value Classes (Refined): Kotlin 2.0 continues to improve value classes (formerly inline classes), ensuring that they are more efficient and flexible. Value classes can be used in cases where a small, immutable data holder is needed without the overhead of full object allocation.</li>
<li>Unit Testing Enhancements: Kotlin 2.0 improves unit testing capabilities for Kotlin Multiplatform projects, providing better tools and frameworks for cross-platform test sharing and execution.</li>
<li>Enhanced Coroutines:
<ol>
<li>Kotlin 2.0 further enhances Kotlin’s popular coroutine library with better integration, new debugging tools, and optimizations for more efficient asynchronous programming.</li>
<li>Structured Concurrency Improvements: Kotlin 2.0 adds additional safeguards and features to make structured concurrency even more robust.</li>
</ol>
</li>
<li>Function Interfaces: Kotlin 2.0 introduces Function Interfaces (aka SAM conversions), which allow developers to convert interfaces with a single abstract method into lambda expressions. This is especially useful when interoperating with Java libraries that heavily use functional interfaces.</li>
<li>Improved Null Safety Features: Kotlin 2.0 strengthens null-safety features, particularly in interoperability with Java. Improved type-checking mechanisms ensure that fewer null-pointer exceptions occur when interacting with non-null Kotlin code and nullable Java code.</li>
<li>Incremental Compilation Improvements: Kotlin 2.0 improves the incremental compilation process, reducing build times even for large projects and making the development experience smoother.</li>
<li>Backward Compatibility with Kotlin 1.x: Kotlin 2.0 is designed to be backward compatible with Kotlin 1.x, allowing gradual migration of projects without major breaking changes.</li>
</ol>
<div class="blog_h1"><span class="graybg">Kotlin Syntax Basics</span></div>
<div class="blog_h2"><span class="graybg">Main Function Declaration</span></div>
<p>In both Java and Kotlin, the main function serves as the entry point of the application. However, the syntax and structure differ between the two languages.</p>
<div class="blog_h3"><span class="graybg">Java Main Method</span></div>
<p>In Java, the main method must be declared within a class and must be public, static, and void, accepting a String[] argument:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}</pre>
<div class="blog_h3"><span class="graybg">Kotlin Main Function</span></div>
<p>In Kotlin, the main function does not need to be part of a class and can be declared at the top level: </p>
<pre class="crayon-plain-tag">fun main(args: Array) {
    println("Hello, Kotlin!")
}</pre>
<div class="blog_h2"><span class="graybg">Variables and Data Types </span></div>
<p>Kotlin introduces a more concise and expressive way to declare variables, emphasizing immutability and type inference.</p>
<div class="blog_h3"><span class="graybg">Mutable (var) vs Immutable (val) Variables</span></div>
<ol style="list-style-type: undefined;">
<li>val (Immutable): Declares a read-only variable whose value cannot be changed once assigned.</li>
<li>var (Mutable): Declares a variable whose value can be changed.</li>
</ol>
<p>For example:</p>
<pre class="crayon-plain-tag">val name = "Alice"   // Immutable variable
var age = 30         // Mutable variable</pre>
<p>Attempting to reassign name will result in a compile-time error. age can be reassigned to a different value.</p>
<div class="blog_h3"><span class="graybg">constants</span></div>
<p>In Kotlin, you can define constants using val and const val. Both are used for read-only values, but they differ in when and how their values are initialized and how they can be used.</p>
<p>val:</p>
<ol>
<li>Declares a read-only property or variable.</li>
<li>The value is assigned at runtime.</li>
<li>Can be used anywhere in the code.</li>
<li>Can hold any type, including objects.</li>
</ol>
<p>const val:</p>
<ol>
<li>Declares a compile-time constant.</li>
<li>The value is assigned at compile time.</li>
<li>Must be a top-level or member of an object or companion object.</li>
<li>Can only hold primitive types and String.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">const val MAX_COUNT = 100
const val APP_NAME = "MyApplication"

object Constants {
    const val PI = 3.1415926535
    const val E = 2.7182818284
}


class MathUtils {
    companion object {
        const val GOLDEN_RATIO = 1.6180339887
    }
}

// Accessing the constant
val ratio = MathUtils.GOLDEN_RATIO</pre>
<div class="blog_h3"><span class="graybg">Type Inference</span></div>
<p>Kotlin can infer the type of a variable from the assigned value, so specifying the type explicitly is optional. Examples:</p>
<pre class="crayon-plain-tag">val number = 42               // Type inferred as Int
val pi = 3.1415               // Type inferred as Double
var message: String = "Hello" // Type explicitly specified</pre>
<div class="blog_h3"><span class="graybg">Primitive Types and Their Equivalents</span></div>
<p>Kotlin does not have primitive types in the same way Java does. All types are objects. However, the compiler optimizes to use primitive types where possible for performance.</p>
<ol style="list-style-type: undefined;">
<li>Numeric Types: Byte, Short, Int, Long, Float, Double</li>
<li>Other Types: Char, Boolean, String</li>
</ol>
<p>Example: </p>
<pre class="crayon-plain-tag">val count: Int = 10
// Or with type inference
val count = 10</pre>
<div class="blog_h2"><span class="graybg">Null Safety</span></div>
<p>One of the most significant features of Kotlin is its approach to null safety, which helps prevent the dreaded NullPointerException.</p>
<div class="blog_h3"><span class="graybg">Nullable and Non-Nullable Types</span></div>
<p><span style="background-color: #c0c0c0;">By default, variables in Kotlin cannot hold a null value.</span> To allow a variable to hold null, you need to declare it as nullable by adding a <pre class="crayon-plain-tag">?</pre> after the type.</p>
<pre class="crayon-plain-tag">var nonNullable: String = "Hello"
// nonNullable = null // Compile-time error

var nullable: String? = "Hello"
nullable = null       // Allowed</pre>
<div class="blog_h3"><span class="graybg">Safe Calls</span></div>
<p>To safely access properties or methods of a nullable variable, use the safe call operator <pre class="crayon-plain-tag">?.</pre>. </p>
<pre class="crayon-plain-tag">// If nullable is not null, length will hold the length of the string.
// If nullable is null, length will be null.
val length = nullable?.length</pre>
<div class="blog_h3"><span class="graybg">Elvis Operator<br /></span></div>
<p>The Elvis operator <pre class="crayon-plain-tag">?:</pre> in Kotlin provides a concise way to handle nullable expressions by specifying a default value when an expression evaluates to null. It's particularly useful when you want to assign a value that might be null but have an alternative ready if it is.</p>
<pre class="crayon-plain-tag">// If expression is not null, result will be the value of expression.
// If expression is null, result will be defaultValue.
val result = expression ?: defaultValue</pre>
<div class="blog_h3"><span class="graybg">Non-Null Assertion Operator</span></div>
<p>The non-null assertion operator !! in Kotlin is used to explicitly assert that a nullable variable or expression is not null. If it is null, the operator will throw a <pre class="crayon-plain-tag">KotlinNullPointerException</pre> at runtime.</p>
<pre class="crayon-plain-tag">// If nullableValue is not null, nonNullableValue will hold its value.
// If nullableValue is null, a KotlinNullPointerException is thrown.
val nonNullableValue = nullableValue!!</pre>
<div class="blog_h3"><span class="graybg">Safe Casting</span></div>
<p>Use <pre class="crayon-plain-tag">as?</pre> for safe casting that returns null if the cast is unsuccessful.</p>
<pre class="crayon-plain-tag">val obj: Any = "Kotlin"
val str: String? = obj as? String</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java </span></div>
<p>In Java, all object references can be null, and there's no language-level enforcement to prevent NullPointerException. </p>
<pre class="crayon-plain-tag">String message = null;
int length = message.length(); // Throws NullPointerException</pre>
<p>To address issues related to null values, Java 8 introduced the <pre class="crayon-plain-tag">Optional</pre> class in the java.util package. Optional is a container object that may or may not contain a non-null value. It provides methods to handle the presence or absence of a value without directly using null.</p>
<p>Example of Using Optional:</p>
<pre class="crayon-plain-tag">import java.util.Optional;

Optional optionalMessage = Optional.ofNullable(getMessage());

if (optionalMessage.isPresent()) {
    int length = optionalMessage.get().length();
} else {
    int length = 0;
}</pre>
<p> Or using a functional style:</p>
<pre class="crayon-plain-tag">int length = optionalMessage.map(String::length).orElse(0);</pre>
<p>Limitations of Optional in Java:</p>
<ol style="list-style-type: undefined;">
<li>Not a Language-Level Feature: Optional is a library class, not a language construct. It doesn't enforce null safety at the type system level.</li>
<li>Limited Usage: It's primarily intended for return types and not recommended for fields or method parameters, limiting its scope.</li>
<li>Verbosity: Using Optional can make the code more verbose compared to Kotlin's null handling.</li>
<li>Performance Overhead: Wrapping values in Optional can introduce performance overhead due to additional object creation.</li>
</ol>
<div class="blog_h2"><span class="graybg">String Templates</span></div>
<p>Kotlin allows embedding variables and expressions within strings using string templates.</p>
<ol>
<li>Variable Interpolation: Use <pre class="crayon-plain-tag">$variableName</pre></li>
<li>Expression Interpolation: Use <pre class="crayon-plain-tag">${expression}</pre></li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">val name = "Alice"
val greeting = "Hello, $name!"
println(greeting) // Output: Hello, Alice!

val age = 30
println("In 5 years, ${name} will be ${age + 5} years old.")
// Output: In 5 years, Alice will be 35 years old.</pre>
<div class="blog_h2"><span class="graybg">Kotlin Operators Overview</span></div>
<p>In Kotlin, operators can be classified into various categories, similar to Java. However, there are some key differences. Below is a comprehensive table of Kotlin operators and their usage. Where applicable, significant differences between Kotlin and Java are highlighted.</p>
<div class="blog_h3"><span class="graybg">Assignment Operators</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Operator</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
<td style="text-align: center;"><strong>Kotlin Example</strong></td>
<td style="text-align: center;"><strong>Difference with Java</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>=</td>
<td>Simple assignment</td>
<td><pre class="crayon-plain-tag">a = b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>+=</td>
<td>Add and assign</td>
<td><pre class="crayon-plain-tag">a += b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>-=</td>
<td>Subtract and assign</td>
<td><pre class="crayon-plain-tag">a -= b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>*=</td>
<td>Multiply and assign</td>
<td><pre class="crayon-plain-tag">a *= b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>/=</td>
<td>Divide and assign</td>
<td><pre class="crayon-plain-tag">a /= b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>%=</td>
<td>Modulus and assign</td>
<td><pre class="crayon-plain-tag">a %= b</pre></td>
<td>No difference</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Arithmetic Operators</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Operator</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
<td style="text-align: center;"><strong>Kotlin Example</strong></td>
<td style="text-align: center;"><strong>Difference with Java</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>+</td>
<td>Addition</td>
<td><pre class="crayon-plain-tag">a + b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>-</td>
<td>Subtraction</td>
<td><pre class="crayon-plain-tag">a - b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>*</td>
<td>Multiplication</td>
<td><pre class="crayon-plain-tag">a * b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>/</td>
<td>Division</td>
<td><pre class="crayon-plain-tag">a / b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>%</td>
<td>Modulus</td>
<td><pre class="crayon-plain-tag">a % b</pre></td>
<td>No difference</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Comparison Operators</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Operator</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
<td style="text-align: center;"><strong>Kotlin Example</strong></td>
<td style="text-align: center;"><strong>Difference with Java</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>==</td>
<td>Equal to</td>
<td><pre class="crayon-plain-tag">a == b</pre></td>
<td>Kotlin compares values, Java compares references</td>
</tr>
<tr>
<td>!=</td>
<td>Not equal to</td>
<td><pre class="crayon-plain-tag">a != b</pre></td>
<td>Kotlin compares values, Java compares references</td>
</tr>
<tr>
<td>&gt;</td>
<td>Greater than</td>
<td><pre class="crayon-plain-tag">a &gt; b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>&lt;</td>
<td>Less than</td>
<td><pre class="crayon-plain-tag">a &lt; b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>&gt;=</td>
<td>Greater than or equal to</td>
<td><pre class="crayon-plain-tag">a &gt;= b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>&lt;=</td>
<td>Less than or equal to</td>
<td><pre class="crayon-plain-tag">a &lt;= b</pre></td>
<td>No difference</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Logical Operators</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Operator</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
<td style="text-align: center;"><strong>Kotlin Example</strong></td>
<td style="text-align: center;"><strong>Difference with Java</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>&amp;&amp;</td>
<td>Logical AND</td>
<td><pre class="crayon-plain-tag">a &amp;&amp; b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>||</td>
<td>Logical OR</td>
<td><pre class="crayon-plain-tag">a || b</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>!</td>
<td>Logical NOT</td>
<td><pre class="crayon-plain-tag">!a</pre></td>
<td>No difference</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Bitwise Operators</span></div>
<p>Unlike Java, Kotlin does not have specific bitwise operators (<code>&amp;</code>, <code>|</code>, etc.). Instead, it uses functions like <code>and()</code>, <code>or()</code>, <code>xor()</code>, <code>inv()</code> for bitwise operations.</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Operator</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
<td style="text-align: center;"><strong>Kotlin Example</strong></td>
<td style="text-align: center;"><strong>Difference with Java</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>and()</td>
<td>Bitwise AND</td>
<td><pre class="crayon-plain-tag">a.and(b)</pre></td>
<td>Kotlin uses a function instead of the <code>&amp;</code> operator</td>
</tr>
<tr>
<td>or()</td>
<td>Bitwise OR</td>
<td><pre class="crayon-plain-tag">a.or(b)</pre></td>
<td>Kotlin uses a function instead of the <code>|</code> operator</td>
</tr>
<tr>
<td>xor()</td>
<td>Bitwise XOR</td>
<td><pre class="crayon-plain-tag">a.xor(b)</pre></td>
<td>Kotlin uses a function instead of the <code>^</code> operator</td>
</tr>
<tr>
<td>inv()</td>
<td>Bitwise inversion</td>
<td><pre class="crayon-plain-tag">a.inv()</pre></td>
<td>Kotlin uses a function instead of the <code>~</code> operator</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Other Operators</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Operator</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
<td style="text-align: center;"><strong>Kotlin Example</strong></td>
<td style="text-align: center;"><strong>Difference with Java</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>in</td>
<td>Checks if a value is in a collection</td>
<td><pre class="crayon-plain-tag">x in array</pre></td>
<td>No Java equivalent</td>
</tr>
<tr>
<td>!in</td>
<td>Checks if a value is not in a collection</td>
<td><pre class="crayon-plain-tag">x !in array</pre></td>
<td>No Java equivalent</td>
</tr>
<tr>
<td>is</td>
<td>Checks if an object is of a certain type</td>
<td><pre class="crayon-plain-tag">x is String</pre></td>
<td>Kotlin uses <code>is</code> instead of <code>instanceof</code> in Java</td>
</tr>
<tr>
<td>as</td>
<td>Type casting</td>
<td><pre class="crayon-plain-tag">x as String</pre></td>
<td>Kotlin uses <code>as</code> for type casting</td>
</tr>
<tr>
<td>* (spread)</td>
<td>Spread operator (used to pass multiple arguments)</td>
<td><pre class="crayon-plain-tag">foo(*args)</pre></td>
<td>Kotlin uses the spread operator to pass arrays or varargs, while Java requires manual array expansion.</td>
</tr>
<tr>
<td>?.</td>
<td>Safe call operator (used to handle nullability)</td>
<td><pre class="crayon-plain-tag">a?.length</pre></td>
<td>No Java equivalent (in Java, null checks are required)</td>
</tr>
<tr>
<td>?:</td>
<td>Elvis operator (provides default value if null)</td>
<td><pre class="crayon-plain-tag">a ?: "default"</pre></td>
<td>No Java equivalent (in Java, null checks and ternary are needed)</td>
</tr>
<tr>
<td>!!</td>
<td>Not-null assertion (throws exception if value is null)</td>
<td><pre class="crayon-plain-tag">a!!</pre></td>
<td>No direct equivalent in Java (Java requires manual null checking and exception handling)</td>
</tr>
<tr>
<td>++</td>
<td>Increment (pre/post)</td>
<td><pre class="crayon-plain-tag">a++</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>--</td>
<td>Decrement (pre/post)</td>
<td><pre class="crayon-plain-tag">a--</pre></td>
<td>No difference</td>
</tr>
<tr>
<td>..</td>
<td>Range operator</td>
<td><pre class="crayon-plain-tag">1..5</pre></td>
<td>No direct Java equivalent</td>
</tr>
<tr>
<td>::</td>
<td>Callable reference (method or constructor)</td>
<td><pre class="crayon-plain-tag">::foo</pre></td>
<td>Similar to Java method references</td>
</tr>
<tr>
<td>[]</td>
<td>Index access</td>
<td><pre class="crayon-plain-tag">array[0]</pre></td>
<td>Same as Java</td>
</tr>
<tr>
<td>()</td>
<td>Invoke operator</td>
<td><pre class="crayon-plain-tag">myFunction()</pre></td>
<td>No direct Java equivalent (Kotlin allows operator overloading)</td>
</tr>
</tbody>
</table>
<p>Understanding these operators, especially those unique to Kotlin, like the spread operator and null-safe operators, will make coding more efficient and error-free compared to Java.</p>
<div class="blog_h2"><span class="graybg">Kotlin Keywords Overview</span></div>
<p>Kotlin reserves certain keywords for defining syntax elements like functions, classes, and more. These keywords cannot be used as identifiers (such as variable or function names) unless escaped with backticks. Below is a table listing all Kotlin keywords and their purposes.</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Keyword</strong></td>
<td style="text-align: center;"><strong>Description</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>abstract</td>
<td>Used to declare an abstract class or function.</td>
</tr>
<tr>
<td>annotation</td>
<td>Defines an annotation class.</td>
</tr>
<tr>
<td>as</td>
<td>Used for type casting.</td>
</tr>
<tr>
<td>break</td>
<td>Terminates the nearest enclosing loop.</td>
</tr>
<tr>
<td>by</td>
<td>Used for delegation and property delegation.</td>
</tr>
<tr>
<td>catch</td>
<td>Handles exceptions in a try block.</td>
</tr>
<tr>
<td>class</td>
<td>Defines a class.</td>
</tr>
<tr>
<td>companion</td>
<td>Declares a companion object within a class.</td>
</tr>
<tr>
<td>const</td>
<td>Declares compile-time constants.</td>
</tr>
<tr>
<td>continue</td>
<td>Skips the current iteration of the nearest enclosing loop.</td>
</tr>
<tr>
<td>crossinline</td>
<td>Prevents non-local returns from lambda expressions.</td>
</tr>
<tr>
<td>data</td>
<td>Declares a data class.</td>
</tr>
<tr>
<td>do</td>
<td>Used with `while` to create a do-while loop.</td>
</tr>
<tr>
<td>else</td>
<td>Specifies the alternative branch in an if-expression.</td>
</tr>
<tr>
<td>enum</td>
<td>Declares an enum class.</td>
</tr>
<tr>
<td>external</td>
<td>Marks a declaration as implemented in native code.</td>
</tr>
<tr>
<td>false</td>
<td>A boolean literal value representing "false".</td>
</tr>
<tr>
<td>final</td>
<td>Prevents a class or function from being overridden.</td>
</tr>
<tr>
<td>for</td>
<td>Used to create a for loop.</td>
</tr>
<tr>
<td>fun</td>
<td>Defines a function.</td>
</tr>
<tr>
<td>if</td>
<td>Specifies a conditional expression.</td>
</tr>
<tr>
<td>in</td>
<td>Checks if a value belongs to a range or collection.</td>
</tr>
<tr>
<td>inline</td>
<td>Used to request that a function be inlined.</td>
</tr>
<tr>
<td>inner</td>
<td>Declares an inner class that holds a reference to its outer class.</td>
</tr>
<tr>
<td>interface</td>
<td>Defines an interface.</td>
</tr>
<tr>
<td>is</td>
<td>Checks if a value is of a specific type.</td>
</tr>
<tr>
<td>lateinit</td>
<td>Delays initialization of a variable.</td>
</tr>
<tr>
<td>noinline</td>
<td>Prevents inlining of lambda expressions in inline functions.</td>
</tr>
<tr>
<td>null</td>
<td>A special literal representing "null".</td>
</tr>
<tr>
<td>object</td>
<td>Declares an object, which is a singleton.</td>
</tr>
<tr>
<td>open</td>
<td>Allows a class or function to be overridden.</td>
</tr>
<tr>
<td>operator</td>
<td>Marks a function as an operator for operator overloading.</td>
</tr>
<tr>
<td>out</td>
<td>Defines covariance in generics.</td>
</tr>
<tr>
<td>override</td>
<td>Overrides a function or property from a superclass or interface.</td>
</tr>
<tr>
<td>package</td>
<td>Declares the package for the file.</td>
</tr>
<tr>
<td>private</td>
<td>Defines the visibility of a declaration to be within the containing class or file.</td>
</tr>
<tr>
<td>protected</td>
<td>Defines visibility to be within the class and its subclasses.</td>
</tr>
<tr>
<td>public</td>
<td>Defines visibility to be accessible from anywhere.</td>
</tr>
<tr>
<td>return</td>
<td>Exits from the nearest enclosing function.</td>
</tr>
<tr>
<td>sealed</td>
<td>Declares a sealed class, which restricts subclassing to within the same file.</td>
</tr>
<tr>
<td>super</td>
<td>Refers to the superclass's implementation.</td>
</tr>
<tr>
<td>this</td>
<td>Refers to the current instance of a class.</td>
</tr>
<tr>
<td>throw</td>
<td>Throws an exception.</td>
</tr>
<tr>
<td>true</td>
<td>A boolean literal value representing "true".</td>
</tr>
<tr>
<td>try</td>
<td>Starts a block of code that may throw an exception.</td>
</tr>
<tr>
<td>typealias</td>
<td>Defines a new name for an existing type.</td>
</tr>
<tr>
<td>val</td>
<td>Declares a read-only property or local variable.</td>
</tr>
<tr>
<td>var</td>
<td>Declares a mutable property or local variable.</td>
</tr>
<tr>
<td>vararg</td>
<td>Allows a function to accept a variable number of arguments.</td>
</tr>
<tr>
<td>when</td>
<td>Acts as a replacement for the switch statement.</td>
</tr>
<tr>
<td>where</td>
<td>Specifies constraints on type parameters.</td>
</tr>
<tr>
<td>while</td>
<td>Starts a while loop.</td>
</tr>
</tbody>
</table>
<p>These keywords are essential for understanding the Kotlin language syntax. By knowing their purpose, you can write cleaner and more efficient Kotlin code. Some keywords like <pre class="crayon-plain-tag">val</pre>, <pre class="crayon-plain-tag">var</pre>, and <pre class="crayon-plain-tag">fun</pre> have no direct equivalent in Java, showcasing Kotlin's unique features.</p>
<div class="blog_h1"><span class="graybg">Control Flow Constructs</span></div>
<p>Control flow constructs are essential in any programming language as they dictate the order in which statements are executed. Kotlin offers a rich set of control flow statements that are both expressive and concise. In this section, we'll explore how Kotlin handles conditional statements and loops, highlighting the differences and similarities with Java.</p>
<div class="blog_h2"><span class="graybg">Conditional Statements</span></div>
<p>Kotlin provides powerful conditional statements, including <pre class="crayon-plain-tag">if</pre> expressions and the versatile <pre class="crayon-plain-tag">when</pre> expression. These constructs allow for more expressive and concise code compared to Java's traditional if-else and switch statements.</p>
<div class="blog_h3"><span class="graybg">if and else Expressions</span></div>
<p>In Kotlin, <span style="background-color: #c0c0c0;">if and else are expressions, meaning they return a value</span>. This allows you to assign the result of an if expression directly to a variable, enhancing code conciseness.</p>
<pre class="crayon-plain-tag">val result = if (condition) {
    // Block of code
    valueIfTrue
} else {
    // Block of code
    valueIfFalse
}</pre>
<p>Example: </p>
<pre class="crayon-plain-tag">val max = if (a &gt; b) a else b</pre>
<p>Equivalent Java Code:</p>
<pre class="crayon-plain-tag">int max = (a &gt; b) ? a : b;</pre>
<p>Kotlin allows for multiple else if branches within an if expression.</p>
<pre class="crayon-plain-tag">val grade = if (score &gt;= 90) {
    "A"
} else if (score &gt;= 80) {
    "B"
} else if (score &gt;= 70) {
    "C"
} else if (score &gt;= 60) {
    "D"
} else {
    "F"
}</pre>
<p>Equivalent Java Code:</p>
<pre class="crayon-plain-tag">String grade;
if (score &gt;= 90) {
    grade = "A";
} else if (score &gt;= 80) {
    grade = "B";
} else if (score &gt;= 70) {
    grade = "C";
} else if (score &gt;= 60) {
    grade = "D";
} else {
    grade = "F";
}</pre>
<div class="blog_h3"><span class="graybg">The When expression </span></div>
<p>Kotlin's when expression is a powerful and flexible construct that can replace Java's switch statement. Starting from Java 14, Java introduced switch expressions, which bring some of the capabilities of Kotlin's when to Java.</p>
<p>Syntax of Kotlin's when Expression:</p>
<pre class="crayon-plain-tag">when (expression) {
    value1 -&gt; result1
    value2, value3 -&gt; result2
    in range -&gt; result3
    !in range -&gt; result4
    is Type -&gt; result5
    else -&gt; defaultResult
}</pre>
<p>Example:</p>
<pre class="crayon-plain-tag">fun determineResponse(input: Any): String {
    return when (input) {
        // Matching a specific value
        1 -&gt; "You entered one."
        
        // Matching multiple values
        2, 3 -&gt; "You entered two or three."

        // Matching a value within a range
        in 4..10 -&gt; "Your number is in the range of 4 to 10."

        // Matching a value outside of a range
        !in 11..20 -&gt; "Your number is not in the range of 11 to 20."

        // Matching by type
        is String -&gt; "You entered a string."

        // Default case
        else -&gt; "I don't know what you entered."
    }
}</pre>
<p>Java 14 introduced switch expressions, which can <span style="background-color: #c0c0c0;">return a value and use the arrow syntax -&gt; </span></p>
<pre class="crayon-plain-tag">int day = 3;

String dayName = switch (day) {
    case 1 -&gt; "Monday";
    case 2 -&gt; "Tuesday";
    case 3 -&gt; "Wednesday";
    case 4 -&gt; "Thursday";
    case 5 -&gt; "Friday";
    case 6, 7 -&gt; "Weekend";
    default -&gt; "Invalid day";
};

System.out.println(dayName); // Output: Wednesday</pre>
<p>Kotlin's when can perform type checks using <pre class="crayon-plain-tag">is</pre>. Java 16 introduced Pattern Matching for instanceof, and Java 17 enhanced pattern matching in switch statements (preview feature). </p>
<pre class="crayon-plain-tag">// Kotlin Example:
fun describe(obj: Any): String =
    when (obj) {
        is Int -&gt; "Integer"
        is String -&gt; "String of length ${obj.length}"
        is Boolean -&gt; "Boolean"
        else -&gt; "Unknown"
    }

println(describe("Hello")) // Output: String of length 5</pre><br />
<pre class="crayon-plain-tag">// Java Example with Pattern Matching (instanceof) (Java 16 and Later):
public String describe(Object obj) {
    if (obj instanceof Integer) {
        return "Integer";
    } else if (obj instanceof String s) {
        return "String of length " + s.length();
    } else if (obj instanceof Boolean) {
        return "Boolean";
    } else {
        return "Unknown";
    }
}

// Java Example with Pattern Matching in switch (Java 17 Preview):
public String describe(Object obj) {
    return switch (obj) {
        case Integer i -&gt; "Integer";
        case String s -&gt; "String of length " + s.length();
        case Boolean b -&gt; "Boolean";
        default -&gt; "Unknown";
    };
}</pre>
<div class="blog_h2"><span class="graybg">Loops</span></div>
<p>Kotlin offers loops that are similar to Java's but with enhanced features and more concise syntax.</p>
<div class="blog_h3"><span class="graybg">for Loop</span></div>
<p>Kotlin's for loop is designed to iterate over any iterable, including ranges, arrays, and collections.</p>
<p>Iterating Over Ranges with Kotlin:</p>
<pre class="crayon-plain-tag">// Iterating Over Ranges
for (i in 1..5) {
    print("$i ") // Output: 1 2 3 4 5
}</pre>
<p>Java Equivalent Using for Loop: </p>
<pre class="crayon-plain-tag">for (int i = 1; i &lt;= 5; i++) {
    System.out.print(i + " "); // Output: 1 2 3 4 5
}</pre>
<p>Java Equivalent Using Streams (Java 8 and Later) :</p>
<pre class="crayon-plain-tag">IntStream.rangeClosed(1, 5).forEach(i -&gt; System.out.print(i + " "));
// Output: 1 2 3 4 5</pre>
<p>Iterating Over Collections with Kotlin:</p>
<pre class="crayon-plain-tag">val fruits = listOf("Apple", "Banana", "Cherry")

for (fruit in fruits) {
    println(fruit)
}</pre>
<p>Java enhanced for loop:</p>
<pre class="crayon-plain-tag">List fruits = List.of("Apple", "Banana", "Cherry");

for (String fruit : fruits) {
    System.out.println(fruit);
}</pre>
<div class="blog_h3"><span class="graybg">while and do-while Loops</span></div>
<p>These loops function similarly in both Kotlin and Java. </p>
<pre class="crayon-plain-tag">var count = 5
while (count &gt; 0) {
    println(count)
    count--
}</pre>
<div class="blog_h3"><span class="graybg">Ranges and Progressions</span></div>
<p>Kotlin provides range expressions that simplify loop constructs.</p>
<pre class="crayon-plain-tag">// Exclusive Range:
for (i in 1 until 5) {
    print("$i ") // Output: 1 2 3 4
}

// Downward Range:
for (i in 5 downTo 1) {
    print("$i ") // Output: 5 4 3 2 1
}


// Stepped Range:
for (i in 1..10 step 2) {
    print("$i ") // Output: 1 3 5 7 9
}</pre>
<p>Java can use IntStream with methods like range, rangeClosed, and custom steps. </p>
<pre class="crayon-plain-tag">IntStream.iterate(1, i -&gt; i + 2)
    .limit(5)
    .forEach(i -&gt; System.out.print(i + " "));
// Output: 1 3 5 7 9</pre>
<div class="blog_h3"><span class="graybg">break and continue with Labels </span></div>
<p>Kotlin allows labeled break and continue statements for controlling nested loops.</p>
<pre class="crayon-plain-tag">outer@ for (i in 1..5) {
    for (j in 1..5) {
        if (i * j &gt; 10) break@outer
        println("i = $i, j = $j")
    }
}</pre>
<p>Java Equivalent Using for labeled loops:</p>
<pre class="crayon-plain-tag">outer: // Label for the outer loop
for (int i = 1; i &lt;= 5; i++) {
    for (int j = 1; j &lt;= 5; j++) {
        if (i * j &gt; 10) {
            break outer; // Break the outer loop using the label
        }
        System.out.println("i = " + i + ", j = " + j);
    }
}</pre>
<div class="blog_h2"><span class="graybg">Exception Handling</span></div>
<p>Exception handling in Kotlin is similar to Java but with some key differences.</p>
<div class="blog_h3"><span class="graybg">Try-Catch Blocks</span></div>
<pre class="crayon-plain-tag">try {
    // Code that may throw an exception
} catch (e: ExceptionType) {
    // Handle exception
} finally {
    // Optional finally block
}</pre>
<p>Example:</p>
<pre class="crayon-plain-tag">try {
    val result = numerator / denominator
} catch (e: ArithmeticException) {
    println("Cannot divide by zero")
} finally {
    println("Execution completed")
}</pre>
<div class="blog_h3"><span class="graybg">Checked vs. Unchecked Exceptions </span></div>
<p>Kotlin:</p>
<ol style="list-style-type: undefined;">
<li>All exceptions are unchecked.</li>
<li>No need to declare exceptions with throws.</li>
<li>Reduces boilerplate code.</li>
</ol>
<p>Java:</p>
<ol style="list-style-type: undefined;">
<li>Differentiates between checked and unchecked exceptions.</li>
<li>Checked exceptions must be declared or handled.</li>
<li>Can lead to verbose code with try-catch blocks.</li>
</ol>
<div class="blog_h3"><span class="graybg">Try-with-Resources </span></div>
<p>Java 7 introduced the try-with-resources statement to manage resources automatically.</p>
<pre class="crayon-plain-tag">try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}</pre>
<p>Kotlin provides the <pre class="crayon-plain-tag">use</pre> extension function for resource management.</p>
<pre class="crayon-plain-tag">BufferedReader(FileReader("file.txt")).use { reader -&gt;
    var line = reader.readLine()
    while (line != null) {
        println(line)
        line = reader.readLine()
    }
}</pre>
<p>The use function ensures the resource is closed after use. </p>
<div class="blog_h3"><span class="graybg">Exception Handling with Coroutines</span></div>
<p>Kotlin's coroutines provide advanced exception handling mechanisms. <span style="background-color: #c0c0c0;">Exceptions in coroutines can be handled within the coroutine or propagated to the caller</span>.</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            // Coroutine code that may throw an exception
        } catch (e: Exception) {
            // Handle exception
        }
    }
    job.join()
}</pre>
<p>Java's Approach to Asynchronous Exception Handling:  Java uses CompletableFuture and ExecutorService for asynchronous programming, with exception handling mechanisms.</p>
<pre class="crayon-plain-tag">CompletableFuture future = CompletableFuture.runAsync(() -&gt; {
    try {
        // Asynchronous code
    } catch (Exception e) {
        // Handle exception
    }
});

future.join();</pre>
<div class="blog_h1"><span class="graybg">Functions in Kotlin</span></div>
<p>Functions are fundamental building blocks in Kotlin, and they come with a variety of features that enhance code readability, conciseness, and expressiveness. In this section, we'll explore how functions in Kotlin differ from those in Java.</p>
<div class="blog_h2"><span class="graybg">Function Declaration and Calling</span></div>
<div class="blog_h3"><span class="graybg">Basic Function Syntax</span></div>
<p>In Kotlin, functions are declared using the <pre class="crayon-plain-tag">fun</pre> keyword, followed by the function name, parameter list, and return type:</p>
<pre class="crayon-plain-tag">fun functionName(parameter1: Type1, parameter2: Type2): ReturnType {
    // function body
    return result
}

// Example
fun add(a: Int, b: Int): Int {
    return a + b
}</pre>
<div class="blog_h3"><span class="graybg">Single-Expression Functions</span></div>
<p>For functions that return a single expression, Kotlin allows you to simplify the syntax using the equals sign =.</p>
<pre class="crayon-plain-tag">fun multiply(a: Int, b: Int): Int = a * b
// If the return type can be inferred, you can omit it:
fun multiply(a: Int, b: Int) = a * b</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>In Java, methods must be declared within a class, and the syntax is more verbose.</p>
<pre class="crayon-plain-tag">public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}</pre>
<p>Starting from Java 8, you can define static methods in interfaces and use lambda expressions, but you still need to define methods within a class or interface.</p>
<p>Java Example with Lambda (Java 8 and Later):</p>
<pre class="crayon-plain-tag">BiFunction&lt;Integer, Integer, Integer&gt; add = (a, b) -&gt; a + b;
int sum = add.apply(5, 3);</pre>
<div class="blog_h2"><span class="graybg">Default and Named Arguments</span></div>
<div class="blog_h3"><span class="graybg">Default Arguments</span></div>
<p>Kotlin allows you to specify default values for function parameters. If an argument is not provided, the default value is used.</p>
<pre class="crayon-plain-tag">fun greet(name: String = "Guest") {
    println("Hello, $name!")
}

greet()            // Output: Hello, Guest!
greet("Alice")     // Output: Hello, Alice!</pre>
<div class="blog_h3"><span class="graybg">Named Arguments </span></div>
<p>When calling a function, you can specify the names of the parameters, allowing you to pass arguments in any order and enhance code readability.</p>
<pre class="crayon-plain-tag">fun displayInfo(name: String, age: Int, country: String) {
    println("$name is $age years old from $country.")
}

displayInfo(age = 30, country = "USA", name = "Bob")</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>In Java, prior to version 15, there is no direct support for default or named arguments in methods. You typically overload methods to achieve similar functionality.</p>
<p>Java Example with Method Overloading:</p>
<pre class="crayon-plain-tag">public void greet() {
    greet("Guest");
}

public void greet(String name) {
    System.out.println("Hello, " + name + "!");
}</pre>
<p>Named arguments are not supported in Java. However, starting from Java 15, the introduction of Records allows for more concise data carriers, but they don't provide named arguments for methods.</p>
<pre class="crayon-plain-tag">// Example Using Records in Java 15
public record Person(String name, int age) {}

public class Main {
    public static void main(String[] args) {
        // Creating an instance of Person
        Person person = new Person("Alice", 30);
        displayInfo(person);
    }

    public static void displayInfo(Person person) {
        System.out.println(person.name() + " is " + person.age() + " years old.");
    }
}</pre>
<div class="blog_h2"><span class="graybg">Extension Functions</span></div>
<p>Extension functions in Kotlin allow you to add new functions to existing classes without inheriting from them or using design patterns like Decorator.</p>
<pre class="crayon-plain-tag">fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}

val word = "level"
println(word.isPalindrome()) // Output: true</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>Java does not support extension functions directly. To achieve similar functionality, you would create utility classes with static methods.</p>
<p>Java Example:</p>
<pre class="crayon-plain-tag">public class StringUtils {
    public static boolean isPalindrome(String s) {
        return s.equals(new StringBuilder(s).reverse().toString());
    }
}

String word = "level";
System.out.println(StringUtils.isPalindrome(word)); // Output: true</pre>
<p>With Java 8 and later, you can use default methods in interfaces to provide implementations, but this requires modifying the interface and doesn't allow adding methods to existing classes like String. </p>
<div class="blog_h2"><span class="graybg">Higher-Order Functions and Lambdas</span></div>
<p>Kotlin treats functions as first-class citizens, meaning you can store functions in variables, pass them as parameters, and return them from other functions.</p>
<div class="blog_h3"><span class="graybg">Higher-Order Functions</span></div>
<p>A higher-order function is a function that takes functions as parameters or returns a function.</p>
<pre class="crayon-plain-tag">fun operate(a: Int, b: Int, operation: (Int, Int) -&gt; Int): Int {
    return operation(a, b)
}

// ::add syntax stands for a function reference
val sum = operate(4, 5, ::add)
println(sum) // Output: 9

// Using lambda expression
// Trailing Lambda Syntax: if the last parameter of the function is a lambda, Kotlin allows 
// you to move the lambda outside of the parentheses for improved readability:
val product = operate(4, 5) { x, y -&gt; x * y }
println(product) // Output: 20</pre>
<div class="blog_h3"><span class="graybg">Lambdas and Anonymous Functions</span></div>
<p>Lambda expressions provide a concise way to represent functions.</p>
<pre class="crayon-plain-tag">val lambdaName: (InputType) -&gt; ReturnType = { arguments -&gt; body }</pre>
<p>Example:</p>
<pre class="crayon-plain-tag">val square: (Int) -&gt; Int = { number -&gt; number * number }
println(square(6)) // Output: 36</pre>
<div class="blog_h3"><span class="graybg">Trailing Lambda</span></div>
<p>Kotlin provides a concise and expressive syntax for passing lambda expressions as function parameters. One of the features that enhance code readability is the trailing lambda syntax. This allows you to <span style="background-color: #c0c0c0;">pass a lambda expression after the function call parentheses</span>, which can make your code more readable, especially when working with higher-order functions.</p>
<pre class="crayon-plain-tag">// Standard syntax
functionName(parameters..., { lambda_parameters -&gt; lambda_body })

// Trailing lambda syntax
functionName(parameters...) { lambda_parameters -&gt; lambda_body }

// If the lambda is the only parameter
functionName { lambda_parameters -&gt; lambda_body }</pre>
<p>Examples:</p>
<pre class="crayon-plain-tag">fun performOperation(x: Int, operation: (Int) -&gt; Int): Int {
    return operation(x)
}

// Standard Syntax
val result = performOperation(5, { num -&gt; num * num })
println(result) // Output: 25


// Trailing Lambda Syntax
val result = performOperation(5) { num -&gt;
    num * num
}
println(result) // Output: 25</pre>
<p>If a function takes only a lambda parameter, you can omit the parentheses entirely.</p>
<pre class="crayon-plain-tag">fun repeatAction(times: Int, action: () -&gt; Unit) {
    for (i in 1..times) {
        action()
    }
}

repeatAction(3) {
    println("Hello, World!")
}
// Output:
// Hello, World!
// Hello, World!
// Hello, World!</pre>
<p>Kotlin's standard library provides many functions that take lambdas as parameters. Trailing lambdas can make these calls more readable.</p>
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3, 4, 5)

// Standard syntax
val evenNumbers = numbers.filter({ it % 2 == 0 })

// Trailing lambda syntax
val evenNumbers = numbers.filter { it % 2 == 0 }

println(evenNumbers) // Output: [2, 4]


val result = numbers
    .filter { it &gt; 2 }
    .map { it * it }
    .also { println("Squared numbers: $it") }</pre>
<div class="blog_h3"><span class="graybg">Using the it Keyword in Lambda </span></div>
<p>In lambdas with a single parameter, you can omit the parameter declaration and use the implicit <pre class="crayon-plain-tag">it</pre> variable.</p>
<pre class="crayon-plain-tag">val squares = numbers.map { it * it }
println(squares) // Output: [1, 4, 9, 16, 25]
 </pre>
<div class="blog_h3"><span class="graybg">Inline Functions</span></div>
<p>Kotlin allows you to declare functions as <pre class="crayon-plain-tag">inline</pre> to reduce overhead associated with higher-order functions.</p>
<pre class="crayon-plain-tag">inline fun performOperation(a: Int, b: Int, operation: (Int, Int) -&gt; Int): Int {
    return operation(a, b)
}</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java </span></div>
<p>Java 8 introduced lambda expressions and functional interfaces, enabling functional programming paradigms.</p>
<pre class="crayon-plain-tag">BiFunction&lt;Integer, Integer, Integer&gt; add = (a, b) -&gt; a + b;
int sum = add.apply(4, 5);
System.out.println(sum); // Output: 9</pre>
<p>Java doesn't support higher-order functions in the same way as Kotlin. You can pass functional interfaces as parameters, but the syntax is more verbose. </p>
<pre class="crayon-plain-tag">public int operate(int a, int b, BiFunction&lt;Integer, Integer, Integer&gt; operation) {
    return operation.apply(a, b);
}

int product = operate(4, 5, (x, y) -&gt; x * y);
System.out.println(product); // Output: 20</pre>
<div class="blog_h2"><span class="graybg">Tail Recursion</span></div>
<p>Kotlin supports tail recursive functions using the <pre class="crayon-plain-tag">tailrec</pre> modifier, which optimizes recursive calls to prevent stack overflow errors.</p>
<pre class="crayon-plain-tag">tailrec fun factorial(n: Int, accumulator: Int = 1): Int {
    return if (n &lt;= 1) accumulator else factorial(n - 1, n * accumulator)
}

println(factorial(5)) // Output: 120</pre>
<p>The tailrec modifier in Kotlin transforms a recursive function into an iterative one during compilation, optimizing it to prevent stack overflow issues that can occur with deep recursion. This is achieved by performing tail call optimization (TCO). Here's a breakdown of the exact effect of the tailrec keyword:</p>
<ol>
<li>Tail Call Optimization (TCO): In Kotlin, <span style="background-color: #c0c0c0;">a function call is considered a tail call if it's the last operation to be executed in a function</span>. If the recursive call is in the tail position (i.e., it is the last thing the function does before returning), Kotlin replaces the recursive call with a loop during compilation, avoiding the need for additional stack frames for each recursive call.</li>
<li>Stack Frame Elimination: Normally, every function call adds a new stack frame, which can lead to stack overflow errors for deep recursion. The tailrec modifier removes the need for these additional frames by reusing the current function’s stack frame.</li>
<li>Iterative Approach: When you mark a function with tailrec, the compiler rewrites the recursive function into a loop under the hood. This makes the recursion as efficient as a traditional loop, avoiding the overhead of managing recursion in the stack.</li>
</ol>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>Java does not have built-in support for tail call optimization. Recursive functions can lead to stack overflow errors if not carefully managed.</p>
<div class="blog_h2"><span class="graybg">Local Functions and Closures</span></div>
<p>Kotlin allows you to define functions inside other functions, and these inner functions can access variables from the outer function.</p>
<pre class="crayon-plain-tag">fun greeting(): () -&gt; Unit {
    val message = "Hello"
    fun sayHello() {
        println(message)
    }
    return ::sayHello
}

val greet = greeting()
greet() // Output: Hello</pre>
<p>A function inside another function is called a Closure because it "close over" its surrounding environment, meaning it captures and remembers the state of variables from the outer function, even after that outer function has finished executing.</p>
<div class="blog_h3"><span class="graybg"> Comparison with Java</span></div>
<p>Java supports lambda expressions and anonymous inner classes but does not support defining named local functions inside methods.</p>
<div class="blog_h2"><span class="graybg">Function Literals with Receiver</span></div>
<p>Kotlin allows you to define lambda expressions that have a receiver object, enabling a DSL-like syntax.</p>
<pre class="crayon-plain-tag">// The lambda (builderAction) is an extension function on StringBuilder that doesn't return any value (Unit).
// Unit is a special type that is used to indicate the absence of a meaningful return value from a function. 
// It is similar to void in languages like Java or C, but it is treated as a real type in Kotlin, and it has
// only one value, which is Unit
fun buildString(builderAction: StringBuilder.() -&gt; Unit): String {
    val sb = StringBuilder()
    sb.builderAction()
    return sb.toString()
}

val result = buildString {
    append("Hello, ")
    append("World!")
}

println(result) // Output: Hello, World!</pre>
<p>The <pre class="crayon-plain-tag">{ ... }</pre> after buildString is Kotlin's trailing lambda syntax for better readability. </p>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>Java does not support function literals with receivers. Achieving similar functionality would require more verbose code and design patterns.</p>
<div class="blog_h2"><span class="graybg">Operator Overloading</span></div>
<p>Kotlin allows you to provide implementations for predefined operators for your own types by overloading them. </p>
<pre class="crayon-plain-tag">// In Kotlin, a data class is a special type of class that is primarily used to hold data. 
// It automatically generates useful methods like equals(), hashCode(), toString(), and copy() for you, 
// based on the properties you define. This makes it especially handy when creating simple classes whose 
// main purpose is to store state (i.e., the values of its properties).
data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point) = Point(x + other.x, y + other.y)
}

val p1 = Point(2, 3)
val p2 = Point(4, 5)
val sum = p1 + p2
println(sum) // Output: Point(x=6, y=8)</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>Java does not support operator overloading, except for the + operator for string concatenation. You would need to define methods like add to achieve similar functionality. </p>
<div class="blog_h2"><span class="graybg">Coroutines</span></div>
<p>Although coroutines are covered in detail in a later section, it's worth mentioning that Kotlin functions can be declared with the <pre class="crayon-plain-tag">suspend</pre> modifier to support asynchronous operations.</p>
<pre class="crayon-plain-tag">suspend fun fetchData(): String {
    // Simulate a long-running operation
    delay(1000)
    return "Data fetched"
}</pre>
<p>suspend functions are special functions in Kotlin that <span style="background-color: #c0c0c0;">can be paused and resumed at a later time without blocking the thread they are running on</span>.</p>
<p>To use the fetchData() function, you'll <span style="background-color: #c0c0c0;">need to call it within a coroutine scope, as suspend functions can only be called from within another suspend function or a coroutine</span>.</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*

fun main() {
    // Start a new coroutine:  This creates a coroutine scope that blocks the main thread 
    // until all coroutines inside it complete.
    runBlocking {
        println("Fetching data...")
        val result = fetchData() // Calls the suspend function
        println(result) // Prints: Data fetched
    }
}</pre>
<div class="blog_h3"><span class="graybg">Comparison with Java</span></div>
<p>Starting from Java 19, Virtual Threads (Project Loom) have been introduced to support lightweight concurrency. However, as of Java 21, coroutines as language constructs are not available. Asynchronous programming in Java is typically handled using CompletableFuture, reactive streams, or third-party libraries.</p>
<p>Java Example with CompletableFuture:</p>
<pre class="crayon-plain-tag">CompletableFuture future = CompletableFuture.supplyAsync(() -&gt; {
    // Simulate long-running operation
    Thread.sleep(1000);
    return "Data fetched";
});

future.thenAccept(System.out::println);</pre>
<div class="blog_h2"><span class="graybg">Special Kotlin Functions</span></div>
<p>Kotlin provides several special functions like <pre class="crayon-plain-tag">with</pre>, <pre class="crayon-plain-tag">apply</pre>, <pre class="crayon-plain-tag">run</pre>, <pre class="crayon-plain-tag">let</pre>, and more to simplify common operations. These functions primarily help manage scope, allow concise object configuration, and reduce boilerplate code. Let's explore these functions with examples.</p>
<div class="blog_h3"><span class="graybg">with</span></div>
<p>The <pre class="crayon-plain-tag">with</pre> function is used to call multiple functions on the same object without repeating its name. It is typically used for operating on an object in a block of code.</p>
<pre class="crayon-plain-tag">data class User(val name: String, var age: Int, var city: String)

fun main() {
    val user = User("John", 25, "New York")
    with(user) {
        println(name)  // Access `name` directly
        age += 1
        println("Updated age: $age")
    }
}</pre>
<p>Output:</p>
<pre class="crayon-plain-tag">John
Updated age: 26</pre>
<p>In the example, <pre class="crayon-plain-tag">with</pre> allows access to the properties of <pre class="crayon-plain-tag">user</pre> without explicitly referring to the object.</p>
<div class="blog_h3"><span class="graybg">apply</span></div>
<p>The <pre class="crayon-plain-tag">apply</pre> function is used to initialize or configure an object. It returns the object itself after applying the configuration.</p>
<pre class="crayon-plain-tag">val user = User("John", 25, "New York").apply {
    age = 26
    city = "San Francisco"
}
println(user)  // Output: User(name=John, age=26, city=San Francisco)</pre>
<p>The <pre class="crayon-plain-tag">apply</pre> function is commonly used for initializing or setting properties in a concise manner.</p>
<div class="blog_h3"><span class="graybg">let</span></div>
<p>The <pre class="crayon-plain-tag">let</pre> function is useful for performing operations on a non-null object and is often used in combination with the safe-call operator (<pre class="crayon-plain-tag">?.</pre>).</p>
<pre class="crayon-plain-tag">val name: String? = "John"
name?.let {
    println("Hello, $it")  // Output: Hello, John
}</pre>
<p>If <pre class="crayon-plain-tag">name</pre> is not null, <pre class="crayon-plain-tag">let</pre> executes the block and prints the value.</p>
<div class="blog_h3"><span class="graybg">run</span></div>
<p>The <pre class="crayon-plain-tag">run</pre> function is similar to <pre class="crayon-plain-tag">let</pre>, but instead of returning the object, it returns the result of the lambda expression. It's great for scoping and executing code in a context.</p>
<pre class="crayon-plain-tag">val user = User("John", 25, "New York")
val result = user.run {
    "User's name is $name, age is $age"
}
println(result)  // Output: User's name is John, age is 25</pre>
<p>In this example, <pre class="crayon-plain-tag">run</pre> returns the result of the expression, not the object itself.</p>
<div class="blog_h3"><span class="graybg">also</span></div>
<p>The <pre class="crayon-plain-tag">also</pre> function is similar to <pre class="crayon-plain-tag">apply</pre>, but it is used to perform additional actions such as logging, side-effects, or validation without affecting the object’s state. It returns the object itself.</p>
<pre class="crayon-plain-tag">val user = User("John", 25, "New York").also {
    println("User created: $it")
}
println(user)  // Output: User(name=John, age=25, city=New York)</pre>
<p><pre class="crayon-plain-tag">also</pre> is often used when you need to perform operations like logging or debugging without changing the object.</p>
<div class="blog_h3"><span class="graybg">takeIf and takeUnless</span></div>
<p>The <pre class="crayon-plain-tag">takeIf</pre> function returns the object if the provided predicate is true; otherwise, it returns null. The opposite is <pre class="crayon-plain-tag">takeUnless</pre>, which returns the object if the predicate is false.</p>
<pre class="crayon-plain-tag">val user = User("John", 25, "New York")

val result = user.takeIf { it.age &gt;= 18 }  // Returns user if age &gt;= 18
println(result)  // Output: User(name=John, age=25, city=New York)

val resultNull = user.takeUnless { it.age &lt; 18 }  // Returns user if age &gt;= 18
println(resultNull)  // Output: User(name=John, age=25, city=New York)</pre>
<p>Both <pre class="crayon-plain-tag">takeIf</pre> and <pre class="crayon-plain-tag">takeUnless</pre> are useful for performing conditional operations on an object.</p>
<p>These special functions in Kotlin provide a powerful way to write concise and expressive code. By using these functions effectively, you can avoid boilerplate code and make your programs easier to understand and maintain.</p>
<div class="blog_h1"><span class="graybg">Object-Oriented Programming</span></div>
<p>Object-Oriented Programming (OOP) is a paradigm centered around objects and classes, enabling developers to model real-world entities and relationships in code. Both Kotlin and Java are object-oriented languages, but Kotlin introduces several enhancements and syntactic sugar to make OOP more concise and expressive. This section explores how Kotlin handles OOP concepts compared to Java. </p>
<div class="blog_h2"><span class="graybg">Classes and Objects</span></div>
<div class="blog_h3"><span class="graybg">Class Declaration</span></div>
<p>In Kotlin, classes are declared using the <pre class="crayon-plain-tag">class</pre> keyword, and you can define properties and methods within them. Unlike Java, <span style="background-color: #c0c0c0;">Kotlin does not require you to place each class in a separate file or match the filename with the class name</span>.</p>
<p>Example:</p>
<pre class="crayon-plain-tag">class Person {
    var name: String = ""
    var age: Int = 0

    fun greet() {
        println("Hello, my name is $name.")
    }
}</pre>
<p>Equivalent Java code:</p>
<pre class="crayon-plain-tag">public class Person {
    private String name = "";
    private int age = 0;

    public void greet() {
        System.out.println("Hello, my name is " + name + ".");
    }
}</pre>
<div class="blog_h3"><span class="graybg">Primary Constructors </span></div>
<p>Kotlin introduces the concept of primary constructors, which are <span style="background-color: #c0c0c0;">declared in the class header and can initialize properties directly</span>.</p>
<pre class="crayon-plain-tag">// val name: String declares an immutable property.
// var age: Int declares a mutable property.
// Properties are initialized through the constructor.
class Person(val name: String, var age: Int) {
    fun greet() {
        println("Hello, my name is $name.")
    }
}

// Instantiate a person
val personInstance = Person(nameArgument, ageArgument)</pre>
<p>Equivalent Java code: </p>
<pre class="crayon-plain-tag">public class Person {
    private final String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void greet() {
        System.out.println("Hello, my name is " + name + ".");
    }
    
    // Getters and setters omitted for brevity
}

// Instantiate a person
Person personInstance = new Person(nameArgument, ageArgument);</pre>
<div class="blog_h3"><span class="graybg">Initializer Blocks</span></div>
<p>In Kotlin, initializer blocks can be used to execute code during object creation.</p>
<pre class="crayon-plain-tag">class Person(val name: String, var age: Int) {
    init {
        println("Person initialized with name = $name and age = $age")
    }
}</pre>
<div class="blog_h3"><span class="graybg">Secondary Constructors</span></div>
<p>Kotlin allows secondary constructors for additional initialization logic. </p>
<pre class="crayon-plain-tag">class Person {
    var name: String
    var age: Int

    constructor(name: String) {
        this.name = name
        this.age = 0
    }

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}</pre>
<p>Please note that Secondary Constructors are less common due to the flexibility of default parameters.</p>
<div class="blog_h3"><span class="graybg">Properties and Fields </span></div>
<p>In Kotlin, properties are a central concept that combines a field (to hold data) and optional accessors (getters and setters) into a single, concise syntax. This approach simplifies code and reduces boilerplate compared to Java, where you typically need to declare private fields and provide public getter and setter methods separately.</p>
<pre class="crayon-plain-tag">//  declares a mutable property name of type String with an initial value of "Unknown".
var name: String = "Unknown"</pre>
<p>For a mutable property declared with <pre class="crayon-plain-tag">var</pre>, Kotlin automatically generates:</p>
<ol style="list-style-type: undefined;">
<li>A getter method to retrieve the property's value.</li>
<li>A setter method to set or modify the property's value.</li>
</ol>
<p>For an immutable property declared with <pre class="crayon-plain-tag">val</pre>, Kotlin only generates a getter. </p>
<p>You can customize the getter and setter if you need additional logic when accessing or modifying the property. Syntax for Custom Accessors:</p>
<pre class="crayon-plain-tag">var propertyName: Type = initialValue
    get() {
        // Custom getter logic
    }
    set(value) {
        // Custom setter logic
    }</pre>
<p>Within the getter and setter, <pre class="crayon-plain-tag">field</pre> is a special backing field identifier provided by Kotlin. It refers to the actual storage of the property's value.</p>
<pre class="crayon-plain-tag">var name: String = "Unknown"
    get() = field
    set(value) {
        field = value.capitalize()
    }</pre>
<p>In Java, we always need to explicitly implement getter and setter:</p>
<pre class="crayon-plain-tag">private String name = "Unknown";

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = capitalize(name);
}

private String capitalize(String value) {
    // Capitalize logic
}</pre>
<div class="blog_h2"><span class="graybg">Inheritance and Interfaces</span></div>
<div class="blog_h3"><span class="graybg">Inheritance</span></div>
<p>I<span style="background-color: #c0c0c0;">n Kotlin, classes are final by default</span>. To allow a class to be subclassed, you must mark it with the <pre class="crayon-plain-tag">open</pre> keyword.</p>
<pre class="crayon-plain-tag">open class Animal {
    open fun sound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    override fun sound() {
        println("Bark")
    }
}</pre>
<p>In Java, classes and methods are open for extension by default unless marked as final:</p>
<pre class="crayon-plain-tag">public class Animal {
    public void sound() {
        System.out.println("Some sound");
    }
}

public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("Bark");
    }
}</pre>
<div class="blog_h3"><span class="graybg">Interfaces </span></div>
<p>Kotlin interfaces<span style="background-color: #c0c0c0;"> can contain both abstract methods and method implementations</span>.</p>
<pre class="crayon-plain-tag">interface Movable {
    fun move()
    fun stop() {
        println("Stopped moving")
    }
}

class Vehicle : Movable {
    override fun move() {
        println("Vehicle is moving")
    }
}</pre>
<p>From Java 8,  interfaces can provide default implementations:</p>
<pre class="crayon-plain-tag">public interface Movable {
    void move();
    
    default void stop() {
        System.out.println("Stopped moving");
    }
}

public class Vehicle implements Movable {
    @Override
    public void move() {
        System.out.println("Vehicle is moving");
    }
}</pre>
<div class="blog_h2"><span class="graybg">Multiple Inheritance of Behavior</span></div>
<p>Kotlin allows a class to implement multiple interfaces, and if <span style="background-color: #c0c0c0;">there are conflicts, you must override the conflicting methods</span>.</p>
<pre class="crayon-plain-tag">interface A {
    fun show() {
        println("A")
    }
}

interface B {
    fun show() {
        println("B")
    }
}

class C : A, B {
    override fun show() {
        super<a>.show()
        super<b>.show()
    }
}

val c = C()
c.show()
// Output:
// A
// B
</b></a></pre>
<p>In Java you need to do something similar to solve the conflict:</p>
<pre class="crayon-plain-tag">public interface A {
    default void show() {
        System.out.println("A");
    }
}

public interface B {
    default void show() {
        System.out.println("B");
    }
}

public class C implements A, B {
    @Override
    public void show() {
        A.super.show();
        B.super.show();
    }
}</pre>
<div class="blog_h2"><span class="graybg">Data Classes</span></div>
<p>Data classes in Kotlin are designed to hold data. The compiler automatically generates <pre class="crayon-plain-tag">equals()</pre>, <pre class="crayon-plain-tag">hashCode()</pre>, <pre class="crayon-plain-tag">toString()</pre>, and <pre class="crayon-plain-tag">copy()</pre> methods.</p>
<pre class="crayon-plain-tag">data class User(val name: String, val age: Int)

val user1 = User("Alice", 30)
println(user1) // Output: User(name=Alice, age=30)

val user2 = user1.copy(age = 31)
println(user2) // Output: User(name=Alice, age=31)</pre>
<p>Java Equivalent Prior to Records:</p>
<pre class="crayon-plain-tag">public class User {
    private final String name;
    private final int age;
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Getters, equals(), hashCode(), and toString() methods
    // Need to be manually implemented or generated by the IDE
}</pre>
<p>Java Equivalent with Records ( Java 16+ ):</p>
<pre class="crayon-plain-tag">public record User(String name, int age) {}

User user1 = new User("Alice", 30);
System.out.println(user1); // Output: User[name=Alice, age=30]

User user2 = new User(user1.name(), 31);
System.out.println(user2); // Output: User[name=Alice, age=31]</pre>
<div class="blog_h2"><span class="graybg">Sealed Classes and Interfaces</span></div>
<p>Sealed classes and interfaces restrict the hierarchy to a finite set of subclasses, known at compile time.</p>
<pre class="crayon-plain-tag">sealed class Result

// Declares a data class Success that inherits from Result
data class Success(val data: String) : Result()
data class Error(val exception: Exception) : Result()
// Declares a singleton object Loading that inherits from Result.
object Loading : Result()</pre>
<p>Java 17 introduced sealed classes and interfaces. </p>
<pre class="crayon-plain-tag">public sealed class Result permits Success, Error, Loading {}

public final class Success extends Result {
    private final String data;
    // Constructor, getters, etc.
}

public final class Error extends Result {
    private final Exception exception;
    // Constructor, getters, etc.
}

public final class Loading extends Result {}</pre>
<div class="blog_h2"><span class="graybg">Visibility Modifiers</span></div>
<p>Kotlin offers several visibility modifiers:</p>
<ol style="list-style-type: undefined;">
<li><span style="background-color: #c0c0c0;">public (default): Visible everywhere.</span></li>
<li><span style="background-color: #c0c0c0;">internal: Visible within the same module.</span></li>
<li>protected: Visible to subclasses.</li>
<li>private: Visible within the containing declaration.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">class Example {
    private val x = 1
    internal val y = 2
    protected val z = 3
    val w = 4 // Public by default
}</pre>
<p>Java Visibility Modifiers: </p>
<ol style="list-style-type: undefined;">
<li>public: Visible everywhere.</li>
<li>protected: Visible within the package and subclasses.</li>
<li><span style="background-color: #c0c0c0;">Package-private (default): Visible within the package.</span></li>
<li>private: Visible within the class.</li>
</ol>
<div class="blog_h2"><span class="graybg">Nested and Inner Classes</span></div>
<div class="blog_h3"><span class="graybg">Nested Classes</span></div>
<p>In Kotlin, <span style="background-color: #c0c0c0;">a nested class is static by default</span>. It does not hold a reference to the outer class.</p>
<pre class="crayon-plain-tag">class Outer {
    class Nested {
        fun hello() = "Hello from Nested"
    }
}

val message = Outer.Nested().hello()
println(message) // Output: Hello from Nested</pre>
<p>Java static netsted class:</p>
<pre class="crayon-plain-tag">public class Outer {
    public static class Nested {
        public String hello() {
            return "Hello from Nested";
        }
    }
}

String message = new Outer.Nested().hello();
System.out.println(message); // Output: Hello from Nested</pre>
<div class="blog_h3"><span class="graybg">Inner Classes</span></div>
<p>An inner class in Kotlin holds a reference to the outer class and can access its members.</p>
<pre class="crayon-plain-tag">class Outer(val name: String) {
    inner class Inner {
        fun greet() = "Hello from $name's Inner"
    }
}

val outer = Outer("Kotlin")
val message = outer.Inner().greet()
println(message) // Output: Hello from Kotlin's Inner</pre>
<p>Java Inner Class:</p>
<pre class="crayon-plain-tag">public class Outer {
    private String name;

    public Outer(String name) {
        this.name = name;
    }

    public class Inner {
        public String greet() {
            return "Hello from " + name + "'s Inner";
        }
    }
}

Outer outer = new Outer("Java");
String message = outer.new Inner().greet();
System.out.println(message); // Output: Hello from Java's Inner</pre>
<div class="blog_h3"><span class="graybg">Nested/Inner Data Class</span></div>
<p>Data classes can also be nested or inner class:</p>
<pre class="crayon-plain-tag">// Nested
data class Person(val name: String, val age: Int) {
    data class Address(val street: String, val city: String)
}

fun main() {
    val address = Person.Address("Main St", "Springfield")
    val person = Person("John Doe", 30)

    println(person)  // Output: Person(name=John Doe, age=30)
    println(address) // Output: Address(street=Main St, city=Springfield)
}


// Inner
data class Person(val name: String, val age: Int) {
    inner data class Address(val street: String, val city: String) {
        fun getFullAddress(): String {
            return "$name lives at $street, $city"  // Accessing outer class (Person) properties
        }
    }
}

fun main() {
    val person = Person("John Doe", 30)
    val address = person.Address("Main St", "Springfield")

    println(address.getFullAddress())  // Output: John Doe lives at Main St, Springfield
}</pre>
<div class="blog_h2"><span class="graybg">Object Declarations and Companion Objects</span></div>
<div class="blog_h3"><span class="graybg">Singleton Objects</span></div>
<p>Kotlin provides a concise way to create singleton objects using <pre class="crayon-plain-tag">object</pre> declarations.</p>
<pre class="crayon-plain-tag">object Singleton {
    fun greet() = "Hello from Singleton"
}

println(Singleton.greet()) // Output: Hello from Singleton</pre>
<p> In Java, you typically use a class with a private constructor and a static instance.</p>
<pre class="crayon-plain-tag">public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public String greet() {
        return "Hello from Singleton";
    }
}

System.out.println(Singleton.getInstance().greet()); // Output: Hello from Singleton</pre>
<div class="blog_h3"><span class="graybg">Companion Objects</span></div>
<p>In Kotlin, companion objects can hold static members and factory methods.</p>
<pre class="crayon-plain-tag">class MyClass {
    companion object {
        const val CONSTANT = 100
        fun create(): MyClass = MyClass()
    }
}

println(MyClass.CONSTANT) // Output: 100
val instance = MyClass.create()</pre>
<p>Java uses static members and methods.</p>
<pre class="crayon-plain-tag">public class MyClass {
    public static final int CONSTANT = 100;

    public static MyClass create() {
        return new MyClass();
    }
}

System.out.println(MyClass.CONSTANT); // Output: 100
MyClass instance = MyClass.create();</pre>
<div class="blog_h2"><span class="graybg">Delegation</span></div>
<div class="blog_h3"><span class="graybg">Class Delegation</span></div>
<p>Kotlin supports delegation of interface implementation to another object.</p>
<pre class="crayon-plain-tag">interface Printer {
    fun print()
}

class ConsolePrinter : Printer {
    override fun print() {
        println("Printing to console")
    }
}

class PrinterManager(printer: Printer) : Printer by printer

val manager = PrinterManager(ConsolePrinter())
manager.print() // Output: Printing to console</pre>
<p>The syntax <pre class="crayon-plain-tag">Printer by printer</pre>in the above example is called <span style="background-color: #c0c0c0;">delegation in Kotlin. It means that PrinterManager will automatically forward (delegate) all calls to the print() method to the printer object that was passed in</span>.</p>
<p>Java does not have built-in support for class delegation. You would need to implement the methods and delegate calls manually.</p>
<pre class="crayon-plain-tag">public interface Printer {
    void print();
}

public class ConsolePrinter implements Printer {
    @Override
    public void print() {
        System.out.println("Printing to console");
    }
}

public class PrinterManager implements Printer {
    private final Printer printer;

    public PrinterManager(Printer printer) {
        this.printer = printer;
    }

    @Override
    public void print() {
        printer.print();
    }
}

PrinterManager manager = new PrinterManager(new ConsolePrinter());
manager.print(); // Output: Printing to console</pre>
<div class="blog_h3"><span class="graybg">Property Delegation</span></div>
<p>Kotlin also allows delegation of property getters and setters.</p>
<pre class="crayon-plain-tag">import kotlin.properties.Delegates

class User {
    // A mutable property (var) named name of type String
    // Use Kotlin's Delegates.observable delegate to manage the behavior of the name property. 
    //   This function allows you to observe changes to the property and take some action 
    //   whenever the property's value is changed.
    // The initial value of the name property is set to ""
    // The last parameter of Delegates.observable is a
    var name: String by Delegates.observable("") { prop, old, new -&gt;
        println("Property '${prop.name}' changed from '$old' to '$new'")
    }
}

val user = User()
user.name = "Alice" // Output: Property 'name' changed from '' to 'Alice'</pre>
<div class="blog_h2"><span class="graybg">Abstract Classes </span></div>
<p>Abstract classes in Kotlin are declared using the abstract keyword and can contain abstract members that must be implemented by subclasses.</p>
<pre class="crayon-plain-tag">abstract class Vehicle {
    abstract fun drive()
    fun stop() {
        println("Vehicle stopped")
    }
}

class Car : Vehicle() {
    override fun drive() {
        println("Car is driving")
    }
}

val car = Car()
car.drive() // Output: Car is driving
car.stop()  // Output: Vehicle stopped</pre>
<p>Equivalent Java code:</p>
<pre class="crayon-plain-tag">public abstract class Vehicle {
    public abstract void drive();
    
    public void stop() {
        System.out.println("Vehicle stopped");
    }
}

public class Car extends Vehicle {
    @Override
    public void drive() {
        System.out.println("Car is driving");
    }
}

Car car = new Car();
car.drive(); // Output: Car is driving
car.stop();  // Output: Vehicle stopped</pre>
<div class="blog_h2"><span class="graybg">Enum Classes</span></div>
<p>Enum classes represent a fixed set of constants.</p>
<pre class="crayon-plain-tag">enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

val dir = Direction.NORTH
println(dir) // Output: NORTH</pre>
<p>Java Enum Class:</p>
<pre class="crayon-plain-tag">public enum Direction {
    NORTH, SOUTH, EAST, WEST;
}

Direction dir = Direction.NORTH;
System.out.println(dir); // Output: NORTH</pre>
<div class="blog_h2"><span class="graybg">Inline Classes and Value Classes </span></div>
<p>Kotlin introduces inline classes (now known as value classes) to create type-safe wrappers without runtime overhead.</p>
<p>Kotlin Value Class (Kotlin 1.5 and Later):</p>
<pre class="crayon-plain-tag">@JvmInline
value class Email(val address: String)

fun sendEmail(email: Email) {
    // ...
}

val email = Email("test@example.com")
sendEmail(email)</pre>
<p>In ths example above, Email is a wrapper around String but without additional allocation.</p>
<div class="blog_h2"><span class="graybg">Object Equality</span></div>
<p>Kotlin distinguishes between structural equality (==) and referential equality (===).</p>
<ol>
<li><pre class="crayon-plain-tag">a == b</pre> checks if the values are equal (calls equals()).</li>
<li><pre class="crayon-plain-tag">a === b</pre> checks if the references are the same.</li>
</ol>
<p>Java Equivalent:</p>
<ol style="list-style-type: undefined;">
<li><pre class="crayon-plain-tag">a.equals(b)</pre> checks value equality.</li>
<li><pre class="crayon-plain-tag">a == b</pre> checks reference equality.</li>
</ol>
<div class="blog_h1"><span class="graybg">Coroutines</span></div>
<div class="blog_h2"><span class="graybg">Introduction to Coroutines</span></div>
<p>Coroutines are a powerful feature in Kotlin that facilitate asynchronous and non-blocking programming. They are lightweight threads that allow you to write asynchronous code in a sequential and readable manner.</p>
<div class="blog_h3"><span class="graybg">The Problem with Traditional Asynchronous Code</span></div>
<p>In traditional asynchronous programming, especially in Java, handling asynchronous tasks often leads to:</p>
<ol style="list-style-type: undefined;">
<li>Callback Hell: Nested callbacks that make code hard to read and maintain.</li>
<li>Complex Thread Management: Manual handling of threads, synchronization, and locking mechanisms.</li>
</ol>
<div class="blog_h3"><span class="graybg">Kotlin's Solution: Coroutines</span></div>
<p>Kotlin coroutines simplify asynchronous programming by:</p>
<ol>
<li>Suspending Functions: Functions that can suspend execution without blocking the thread.</li>
<li>Structured Concurrency: Managing coroutines in a structured way to avoid leaks and ensure proper cancellation.</li>
</ol>
<div class="blog_h3"><span class="graybg">How Coroutines Differ from Traditional Threading</span></div>
<ol style="list-style-type: undefined;">
<li>Lightweight: Coroutines are much lighter than threads. You can run thousands of coroutines without significant overhead.</li>
<li>Non-blocking: <span style="background-color: #c0c0c0;">Suspending a coroutine doesn't block the underlying thread</span>, allowing other coroutines to run.</li>
<li>Simplified Syntax: Coroutines allow writing asynchronous code sequentially, improving readability.</li>
</ol>
<div class="blog_h2"><span class="graybg">Coroutine Builders</span></div>
<p>Coroutine builders are functions that help you create and start coroutines.</p>
<div class="blog_h3"><span class="graybg">launch</span></div>
<p>Starts a new coroutine without blocking the current thread. It returns a Job that can be used to manage the coroutine.</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}
// Output:
// Hello,
// World!</pre>
<div class="blog_h3"><span class="graybg">async</span></div>
<p>Starts a new coroutine and returns a <pre class="crayon-plain-tag">Deferred</pre> result (similar to <pre class="crayon-plain-tag">Future</pre> in Java).</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred = async {
        delay(1000L)
        "Result"
    }
    println("Waiting for result...")
    val result = deferred.await()
    println("Result: $result")
}
// Output:
// Waiting for result...
// Result: Result</pre>
<div class="blog_h3"><span class="graybg">runBlocking</span></div>
<p>Bridges the gap between regular blocking code and suspending code. It blocks the current thread until its coroutine completes.</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Start")
    delay(1000L)
    println("End")
}
// Output:
// Start
// End</pre>
<div class="blog_h2"><span class="graybg">Suspending Functions</span></div>
<p>Functions marked with the suspend keyword that can suspend execution without blocking the thread. This kind of functions can only be called from within a coroutine or another suspending function.</p>
<pre class="crayon-plain-tag">suspend fun fetchData(): String {
    delay(1000L) // Simulate long-running task
    return "Data"
}

fun main() = runBlocking {
    val data = fetchData()
    println("Fetched: $data")
}</pre>
<div class="blog_h2"><span class="graybg">Coroutine Scope and Context</span></div>
<div class="blog_h3"><span class="graybg">Coroutine Scope</span></div>
<p>Coroutine scope defines the lifecycle of coroutines and provides context for them:</p>
<ol>
<li>GlobalScope: Lives for the entire lifetime of the application:
<ol>
<li>Coroutines launched in this scope live for the entire lifetime of the application.</li>
<li>These coroutines are not bound to any specific lifecycle (like an activity or a function) and keep running unless explicitly canceled or when the application terminates.</li>
<li>It should be avoided in most cases because it doesn't allow proper lifecycle management, which could lead to memory leaks or unintended behavior.</li>
</ol>
</li>
<li>CoroutineScope: Custom scope for structured concurrency:
<ol>
<li>It is used to create structured concurrency. This means coroutines launched within this scope are bound to its lifecycle, and if the scope is canceled, all the coroutines within it are also canceled.</li>
<li>CoroutineScope can be manually created, or it can be inherited from a parent scope like in runBlocking.</li>
<li>It is preferred for organizing coroutines so they can be properly canceled, ensuring that resources are not leaked.</li>
</ol>
</li>
</ol>
<p>Example of GlobalScope:</p>
<pre class="crayon-plain-tag">GlobalScope.launch {
    println("Running in GlobalScope")
}</pre>
<p> In this example, the coroutine runs independently of any lifecycle and will only stop when canceled or the application terminates.</p>
<p>Example of CoroutineScope inherited from a parent scope:</p>
<pre class="crayon-plain-tag">fun main() = runBlocking {
    launch { // Inherits parent scope
        println("Coroutine in runBlocking scope")
    }
}</pre>
<p><pre class="crayon-plain-tag">runBlocking</pre> creates a special scope, used mainly in testing or main functions, where the code inside runBlocking is run synchronously.</p>
<p>Example of a custom scope: </p>
<pre class="crayon-plain-tag">class MyClass {
    private val scope = CoroutineScope(Dispatchers.Default)

    fun startTask() {
        scope.launch {
            println("Running in a custom CoroutineScope")
        }
    }

    fun stopTask() {
        scope.cancel()  // Cancels all coroutines in this scope
    }
}</pre>
<div class="blog_h3"><span class="graybg">Coroutine Context</span></div>
<p>A coroutine context contains information like the job, dispatcher, and exception handler.</p>
<p>A dispatcher defines which thread or thread pool a coroutine will be executed on. Dispatchers help distribute and manage tasks efficiently based on their requirements, such as computational intensity, input/output operations, or user interface tasks:</p>
<ol>
<li>Dispatchers.Default:
<ol>
<li>Used for CPU-intensive tasks (e.g., complex calculations, sorting, etc.).</li>
<li>It uses a shared pool of background threads optimized for CPU-bound operations.</li>
</ol>
</li>
<li>Dispatchers.IO:
<ol>
<li>Designed for I/O operations like reading from or writing to files, network requests, or database interactions.</li>
<li>It uses a pool of threads optimized for blocking I/O operations to prevent CPU starvation.</li>
</ol>
</li>
<li>Dispatchers.Main:
<ol>
<li>Used for UI-related work, typically on the main thread of an application (for example, in Android development).</li>
<li>It ensures that tasks interacting with the user interface are executed on the main thread to avoid UI lag or inconsistencies.</li>
</ol>
</li>
</ol>
<p>An example of using a dispatcher:</p>
<pre class="crayon-plain-tag">fun main() = runBlocking {
    launch(Dispatchers.IO) {
        val data = fetchData()
        println("Data: $data")
    }
}</pre>
<div class="blog_h2"><span class="graybg">Structured Concurrency</span></div>
<p>Structured concurrency is a design principle in Kotlin Coroutines that ensures that coroutines are executed within a structured scope, with clear parent-child relationships. This design makes it easier to manage their lifecycle, handle exceptions, and ensure that resources are properly released.</p>
<p>In simple terms, structured concurrency ensures that coroutines have a well-defined lifecycle. The parent coroutine will not complete until all of its child coroutines have finished, and if the parent coroutine is canceled, all of its child coroutines will also be automatically canceled. This structure helps avoid common pitfalls such as leaking coroutines or leaving background tasks running indefinitely.</p>
<div class="blog_h3"><span class="graybg">Key Benefits of Structured Concurrency</span></div>
<ol style="list-style-type: undefined;">
<li>Automatic Cancellation: <span style="background-color: #c0c0c0;">When a parent coroutine is canceled, all its child coroutines are canceled automatically.</span> This prevents coroutines from running longer than necessary and ensures that resources (like memory or network connections) are freed up.</li>
<li>Avoiding Leaked Coroutines: <span style="background-color: #c0c0c0;">Coroutines are tied to a scope, and the lifecycle of that scope dictates the lifetime of the coroutines</span>. This avoids situations where coroutines continue running even after their associated tasks or parent coroutine is no longer needed, preventing resource leaks.</li>
<li>Lifecycle Management:<span style="background-color: #c0c0c0;"> The parent coroutine waits for its child coroutines to finish</span>. This ensures a predictable and manageable lifecycle for the coroutines, avoiding orphaned coroutines that can be hard to trace and manage.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">suspend fun fetchData(): String = coroutineScope {
    // Launch two async coroutines to fetch data concurrently
    val data1 = async { /* Simulate fetching data 1 */ "Data1" }
    val data2 = async { /* Simulate fetching data 2 */ "Data2" }

    // Wait for both data to be fetched and concatenate the results
    data1.await() + data2.await()
}</pre>
<p>Explanation:</p>
<ol>
<li>coroutineScope: The coroutineScope function creates a scope that ensures all the coroutines launched within it (like async or launch) are completed before it returns. This ensures that fetchData() will only return when both data1 and data2 have finished fetching their data.</li>
<li>async: The async function launches a new coroutine concurrently to execute a block of code. It's similar to launch, but it returns a Deferred result, which you can await to get the result.</li>
<li>await: This suspends the parent coroutine until the result of the child coroutine (created by async) is available. In this case, the result is the fetched data.</li>
<li>Structured Concurrency in Action:
<ol>
<li>Both data1 and data2 are launched concurrently.</li>
<li>The coroutineScope ensures that the parent coroutine waits for both data1.await() and data2.await() to finish before proceeding.</li>
<li>If an exception occurs in one of the async blocks, or if the parent coroutine is canceled, both data1 and data2 coroutines will be automatically canceled.</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">How Structured Concurrency Works Under the Hood</span></div>
<ol style="list-style-type: undefined;">
<li>Parent-Child Relationship: Every coroutine has <span style="background-color: #c0c0c0;">a parent-child relationship when launched within a scope. The coroutineScope establishes this relationship</span>.</li>
<li>Exception Propagation: <span style="background-color: #c0c0c0;">If a child coroutine fails with an exception, that exception is propagated to its parent</span>, and the entire coroutine scope is canceled unless handled explicitly. This ensures that errors are not silently ignored.</li>
<li>Job Hierarchy: Coroutines form a hierarchy of jobs, with a parent job being responsible for managing the completion of its children. Cancellation of the parent job cascades down to its children.</li>
</ol>
<div class="blog_h2"><span class="graybg">Channels and Flow</span></div>
<p>Both Channels and Flows provide ways to deal with asynchronous streams of data in Kotlin coroutines, but they have different use cases, behavior, and underlying mechanisms. Here's a breakdown of each, followed by the key differences.</p>
<div class="blog_h3"><span class="graybg">Channels</span></div>
<ol>
<li>Channels are similar to queues and provide a way for coroutines to send and receive data.</li>
<li>Channels allow bi-directional communication between producer and consumer coroutines, where one coroutine can send data and another coroutine can receive it.</li>
<li>Channels can be hot, meaning the data is produced whether or not the consumer is actively receiving it.</li>
<li>When a channel is closed (using channel.close()), it signals that no further values will be sent, but the consumer can still receive the remaining values until the channel is empty.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val channel = Channel()

    // Launching a coroutine to send data to the channel
    launch {
        for (x in 1..5) channel.send(x * x)  // Sending values (1^2, 2^2, ..., 5^2)
        channel.close()  // Close the channel to indicate no more data
    }

    // Receiving data from the channel
    for (y in channel) {
        println(y)  // Prints 1, 4, 9, 16, 25
    }
}</pre>
<div class="blog_h3"><span class="graybg">Flow</span></div>
<ol>
<li>A Flow is a cold asynchronous data stream that emits values sequentially.</li>
<li>Flows are <span style="background-color: #c0c0c0;">unidirectional and typically represent a one-way stream of data from producer to consumer</span>.</li>
<li>Flows are <span style="background-color: #c0c0c0;">cold, meaning the data is only produced when a consumer starts collecting the flow</span>. <span style="background-color: #c0c0c0;">If no one is collecting the flow, the producer is inactive</span>.</li>
<li>Flows are much more declarative and follow a pattern similar to reactive streams. They emit data using the emit() function and are consumed using collect().</li>
<li>Flow APIs handle backpressure and cancellation transparently.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simpleFlow(): Flow = flow {
    for (i in 1..3) {
        delay(100)  // Simulate asynchronous work
        emit(i)  // Emit values (1, 2, 3)
    }
}

fun main() = runBlocking {
    simpleFlow().collect { value -&gt;
        println(value)  // Prints 1, 2, 3
    }
}</pre>
<div class="blog_h2"><span class="graybg">Comparison with Java Threading</span></div>
<div class="blog_h3"><span class="graybg">Traditional Java Threading</span></div>
<ol>
<li>Threads: Heavyweight, managed by the OS.</li>
<li>Synchronization: Requires explicit handling of synchronization, locks, and potential deadlocks.</li>
<li>Asynchronous APIs: Use of Future, Callable, ExecutorService, and CompletableFuture. </li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -&gt; {
            try {
                Thread.sleep(1000);
                System.out.println("World!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        System.out.println("Hello,");
    }
}
// Output may vary due to thread scheduling:
// Hello,
// World!</pre>
<div class="blog_h3"><span class="graybg">Java's CompletableFuture (Java 8 and Later)</span></div>
<p>CompletableFuture is for building asynchronous computation stages.</p>
<pre class="crayon-plain-tag">import java.util.concurrent.*;

public class CompletableFutureExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture future = CompletableFuture.supplyAsync(() -&gt; {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Result";
        });
        System.out.println("Waiting for result...");
        String result = future.get();
        System.out.println("Result: " + result);
    }
}
// Output:
// Waiting for result...
// Result: Result</pre>
<div class="blog_h2"><span class="graybg">Comparison with Java Virtual Threads</span></div>
<p>Java Virtual Threads are a feature introduced under Project Loom, available as a preview in Java 19 and beyond. Virtual Threads aim to provide lightweight, high-throughput threading by decoupling the notion of a Java thread from an operating system thread. Key features include:</p>
<ol>
<li>Lightweight Threads: Virtual Threads are managed by the JVM rather than the OS, allowing for millions of threads.</li>
<li>Familiar APIs: Use the same java.lang.Thread API, making it easier for developers to adopt.</li>
<li>Better Resource Utilization: Improves scalability and performance for applications with high concurrency needs.</li>
<li>Compatibility: Works with existing Java code and libraries.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">public class VirtualThreadExample {
    public static void main(String[] args) throws Exception {
        Thread.startVirtualThread(() -&gt; {
            try {
                Thread.sleep(1000);
                System.out.println("World!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("Hello,");
        Thread.sleep(1500); // Ensure the virtual thread has time to execute
    }
}</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">Generics &amp; Variance<br /></span></div>
<div class="blog_h2"><span class="graybg">Generics </span></div>
<p>Generics are a powerful feature in Kotlin that allow you to write flexible and reusable code. By using generics, you can define classes, methods, and interfaces that work with any type while preserving type safety. Generics enable you to create algorithms or data structures that can handle multiple types without having to write the same logic multiple times for different types.</p>
<div class="blog_h3"><span class="graybg">Basic Syntax of Generics</span></div>
<p>A generic class or function in Kotlin can accept a type parameter, which allows the user to specify the actual type when using that class or function. The type parameter is usually denoted by a single capital letter, commonly T, but you can use any name you like.</p>
<pre class="crayon-plain-tag">// Defining a generic class Box with a type parameter T
class Box(val value: T)

fun main() {
    // Creating a Box that holds an Int
    val intBox: Box = Box(42)
    println(intBox.value)  // Output: 42

    // Creating a Box that holds a String
    val stringBox: Box = Box("Hello")
    println(stringBox.value)  // Output: Hello
}</pre>
<div class="blog_h3"><span class="graybg">Generic Functions</span></div>
<p>Just like classes, functions in Kotlin can also be made generic by introducing type parameters in the function definition. Here's how you can define and use a generic function: </p>
<pre class="crayon-plain-tag">// Defining a generic function that works with any type
fun  printBoxContent(box: Box) {
    println("Box contains: ${box.value}")
}

fun main() {
    val intBox = Box(42)
    val stringBox = Box("Hello")
    
    // Calling the generic function with different types
    printBoxContent(intBox)    // Output: Box contains: 42
    printBoxContent(stringBox)  // Output: Box contains: Hello
}</pre>
<div class="blog_h3"><span class="graybg">Type Constraints in Generics</span></div>
<p>Sometimes, you might want to restrict the types that can be used as generic arguments. Kotlin allows you to apply type constraints to limit the types that can be passed to a generic type parameter.</p>
<p>For example, you can limit a generic type to only accept subtypes of a particular class or implement a specific interface. This is done using the where keyword or inline directly after the type parameter with the <pre class="crayon-plain-tag">:</pre> symbol.</p>
<pre class="crayon-plain-tag">// Defining a generic class with a type constraint
class Container(val value: T)

fun main() {
    val intContainer = Container(42)   // Allowed because Int is a subtype of Number
    val doubleContainer = Container(3.14)  // Allowed because Double is a subtype of Number

    // val stringContainer = Container("Hello")  // Error: String is not a subtype of Number
}</pre>
<div class="blog_h3"><span class="graybg">Type Erasure in Generics</span></div>
<p>Like Java, Kotlin uses type erasure for generics, meaning that the generic type information is only available at compile time and is erased at runtime. This means you cannot directly check the type of a generic parameter at runtime.</p>
<p>For instance, trying to check the type of a generic parameter like this will not work:</p>
<pre class="crayon-plain-tag">fun  checkType(value: T) {
    if (value is List) {
        // Error: Cannot check for List at runtime due to type erasure
    }
}</pre>
<div class="blog_h3"><span class="graybg">Generics with Multiple Type Parameters </span></div>
<p>You can also define classes or functions with multiple generic type parameters, allowing even more flexibility.</p>
<pre class="crayon-plain-tag">// Defining a class with two generic type parameters
class PairBox&lt;T1, T2&gt;(val first: T1, val second: T2)

fun main() {
    val pair = PairBox(1, "One")
    println("First: ${pair.first}, Second: ${pair.second}")  // Output: First: 1, Second: One
}</pre>
<div class="blog_h2"><span class="graybg">Reified Keyword</span></div>
<p>In Kotlin, generics are usually erased at runtime, which means that type information is not available during runtime. However, in some cases, you may need to <span style="background-color: #ffffff;"><span style="background-color: #c0c0c0;">retain the generic type information</span> for certain operations</span>, such as casting or checking the type of an object. This is where the <pre class="crayon-plain-tag">reified</pre> keyword comes into play.</p>
<p>By using the <pre class="crayon-plain-tag">reified</pre> keyword in combination with an <pre class="crayon-plain-tag">inline</pre> function, Kotlin allows you to retain the type information at runtime. Let’s explore how this works:</p>
<div class="blog_h3"><span class="graybg">Usage of Reified Keyword</span></div>
<p>Normally, without <pre class="crayon-plain-tag">reified</pre>, you can’t access the type of generic parameters at runtime because of type erasure. However, using <pre class="crayon-plain-tag">reified</pre>, you can perform type checks and casts directly:</p>
<pre class="crayon-plain-tag">inline fun isTypeOf(value: Any): Boolean {
    return value is T
}

fun main() {
    val result = isTypeOf("Hello")
    println(result) // Output: true
}</pre>
<p>In this example, the <pre class="crayon-plain-tag">isTypeOf</pre> function is an <pre class="crayon-plain-tag">inline</pre> function with a <pre class="crayon-plain-tag">reified</pre> generic type <pre class="crayon-plain-tag">T</pre>. The reified type allows Kotlin to know what type <pre class="crayon-plain-tag">T</pre> is during runtime, which is not possible with regular generics.</p>
<div class="blog_h3"><span class="graybg">Practical Use of Reified</span></div>
<p>One practical use of <pre class="crayon-plain-tag">reified</pre> is to simplify code that involves type casting. Without <pre class="crayon-plain-tag">reified</pre>, you would need to pass the class type explicitly, which makes the code more verbose. Let’s look at the difference:</p>
<p>Without Reified:</p>
<pre class="crayon-plain-tag">fun  getClassName(clazz: Class): String {
    return clazz.simpleName
}

fun main() {
    val className = getClassName(String::class.java)
    println(className) // Output: String
}</pre>
<p>With Reified:</p>
<pre class="crayon-plain-tag">inline fun  getClassName(): String {
    return T::class.java.simpleName
}

fun main() {
    val className = getClassName()
    println(className) // Output: String
}</pre>
<p>As you can see, by using <pre class="crayon-plain-tag">reified</pre>, the function signature becomes simpler, and there is no need to explicitly pass the class type. Kotlin can infer it automatically at runtime.</p>
<div class="blog_h3"><span class="graybg">Limitations of Reified</span></div>
<p>It’s important to note that the <pre class="crayon-plain-tag">reified</pre> keyword can only be used in <pre class="crayon-plain-tag">inline</pre> functions. This is because the type information is only preserved during runtime if the function is inlined. Otherwise, Kotlin would still erase the type information as part of type erasure.</p>
<p>Here’s an attempt to use <pre class="crayon-plain-tag">reified</pre> in a non-inline function, which would cause a compilation error:</p>
<pre class="crayon-plain-tag">fun  getClassName(): String { // Error: Reified type parameter T is not allowed in non-inline functions
    return T::class.java.simpleName
}</pre>
<p>To summarize, <pre class="crayon-plain-tag">reified</pre> provides a powerful way to retain type information at runtime in Kotlin, particularly when dealing with generics. Its most useful applications involve type checks and casts, making the code more concise and easier to read.</p>
<div class="blog_h2"><span class="graybg">Variance</span></div>
<p>In the context of programming languages that support generics (such as Kotlin, Java, Scala, etc.), variance  is a concept that describes how subtyping between more complex types (like generics or parameterized types) relates to subtyping between their components (like their type parameters).</p>
<p>Specifically, variance determines whether one generic type can be considered a subtype or supertype of another generic type based on their type parameters. For example, if you have two types <pre class="crayon-plain-tag">A</pre> and <pre class="crayon-plain-tag">B</pre> where A is a subtype of B, variance answers the question: is <pre class="crayon-plain-tag">Box</pre> a subtype of <pre class="crayon-plain-tag">Box<b></b></pre>?</p>
<div class="blog_h3"><span class="graybg">Covariance</span></div>
<p>The <pre class="crayon-plain-tag">out</pre> keyword in Kotlin indicates covariance (协变). Covariance allows a generic type to preserve the subtype relationship of its type parameters. <span style="background-color: #c0c0c0;">If type A is a subtype of type B, then <pre class="crayon-plain-tag">Box</pre> will be a subtype of <pre class="crayon-plain-tag">Box<b></b></pre> if the generic type is covariant</span>. <span style="background-color: #c0c0c0;">Covariance allows you to read values from a generic type but restricts you from modifying it</span>.</p>
<pre class="crayon-plain-tag">// Define a covariant Box class
class Box(val value: T)

fun main() {
    val intBox: Box = Box(42)
    val anyBox: Box = intBox  // This is allowed due to covariance (Int is a subtype of Any)

    // You can safely read from anyBox
    println(anyBox.value)  // Output: 42

    // However, you cannot modify anyBox because it's covariant
    // anyBox.value = "Hello"  // Error: Val cannot be reassigned
}</pre>
<p>Here, Box is covariant in <pre class="crayon-plain-tag">T</pre>. If <pre class="crayon-plain-tag">Int</pre> is a subtype of <pre class="crayon-plain-tag">Any</pre>, then <pre class="crayon-plain-tag">Box</pre> is considered a subtype of <pre class="crayon-plain-tag">Box</pre>, because you can safely read an <pre class="crayon-plain-tag">Any</pre> from a <pre class="crayon-plain-tag">Box</pre>.</p>
<p>Think of covariant types as producers. For example, a <pre class="crayon-plain-tag">Box</pre> can produce Strings, and since <pre class="crayon-plain-tag">String</pre> is a subtype of <pre class="crayon-plain-tag">Any</pre>, a <pre class="crayon-plain-tag">Box</pre> can also be treated as a <pre class="crayon-plain-tag">Box</pre>(since you can read values of type <pre class="crayon-plain-tag">Any</pre> from it).</p>
<div class="blog_h3"><span class="graybg">Contravariance </span></div>
<p>Contravariance is the opposite of covariance. If type <pre class="crayon-plain-tag">A</pre> is a subtype of type <pre class="crayon-plain-tag">B</pre>, then <pre class="crayon-plain-tag">Box<b></b></pre> will be a subtype of <pre class="crayon-plain-tag">Box</pre>if the generic type is contravariant. Contravariance allows you to write values to a generic type but restricts you from reading them.</p>
<pre class="crayon-plain-tag">// Define a contravariant Box class
class Box {
    fun setValue(value: T) {
        println("Setting value: $value")
    }
}

fun main() {
    val anyBox: Box = Box()
    val stringBox: Box = anyBox  // This is allowed due to contravariance (String is a subtype of Any)

    // You can safely write a String to anyBox, because it's contravariant
    stringBox.setValue("Hello, World!")  // Output: Setting value: Hello, World!

    // However, you cannot read from stringBox because it's contravariant
    // val value: String = stringBox.value  // Error: Cannot read from a contravariant type
}</pre>
<p>In this example, <pre class="crayon-plain-tag">Box</pre> is contravariant. If <pre class="crayon-plain-tag">String</pre> is a subtype of <pre class="crayon-plain-tag">Any</pre>, then <pre class="crayon-plain-tag">Box</pre> is considered a subtype of <pre class="crayon-plain-tag">Box</pre>, because you can safely write a <pre class="crayon-plain-tag">String</pre> into a <pre class="crayon-plain-tag">Box</pre> , but you cannot safely read from it since you don’t know the exact type.</p>
<p>Think of contravariant types as consumers. A <pre class="crayon-plain-tag">Box</pre> can accept any type, so it can accept a <pre class="crayon-plain-tag">String</pre>. Hence, it can be treated as a <pre class="crayon-plain-tag">Box</pre>.</p>
<div class="blog_h3"><span class="graybg">Invariance </span></div>
<p>Invariance means that there is no relationship between the subtyping of the type parameters and the subtyping of the generic types themselves. In other words, if type <pre class="crayon-plain-tag">A</pre> is a subtype of type <pre class="crayon-plain-tag">B</pre>, there is no relationship between <pre class="crayon-plain-tag">Box</pre> and <pre class="crayon-plain-tag">Box<b></b></pre>. They are considered independent.Invariant types are neither covariant nor contravariant; they are strict about the type they accept.</p>
<pre class="crayon-plain-tag">// Define an invariant Box class
class Box(val value: T)

fun main() {
    val intBox: Box = Box(42)
    // val anyBox: Box = intBox  // Error: Type mismatch

    // You can both read and write values, but they must be of the exact type
    val anotherIntBox: Box = Box(100)
    println(anotherIntBox.value)  // Output: 100

    // If we had a setter (not shown here), it would only accept Int
    // anotherIntBox.setValue("Hello")  // Error: Type mismatch
}</pre>
<p>Here, <pre class="crayon-plain-tag">Box</pre> is invariant. You cannot pass a <pre class="crayon-plain-tag">Box</pre> to a function that expects a <pre class="crayon-plain-tag">Box</pre> , even though <pre class="crayon-plain-tag">Int</pre> is a subtype of <pre class="crayon-plain-tag">Any</pre>.</p>
<p>Invariant types are both producers and consumers of their exact type. You can both read and write values, but they must always be of the specific type <pre class="crayon-plain-tag">T</pre>. There is no flexibility in treating it as another type.</p>
<div class="blog_h1"><span class="graybg">Collections and Functional Operations </span></div>
<p>Collections are fundamental data structures that allow developers to store and manipulate groups of objects. Kotlin provides a rich set of collection APIs and functional operations that enhance productivity and code readability. In this section, we'll explore Kotlin's collections, compare them with Java's collection framework, and delve into functional operations that simplify common programming tasks.</p>
<div class="blog_h2"><span class="graybg">Overview of Collections in Kotlin</span></div>
<p> Kotlin collections are divided into two categories:</p>
<ol>
<li>Immutable Collections: Read-only collections that cannot be modified after creation.</li>
<li>Mutable Collections: Collections that can be modified, allowing addition, removal, and updating of elements.</li>
</ol>
<div class="blog_h3"><span class="graybg">Immutable Collections</span></div>
<p>Immutable collections are preferred in functional programming paradigms as they prevent accidental modification and promote thread safety. </p>
<p>Common Immutable Collection Types:</p>
<ol>
<li>List: Ordered collection of elements.</li>
<li>Set: Unordered collection of unique elements.</li>
<li>Map: Collection of key-value pairs.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">val numbers: List = listOf(1, 2, 3)</pre>
<div class="blog_h3"><span class="graybg">Mutable Collections </span></div>
<p>Mutable collections can be modified after creation.</p>
<p>Common Mutable Collection Types:</p>
<ol>
<li>MutableList</li>
<li>MutableSet</li>
<li>MutableMap</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">val mutableNumbers: MutableList = mutableListOf(1, 2, 3)
mutableNumbers.add(4)</pre>
<div class="blog_h3"><span class="graybg">Creating Collections</span></div>
<pre class="crayon-plain-tag">val fruits = listOf("Apple", "Banana", "Cherry")

val mutableFruits = mutableListOf("Apple", "Banana")
mutableFruits.add("Cherry")


val numbers = setOf(1, 2, 3, 2)
println(numbers) // Output: [1, 2, 3]

val mutableNumbers = mutableSetOf(1, 2, 3)
mutableNumbers.add(4)


val countryCodes = mapOf("US" to "United States", "CA" to "Canada")

val mutableCountryCodes = mutableMapOf("US" to "United States")
mutableCountryCodes["CA"] = "Canada"</pre>
<div class="blog_h2"><span class="graybg">Functional Operations on Collections</span></div>
<p>Kotlin provides a plethora of functional operations that make working with collections more expressive and concise. These operations are inspired by functional programming paradigms.</p>
<div class="blog_h3"><span class="graybg">Common Functional Operations</span></div>
<ol style="list-style-type: undefined;">
<li>map: Transforms each element.<br />
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3)
val squares = numbers.map { it * it }
println(squares) // Output: [1, 4, 9] </pre>
</li>
<li>filter: Filters elements based on a predicate.<br />
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // Output: [2, 4]</pre>
</li>
<li>reduce: Reduces the collection to a single value.<br />
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3, 4)
val sum = numbers.reduce { acc, num -&gt; acc + num }
println(sum) // Output: 10 </pre>
</li>
<li>fold: Similar to reduce but with an initial value.<br />
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3, 4)
val product = numbers.fold(1) { acc, num -&gt; acc * num }
println(product) // Output: 24</pre>
</li>
<li>forEach: Performs an action on each element.</li>
<li>groupBy: Groups elements by a key.<br />
<pre class="crayon-plain-tag">data class Person(val name: String, val city: String)

val people = listOf(
    Person("Alice", "New York"),
    Person("Bob", "Paris"),
    Person("Charlie", "New York")
)

val peopleByCity = people.groupBy { it.city }
println(peopleByCity)
// Output:
// {
//   New York=[Person(name=Alice, city=New York), Person(name=Charlie, city=New York)],
//   Paris=[Person(name=Bob, city=Paris)]
// }</pre>
</li>
<li>flatMap: Maps each element to a collection and flattens the results.<br />
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3)
val expandedNumbers = numbers.flatMap { listOf(it, it * 10) }
println(expandedNumbers) // Output: [1, 10, 2, 20, 3, 30]</pre>
</li>
<li>partition: Splits the collection into two based on a predicate.</li>
</ol>
<div class="blog_h2"><span class="graybg">Sequences</span></div>
<p>Sequences are lazily evaluated collections in Kotlin. They are useful when working with large datasets or when the operations are computationally intensive.</p>
<p>You can create a sequence from a collection:</p>
<pre class="crayon-plain-tag">val numbers = listOf(1, 2, 3, 4, 5)
val numberSequence = numbers.asSequence()</pre>
<p>Or generate one:</p>
<pre class="crayon-plain-tag">val naturalNumbers = generateSequence(1) { it + 1 }
val firstTenNumbers = naturalNumbers.take(10).toList()
println(firstTenNumbers) // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]</pre>
<p>Example of using a sequence:</p>
<pre class="crayon-plain-tag">// Even though we start with a range of 1 to 1,000,000, only the necessary computations
// are performed due to lazy evaluation.
val numbers = (1..1_000_000).asSequence()
    .map { it * 2 }
    .filter { it % 3 == 0 }
    .take(10)
    .toList()
println(numbers) // Output: [6, 12, 18, 24, 30, 36, 42, 48, 54, 60]</pre>
<div class="blog_h3"><span class="graybg">Benefits of Sequences</span></div>
<ol>
<li>Lazy Evaluation: Operations are evaluated as needed, which can improve performance.</li>
<li>Avoids Intermediate Collections: Reduces memory overhead. </li>
</ol>
<div class="blog_h2"><span class="graybg">Collection Builders</span></div>
<p>Kotlin provides collection builders for creating collections in a functional style.</p>
<pre class="crayon-plain-tag">val numbers = buildList {
    add(1)
    add(2)
    addAll(listOf(3, 4, 5))
}
println(numbers) // Output: [1, 2, 3, 4, 5]</pre>
<div class="blog_h2"><span class="graybg">Parallel Processing</span></div>
<p>Kotlin sequences are not inherently parallel. However, Kotlin coroutines can be used for asynchronous and parallel operations. For example:</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers

fun main() = runBlocking {
    val numbers = (1..5).toList()
    val squares = numbers.map { number -&gt;
        async(Dispatchers.Default) {
            number * number
        }
    }.awaitAll()
    println(squares) // Output: [1, 4, 9, 16, 25]
}</pre>
<div class="blog_h2"><span class="graybg">Comparison with Java Collections</span></div>
<p>Java's collection framework is robust and has evolved over time, especially with the introduction of Streams in Java 8.</p>
<div class="blog_h3"><span class="graybg">Java Collections</span></div>
<ol>
<li>Immutable Collections: Introduced in Java 9 with List.of(), Set.of(), Map.of().</li>
<li>Mutable Collections: The standard ArrayList, HashSet, HashMap, etc.</li>
</ol>
<p>Example of Immutable List in Java:</p>
<pre class="crayon-plain-tag">List numbers = List.of(1, 2, 3);</pre>
<div class="blog_h3"><span class="graybg">Functional Operations in Java</span></div>
<p>Java 8 introduced Streams ( Also lazily loaded ) , which provide functional operations on collections.</p>
<pre class="crayon-plain-tag">List numbers = Arrays.asList(1, 2, 3, 4, 5);
List squares = numbers.stream()
    .map(n -&gt; n * n)
    .collect(Collectors.toList());
System.out.println(squares); // Output: [1, 4, 9, 16, 25]</pre>
<p>Sequences in Kotlin</p>
<ol style="list-style-type: undefined;">
<li>Similar to Java Streams but more integrated into the language.</li>
<li>No Need for Explicit Conversion: Collections can be seamlessly converted to sequences.</li>
</ol>
<p>Streams in Java</p>
<ol style="list-style-type: undefined;">
<li>Separate API: Requires conversion using stream() method.</li>
<li>Terminal Operations: Must perform a terminal operation to execute the stream pipeline.</li>
</ol>
<div class="blog_h1"><span class="graybg">Interop Between Kotlin and Java</span></div>
<p>One of the key strengths of Kotlin is its seamless interoperability with Java. This means you can:</p>
<ol>
<li>Call Kotlin code from Java.</li>
<li>Call Java code from Kotlin.</li>
<li>Use existing Java libraries and frameworks in Kotlin projects.</li>
<li>Migrate codebases incrementally from Java to Kotlin.</li>
</ol>
<p>In this section, we'll explore how Kotlin and Java interoperate, covering various aspects such as calling functions, handling nullability, working with classes, exceptions, and more.</p>
<div class="blog_h2"><span class="graybg">Calling Kotlin from Java </span></div>
<div class="blog_h3"><span class="graybg">Basic Function Calls</span></div>
<p>Kotlin functions can be called from Java code without any special effort. </p>
<p>Kotlin Function:</p>
<pre class="crayon-plain-tag">// File: Utils.kt
package cc.gmem.utils

fun greet(name: String): String {
    return "Hello, $name!"
}</pre>
<p>Calling from Java:</p>
<pre class="crayon-plain-tag">import cc.gmem.utils.UtilsKt;

public class Main {
    public static void main(String[] args) {
        String message = UtilsKt.greet("Alice");
        System.out.println(message); // Output: Hello, Alice!
    }
}</pre>
<p>Explanation:</p>
<ol>
<li>By default, top-level functions in Kotlin are compiled into a class named after the file with the suffix Kt.</li>
<li>In this case, the class is UtilsKt, and the function greet is a static method. </li>
</ol>
<div class="blog_h3"><span class="graybg">Using @JvmName Annotation </span></div>
<p>You can customize the generated class name using the @file:JvmName annotation.</p>
<pre class="crayon-plain-tag">@file:JvmName("UtilFunctions")
package cc.gmem.utils

fun greet(name: String): String {
    return "Hello, $name!"
}</pre>
<p> For the function above you can call it from Java:</p>
<pre class="crayon-plain-tag">import cc.gmem.utils.UtilFunctions;

public class Main {
    public static void main(String[] args) {
        String message = UtilFunctions.greet("Alice");
        System.out.println(message);
    }
}</pre>
<div class="blog_h3"><span class="graybg">Static Methods and Companion Objects</span></div>
<p>Kotlin's companion objects can be used to expose static members to Java.</p>
<pre class="crayon-plain-tag">class MathUtils {
    companion object {
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
}</pre>
<p> For the function above you can call it from Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        int sum = MathUtils.Companion.add(5, 3);
        System.out.println(sum); // Output: 8
    }
}</pre>
<div class="blog_h3"><span class="graybg">Using @JvmStatic </span></div>
<p>To make the method appear as a static method in Java, use the @JvmStatic annotation.</p>
<pre class="crayon-plain-tag">class MathUtils {
    companion object {
        @JvmStatic
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
}</pre>
<p>Calling from Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        int sum = MathUtils.add(5, 3);
        System.out.println(sum);
    }
}</pre>
<div class="blog_h3"><span class="graybg">Kotlin Properties</span></div>
<p>Kotlin properties are compiled to getter and setter methods in Java.</p>
<pre class="crayon-plain-tag">class Person(var name: String, val age: Int)</pre>
<p>Accessing from Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.getName()); // Getter for 'name'
        person.setName("Bob"); // Setter for 'name'
        System.out.println(person.getAge()); // Getter for 'age' (no setter since it's 'val')
    }
}</pre>
<div class="blog_h3"><span class="graybg">Handling Nullability</span></div>
<p>Kotlin's null safety affects how types are represented in Java.</p>
<pre class="crayon-plain-tag">fun getName(): String? {
    return null
}</pre><br />
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        String name = UtilsKt.getName();
        if (name != null) {
            System.out.println(name.length());
        } else {
            System.out.println("Name is null");
        }
    }
}</pre>
<div class="blog_h2"><span class="graybg">Calling Java from Kotlin</span></div>
<p>Kotlin can seamlessly use existing Java classes and methods.</p>
<div class="blog_h3"><span class="graybg">Basic Class Usage</span></div>
<pre class="crayon-plain-tag">public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}</pre>
<p>Using in Kotlin:</p>
<pre class="crayon-plain-tag">fun main() {
    val calculator = Calculator()
    val sum = calculator.add(5, 3)
    println(sum) // Output: 8
}</pre>
<div class="blog_h3"><span class="graybg">Static Members </span></div>
<pre class="crayon-plain-tag">public class MathUtils {
    public static int multiply(int a, int b) {
        return a * b;
    }
}</pre>
<p>Using in Kotlin: </p>
<pre class="crayon-plain-tag">fun main() {
    val product = MathUtils.multiply(4, 5)
    println(product) // Output: 20
}</pre>
<div class="blog_h3"><span class="graybg">Nullability and Platform Types </span></div>
<p>When calling Java code from Kotlin, types are treated as platform types, which can be nullable or non-nullable.</p>
<pre class="crayon-plain-tag">public String getName() {
    return null;
}</pre>
<p>Using in Kotlin:</p>
<pre class="crayon-plain-tag">fun main() {
    val name = getName()
    println(name.length) // May throw NullPointerException
    val name1: String? = getName() // Safe: Allows null
}</pre>
<p>Since getName() is a platform type, Kotlin does not enforce null checks. It's up to the developer to handle potential nulls. </p>
<div class="blog_h3"><span class="graybg">Handling Checked Exceptions</span></div>
<p>Kotlin does not have checked exceptions. When calling Java methods that throw checked exceptions, you need to handle them manually.</p>
<pre class="crayon-plain-tag">public void readFile(String path) throws IOException {
    // ...
}</pre>
<p>Using in Kotlin:</p>
<pre class="crayon-plain-tag">fun main() {
    try {
        readFile("file.txt")
    } catch (e: IOException) {
        e.printStackTrace()
    }
}</pre>
<p>Please note: You need to catch the exception explicitly; the compiler does not enforce it.</p>
<div class="blog_h2"><span class="graybg">Java Annotations and Kotlin</span></div>
<div class="blog_h3"><span class="graybg">@NotNull and @Nullable</span></div>
<p>Kotlin recognizes Java's nullability annotations to improve null safety.</p>
<pre class="crayon-plain-tag">public @Nullable String getName() {
    return null;
}</pre>
<p>Using in Kotlin: </p>
<pre class="crayon-plain-tag">fun main() {
    val name = getName()
    if (name != null) {
        println(name.length)
    } else {
        println("Name is null")
    }
}</pre>
<p>Kotlin treats @Nullable types as nullable. </p>
<div class="blog_h3"><span class="graybg">@JvmOverloads</span></div>
<p>Kotlin functions with default parameters can generate overloads for Java using @JvmOverloads.</p>
<pre class="crayon-plain-tag">class Greeter {
    @JvmOverloads
    fun greet(name: String = "Guest") {
        println("Hello, $name!")
    }
}</pre>
<p>Calling from Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        Greeter greeter = new Greeter();
        greeter.greet();          // Calls greet() with default parameter
        greeter.greet("Alice");   // Calls greet(String)
    }
}</pre>
<div class="blog_h3"><span class="graybg">@JvmField</span></div>
<p>By default, Kotlin properties are accessed via getters and setters. To expose a public field directly to Java, use @JvmField.</p>
<pre class="crayon-plain-tag">class Constants {
    @JvmField
    val MAX_COUNT = 100
}</pre>
<p> Accessing from Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        int max = Constants.MAX_COUNT;
        System.out.println(max); // Output: 100
    }
}</pre>
<div class="blog_h2"><span class="graybg">Annotation Processing</span></div>
<p>Kotlin supports Java's annotation processing tools.</p>
<ol>
<li>Using Annotations: You can use annotations like @Entity, @Autowired, etc., in Kotlin classes.</li>
<li>Annotation Processors: Tools like Dagger, Hibernate, and Spring Data work with Kotlin code.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">@Entity
data class User(
    @Id val id: Long,
    val name: String
)</pre>
<div class="blog_h2"><span class="graybg">Differences in Language Features </span></div>
<div class="blog_h3"><span class="graybg">SAM Conversions</span></div>
<p>About Single Abstract Method (SAM) Interfaces:</p>
<ol>
<li>In Java, functional interfaces can be implemented using lambda expressions.</li>
<li>Kotlin supports SAM conversions for Java interfaces but not for Kotlin interfaces.</li>
</ol>
<pre class="crayon-plain-tag">public interface Runnable {
    void run();
}</pre>
<p>Using in Kotlin: </p>
<pre class="crayon-plain-tag">fun main() {
    // You can pass a lambda to a Java SAM interface in Kotlin.
    val runnable = Runnable { println("Running") }
    runnable.run()
}</pre>
<div class="blog_h3"><span class="graybg">Kotlin Data Classes in Java</span></div>
<p>Kotlin data classes generate equals(), hashCode(), toString(), and copy() methods.</p>
<p>Kotlin Data Class:</p>
<pre class="crayon-plain-tag">data class User(val name: String, val age: Int)</pre>
<p>Using in Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        User user = new User("Alice", 30);
        System.out.println(user.getName()); // Access properties via getters
        System.out.println(user); // Calls toString()
    }
}</pre>
<p>Plese note that the <pre class="crayon-plain-tag">copy()</pre> function is not directly accessible in Java.</p>
<div class="blog_h3"><span class="graybg">Type Aliases</span></div>
<p>Kotlin's typealias declarations are not visible in Java.</p>
<pre class="crayon-plain-tag">typealias StringMap = Map&lt;String, String&gt;

fun getMap(): StringMap {
    return mapOf("key" to "value")
}</pre>
<p>Java usage:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        Map&lt;String, String&gt; map = UtilsKt.getMap();
        System.out.println(map);
    }
}</pre>
<div class="blog_h3"><span class="graybg">Extension Functions</span></div>
<p>Kotlin extension functions are not directly accessible from Java.</p>
<p>Kotlin Extension Function:</p>
<pre class="crayon-plain-tag">fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}</pre>
<p>Using in Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        String word = "level";
        // Extension functions are compiled into static methods with the receiver type as the first parameter
        boolean result = StringExtensionsKt.isPalindrome(word);
        System.out.println(result); // Output: true
    }
}</pre>
<div class="blog_h3"><span class="graybg">Coroutines Interoperability</span></div>
<p>Kotlin coroutines can be used from Java code, but it's more complex. </p>
<p>Kotlin Suspended Function:</p>
<pre class="crayon-plain-tag">suspend fun fetchData(): String {
    delay(1000L)
    return "Data"
}</pre>
<p>Using in Java:</p>
<pre class="crayon-plain-tag">public class Main {
    public static void main(String[] args) {
        // Calling suspend functions from Java requires handling Continuation, making it less straightforward.
        Continuation continuation = new Continuation() {
            @Override
            public CoroutineContext getContext() {
                return EmptyCoroutineContext.INSTANCE;
            }

            @Override
            public void resumeWith(Object result) {
                if (result instanceof Result) {
                    Result res = (Result) result;
                    System.out.println(res.getOrNull());
                }
            }
        };

        FetchDataKt.fetchData(continuation);
    }
}</pre>
<div class="blog_h1"><span class="graybg">Practical Applications</span></div>
<p>Having explored various advanced features of Kotlin, including coroutines, delegation, generics and variance, collections, and interoperability with Java, let's look at how these features are applied in real-world scenarios.</p>
<div class="blog_h2"><span class="graybg">Android Development with Kotlin</span></div>
<div class="blog_h3"><span class="graybg">Kotlin as the Preferred Language for Android</span></div>
<p>Kotlin is officially supported by Google as the preferred language for Android app development. It offers concise syntax, null safety, and powerful features that improve developer productivity.</p>
<div class="blog_h3"><span class="graybg">Using Coroutines for Asynchronous Tasks</span></div>
<p>Problem: Android apps often perform operations that can block the main thread, such as network requests or database operations. Blocking the main thread can lead to a poor user experience.</p>
<p>Solution with Coroutines: Use coroutines to perform asynchronous tasks without blocking the main thread.</p>
<p>Example:</p>
<pre class="crayon-plain-tag">class MainActivity : AppCompatActivity() {

    private val viewModel: DataViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Launches a coroutine tied to the lifecycle of the activity.
        lifecycleScope.launch {
            val data = viewModel.fetchData()
            updateUI(data)
        }
    }

    private fun updateUI(data: String) {
        // Update UI elements with the fetched data
    }
}

class DataViewModel : ViewModel() {
    // Switches the coroutine context to a background thread for I/O operations.
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
        // Simulate network call
        delay(1000)
        "Data from server"
    }
}</pre>
<div class="blog_h3"><span class="graybg">Leveraging Extensions and Delegation </span></div>
<p>Problem: Reducing boilerplate code and improving code organization in Android applications.</p>
<p>Solution with Extensions and Delegation</p>
<ol>
<li>Extensions: Add utility functions to existing classes without inheritance.</li>
<li>Delegation: Use property delegation for shared preferences.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">// Extension function for Toast messages
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

// Usage in an Activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        showToast("Welcome to the app!")
    }
}

// Delegated property for SharedPreferences
class UserPreferences(context: Context) {
    private val prefs: SharedPreferences by lazy {
        context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
    }

    var userName: String?
        get() = prefs.getString("user_name", null)
        set(value) = prefs.edit().putString("user_name", value).apply()
}</pre>
<div class="blog_h3"><span class="graybg">Handling Null Safety and Interoperability</span></div>
<p>Problem: Dealing with null references and integrating Java libraries in Android apps.</p>
<p>Solution:</p>
<ol>
<li>Utilize Kotlin's null safety features to prevent crashes.</li>
<li>Interoperate with Java libraries seamlessly.</li>
</ol>
<p>Example:</p>
<pre class="crayon-plain-tag">// Using null safety
val intentData: String? = intent.getStringExtra("data")
intentData?.let {
    // Use the data safely
}

// Interoperating with a Java library
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val date = dateFormat.parse("2023-10-01")</pre>
<div class="blog_h2"><span class="graybg">Server-Side Development</span></div>
<div class="blog_h3"><span class="graybg">Building RESTful APIs with Ktor</span></div>
<p>Ktor is an asynchronous framework for building microservices and web applications.</p>
<pre class="crayon-plain-tag">import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/hello") {
                call.respondText("Hello, World!", ContentType.Text.Plain)
            }
            post("/data") {
                val data = call.receive()
                call.respondText("Received: $data", ContentType.Text.Plain)
            }
        }
    }.start(wait = true)
}</pre>
<div class="blog_h3"><span class="graybg">Database Access with Exposed</span></div>
<pre class="crayon-plain-tag">// Define the table
object Users : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val age = integer("age")
    override val primaryKey = PrimaryKey(id)
}

// Perform database operations
transaction {
    // Insert a new user
    Users.insert {
        it[name] = "Alice"
        it[age] = 30
    }

    // Query users
    val userList = Users.selectAll().map {
        it[Users.name] to it[Users.age]
    }
}</pre>
<div class="blog_h3"><span class="graybg">Concurrency with Coroutines </span></div>
<p>Leverage coroutines for high-throughput server applications. Example:</p>
<pre class="crayon-plain-tag">suspend fun handleRequest(request: Request): Response = coroutineScope {
    val data = async { fetchDataFromDatabase() }
    val computation = async { performComputation() }

    // Wait for both tasks to complete
    Response(data.await(), computation.await())
}</pre>
<div class="blog_h3"><span class="graybg">Testing and Mocking in Kotlin</span></div>
<p>mockk library can be used for mocking:</p>
<pre class="crayon-plain-tag">class UserServiceTest {

    private val repository = mockk()
    private val userService = UserService(repository)

    //  In Kotlin, you can use backticks (``)* to enclose function names, allowing you to include spaces, 
    //  special characters, or even keywords that are normally not allowed in regular function identifiers. 
    @Test
    fun `should return user when found`() = runBlocking {
        val user = User(1, "Alice")
        coEvery { repository.findUser(1) } returns user

        val result = userService.getUser(1)

        assertEquals(user, result)
        coVerify { repository.findUser(1) }
    }
}</pre>
<div class="blog_h2"><span class="graybg">Integrating Kotlin with Spring Framework</span></div>
<div class="blog_h3"><span class="graybg">Build Configuration</span></div>
<p>Gradle (build.gradle.kts)</p>
<pre class="crayon-plain-tag">plugins {
    id("org.springframework.boot") version "3.1.0"
    id("io.spring.dependency-management") version "1.1.0"
    kotlin("jvm") version "1.9.10"
    kotlin("plugin.spring") version "1.9.10"
    kotlin("plugin.jpa") version "1.9.10"
}

group = "cc.gmem"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    // Add other dependencies as needed
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.mockk:mockk:1.13.5")
}

tasks.withType {
    useJUnitPlatform()
}</pre>
<div class="blog_h3"><span class="graybg">Creating a RESTful Web Service</span></div>
<p>Application Entry Point:</p>
<pre class="crayon-plain-tag">package cc.gmem.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DemoApplication

fun main(args: Array) {
    runApplication(*args)
}</pre>
<div class="blog_h3"><span class="graybg">Creating a Controller</span></div>
<pre class="crayon-plain-tag">package cc.gmem.demo.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

data class Greeting(val id: Long, val content: String)

@RestController
class GreetingController {

    @GetMapping("/greeting/{name}")
    fun greeting(@PathVariable name: String): Greeting {
        return Greeting(id = 1, content = "Hello, $name!")
    }
}</pre>
<div class="blog_h3"><span class="graybg">Running the Application</span></div>
<pre class="crayon-plain-tag">./gradlew bootRun</pre>
<div class="blog_h3"><span class="graybg">Dependency Injection and Bean Configuration</span></div>
<p>Kotlin encourages constructor injection due to its concise syntax.</p>
<pre class="crayon-plain-tag">package cc.gmem.demo.service

import org.springframework.stereotype.Service

@Service
class UserService(private val userRepository: UserRepository) {

    fun findAllUsers(): List = userRepository.findAll()
}</pre>
<div class="blog_h3"><span class="graybg">Database Access with Spring Data JPA </span></div>
<p>Entity definition:</p>
<pre class="crayon-plain-tag">package cc.gmem.demo.model

import jakarta.persistence.*

@Entity
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    val name: String,
    val email: String
)</pre>
<p>Repository interface:</p>
<pre class="crayon-plain-tag">package cc.gmem.demo.repository

import cc.gmem.demo.model.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository&lt;User, Long&gt; {
    fun findByEmail(email: String): User?
}</pre>
<div class="blog_h3"><span class="graybg">Using the Repository in a Service</span></div>
<pre class="crayon-plain-tag">package cc.gmem.demo.service

import cc.gmem.demo.model.User
import cc.gmem.demo.repository.UserRepository
import org.springframework.stereotype.Service

@Service
class UserService(private val userRepository: UserRepository) {

    fun getUserByEmail(email: String): User? = userRepository.findByEmail(email)
}</pre>
<div class="blog_h3"><span class="graybg">Controller Endpoint</span></div>
<pre class="crayon-plain-tag">package cc.gmem.demo.controller

import cc.gmem.demo.model.User
import cc.gmem.demo.service.UserService
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {

    @GetMapping("/{email}")
    fun getUser(@PathVariable email: String): User? {
        return userService.getUserByEmail(email)
    }
}</pre>
<div class="blog_h3"><span class="graybg">Defining Beans with @Configuration</span></div>
<pre class="crayon-plain-tag">package cc.gmem.demo.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import cc.gmem.demo.service.CustomService

@Configuration
class AppConfig {

    @Bean
    fun customService(): CustomService = CustomService()
}</pre>
<div class="blog_h3"><span class="graybg">Asynchronous Request Handling </span></div>
<p>Spring Framework supports coroutines and reactive programming.</p>
<p>Add kotlinx-coroutines-reactor as a dependency:</p>
<pre class="crayon-plain-tag">implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")</pre>
<p>Using suspend Functions in Controllers:</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.delay
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class AsyncController {

    @GetMapping("/async")
    suspend fun getAsyncData(): String {
        delay(1000) // Simulate non-blocking delay
        return "Async Response"
    }
}</pre>
<div class="blog_h3"><span class="graybg">Testing Spring Applications in Kotlin</span></div>
<p>Writing Tests with JUnit 5:</p>
<pre class="crayon-plain-tag">package cc.gmem.demo.service

import cc.gmem.demo.model.User
import cc.gmem.demo.repository.UserRepository
import io.mockk.*
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class UserServiceTest {

    private val userRepository = mockk()
    private val userService = UserService(userRepository)

    @Test
    fun `should return user when email exists`() {
        val email = "test@example.com"
        val user = User(id = 1, name = "Test User", email = email)

        every { userRepository.findByEmail(email) } returns user

        val result = userService.getUserByEmail(email)

        assertEquals(user, result)
        verify { userRepository.findByEmail(email) }
    }
}</pre>
<div class="blog_h2"><span class="graybg">Spring Reactive in Kotlin</span></div>
<p>Spring WebFlux is the reactive web framework in the Spring ecosystem, designed for building non-blocking, event-driven applications. When combined with Kotlin's coroutines, Spring WebFlux provides an elegant, efficient way to handle asynchronous operations.</p>
<p>In this section, we'll explore how to set up reactive routes in Kotlin using Spring WebFlux and coroutines. We'll focus on declarative routing with <pre class="crayon-plain-tag">coRouter</pre>, handling asynchronous data flows, and writing clean, functional code in Kotlin.</p>
<div class="blog_h3"><span class="graybg">What is Reactive Programming?</span></div>
<p>Reactive programming is a programming paradigm focused on building asynchronous, non-blocking systems that are scalable and resilient. Unlike traditional blocking I/O, reactive programming allows your application to <span style="background-color: #c0c0c0;">handle a large number of requests efficiently by reacting to incoming data streams as they arrive, rather than waiting for blocking operations to complete</span>.</p>
<p>Spring WebFlux is the reactive counterpart of Spring MVC, and it is built on the Project Reactor library, which provides the core reactive API.</p>
<div class="blog_h3"><span class="graybg">Key Concepts in Spring WebFlux</span></div>
<p>Spring WebFlux introduces several new concepts for building reactive applications:</p>
<ul>
<li>Mono - Represents a single asynchronous value or an empty value.</li>
<li>Flux - Represents a stream of asynchronous values, zero or more.</li>
<li>Non-blocking I/O - WebFlux runs on top of a non-blocking I/O framework such as Netty or Undertow.</li>
<li>Coroutines - Kotlin’s coroutines allow us to write asynchronous, non-blocking code in a declarative style.</li>
</ul>
<p>In Kotlin, using coroutines makes reactive programming more intuitive by handling asynchronous tasks in a sequential, readable manner.</p>
<div class="blog_h3"><span class="graybg">Setting up Reactive Routes in Kotlin</span></div>
<p>One of the core features of Spring WebFlux is the ability to define reactive routes using Kotlin DSL with <pre class="crayon-plain-tag">coRouter</pre>. This allows for a clean, functional approach to route definition.</p>
<p>Here’s how you can define reactive routes:</p>
<pre class="crayon-plain-tag">import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.server.coRouter

@Configuration
class RoutesConfig {

    @Bean
    fun apiRouter(handler: ApiHandler) = coRouter {
        "/api".nest {
            GET("/items", handler::getAllItems)
            GET("/items/{id}", handler::getItemById)
            POST("/items", handler::createItem)
        }
    }
}</pre>
<p>In this example, the <pre class="crayon-plain-tag">coRouter</pre> function is used to define reactive routes in a Kotlin DSL style:</p>
<ul>
<li>The GET and POST methods define routes that map to specific paths, like <pre class="crayon-plain-tag">/items</pre> and <pre class="crayon-plain-tag">/items/{id}</pre>.</li>
<li><pre class="crayon-plain-tag">handler::getAllItems</pre> refers to handler functions that will process incoming requests asynchronously.</li>
<li><pre class="crayon-plain-tag">nest</pre> is used to group multiple routes under a common path, such as <pre class="crayon-plain-tag">/api</pre>.</li>
</ul>
<p>This approach replaces traditional annotation-based controllers with a more declarative and functional style, making the code more concise and readable.</p>
<div class="blog_h3"><span class="graybg">Kotlin Coroutines in WebFlux</span></div>
<p>Spring WebFlux, when used with Kotlin, leverages coroutines for asynchronous processing. This helps eliminate the complexity of callbacks and makes reactive code look more like traditional blocking code while retaining its non-blocking nature.</p>
<p>For instance, here’s a handler function written with coroutines:</p>
<pre class="crayon-plain-tag">import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse

class ApiHandler(private val service: ItemService) {

    suspend fun getAllItems(request: ServerRequest): ServerResponse {
        val items = service.getAllItems().collectList().awaitSingle()
        return ServerResponse.ok().bodyValueAndAwait(items)
    }

    suspend fun getItemById(request: ServerRequest): ServerResponse {
        val id = request.pathVariable("id")
        val item = service.getItemById(id).awaitSingleOrNull()
        return item?.let { ServerResponse.ok().bodyValueAndAwait(it) }
            ?: ServerResponse.notFound().buildAndAwait()
    }

    suspend fun createItem(request: ServerRequest): ServerResponse {
        val newItem = request.awaitBody()
        val savedItem = service.saveItem(newItem).awaitSingle()
        return ServerResponse.ok().bodyValueAndAwait(savedItem)
    }
}

// Implementation of ItemService

import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

data class Item(val id: String, val name: String, val price: Double)

interface ItemService {
    fun getAllItems(): Flux&lt;Item&gt;
    fun getItemById(id: String): Mono&lt;Item&gt;
    fun saveItem(item: Item): Mono&lt;Item&gt;
}

class ItemServiceImpl : ItemService {

    private val items = mutableListOf(
        Item("1", "Laptop", 999.99),
        Item("2", "Smartphone", 599.99),
        Item("3", "Tablet", 299.99)
    )

    override fun getAllItems(): Flux&lt;Item&gt; {
        return Flux.fromIterable(items)
    }

    override fun getItemById(id: String): Mono&lt;Item&gt; {
        val item = items.find { it.id == id }
        return if (item != null) {
            Mono.just(item)
        } else {
            Mono.empty()
        }
    }

    override fun saveItem(item: Item): Mono&lt;Item&gt; {
        items.add(item)
        return Mono.just(item)
    }
}</pre>
<p>In this code:</p>
<ul>
<li>awaitSingle() is used to await the result of a <pre class="crayon-plain-tag">Mono</pre> without blocking the thread.</li>
<li>suspend functions are used to declare that these functions will be executed asynchronously in a non-blocking manner using coroutines.</li>
<li>Handlers return a <pre class="crayon-plain-tag">ServerResponse</pre> object, which is a reactive response object in WebFlux.</li>
</ul>
<p>Kotlin’s coroutine support in WebFlux allows you to avoid callback hell and write non-blocking code in a sequential, easy-to-read style.</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/comprehensive-study-kotlin-java-developers">A Comprehensive Study of Kotlin for Java Developers</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/comprehensive-study-kotlin-java-developers/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>利用LangChain和语言模型交互</title>
		<link>https://blog.gmem.cc/langchain</link>
		<comments>https://blog.gmem.cc/langchain#comments</comments>
		<pubDate>Tue, 04 Jul 2023 02:53:33 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[AI]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=39335</guid>
		<description><![CDATA[<p>LangChain是什么 从名字上可以看出来，LangChain可以用来构建自然语言处理能力的链条。它是一个库，提供了统一的接口，来和各种语言模型进行交互。更重要的是，它支持使用插件（工具），让语言模型能够获取实时的知识。LangChain基于Python，如果你熟悉JavaScript/TypeScript，可以考虑使用LangChain.js。 具体来说，LangChain包括以下模块： Model I/O：提供针对语言模型的统一风格接口 数据连接（Data connection）：提供应用程序相关数据的接口 处理链（Chains）：用于构建针对模型、提示词、工具调用的流水线 代理（Agents）：自动选择适当工具解决问题 记忆（Memory）：跨越多次处理链调用，来保持程序状态信息 Callbacks：在处理链的中间步骤中进行回调 安装 [crayon-69e1486ba8361388229805/] 快速参考 调用语言模型 以OpenAI为例，安装依赖： [crayon-69e1486ba8369759697443/] 通过环境变量来设置API Key： [crayon-69e1486ba836c009924744/] 如果需要通过代理来访问OpenAPI的API端点，参考下面的方式设置环境变量： [crayon-69e1486ba836e209988616/] <a class="read-more" href="https://blog.gmem.cc/langchain">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/langchain">利用LangChain和语言模型交互</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">LangChain是什么</span></div>
<p>从名字上可以看出来，LangChain可以用来构建自然语言处理能力的链条。它是一个库，提供了统一的接口，来和各种语言模型进行交互。更重要的是，它支持使用插件（工具），让语言模型能够获取实时的知识。LangChain基于Python，如果你熟悉JavaScript/TypeScript，可以考虑使用LangChain.js。</p>
<p>具体来说，LangChain包括以下模块：</p>
<ol>
<li>Model I/O：提供针对语言模型的统一风格接口</li>
<li>数据连接（Data connection）：提供应用程序相关数据的接口</li>
<li>处理链（Chains）：用于构建针对模型、提示词、工具调用的流水线</li>
<li>代理（Agents）：自动选择适当工具解决问题</li>
<li>记忆（Memory）：跨越多次处理链调用，来保持程序状态信息</li>
<li>Callbacks：在处理链的中间步骤中进行回调</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag"># 最小化依赖
pip install langchain

# 支持主流大语言模型
pip install langchain[llms]

# 支持所有能力，注意，占用很大磁盘空间且较为缓慢
pip install langchain[all]</pre>
<div class="blog_h1"><span class="graybg">快速参考</span></div>
<div class="blog_h2"><span class="graybg">调用语言模型</span></div>
<p>以OpenAI为例，安装依赖：</p>
<pre class="crayon-plain-tag">pip install openai</pre>
<p>通过环境变量来设置API Key：</p>
<pre class="crayon-plain-tag">export OPENAI_API_KEY="..."</pre>
<p>如果需要通过代理来访问OpenAPI的API端点，参考下面的方式设置环境变量：</p>
<pre class="crayon-plain-tag">HTTP_PROXY=https://user:pswd@proxy.gmem.cc
HTTPS_PROXY=https://user:pswd@proxy.gmem.cc</pre>
<p>下面的代码，根据你提供的输入来预测输出，也就是进行问答：</p>
<pre class="crayon-plain-tag">from langchain.llms import OpenAI
# 温度用来控制随机性，温度越高输出越随机
llm = OpenAI(temperature=0.9)
print(llm.predict("Hey what's up?"))
# Nothing much, just trying to get some work done. How about you?</pre>
<div class="blog_h2"><span class="graybg">使用对话模型</span></div>
<p>对话模型（Chat Models）是语言模型的变体。语言模型的输入、输出是一段文本，<span style="background-color: #c0c0c0;">对话模型的输入则是一系列消息，这些消息构成了对话的上下文</span>。对话模型的输出，则是一个消息。</p>
<p>LangChain将消息分为不同的类型，它们有对应的Python类：</p>
<ol>
<li>AIMessage：表示从模型角度发出的消息（a message sent from the perspective of the AI）</li>
<li>HumanMessage：表示从最终用户角度发出的消息</li>
<li>SystemMessage：用于提出系统级别的指令，例如指示语言模型该如何格式化输出</li>
<li>ChatMessage：通用消息类型，可以指定一个role参数，从而等价于上述消息之一</li>
</ol>
<p>这个消息分类，也是源自OpenAI的ChatGPT，ChatGPT会根据消息类型的不同，使用不同的处理方式。</p>
<pre class="crayon-plain-tag">if __name__ == "__main__":
    from langchain.chat_models import ChatOpenAI
    from langchain.schema import (
        SystemMessage,
        HumanMessage
    )

    chat = ChatOpenAI(temperature=0)
    resp = chat.predict_messages([
        SystemMessage(content="Translate whatever I say into Japanese."),
        HumanMessage(content="I love coding"),
    ])
    print(resp.content) # 私はコーディングが大好きです。</pre>
<div class="blog_h2"><span class="graybg">提示词模板</span></div>
<p>语言模型具有很大的灵活性，我们将其集成到自己的应用程序中时，通常需要加以限制，不能让最终用户随意的提供输入。这种情况下，可以使用提示词模板。</p>
<p>具体来说，就是由应用程序作者提供提示词的主体部分，最终用户仅仅填充提示词中的占位符。假设我们开发一个翻译器，那么用户可能仅仅需要指定待翻译的文本、目标语言：</p>
<pre class="crayon-plain-tag">from langchain.chat_models import ChatOpenAI
from langchain.prompts import SystemMessagePromptTemplate, HumanMessagePromptTemplate, ChatPromptTemplate

if __name__ == "__main__":
    system_message_prompt = SystemMessagePromptTemplate.from_template(
        """
        Act as a sophisticated translator between multiple languages. You can
        detect the language of a text, and translate it into a different one that I specify.
        """
    )
    human_message_prompt = HumanMessagePromptTemplate.from_template(
        """
        translate this sentence into {language}: {text}
        """
    )
    chat_prompt = ChatPromptTemplate.from_messages(
        [system_message_prompt, human_message_prompt]
    )
    messages = chat_prompt.format_messages(language="Chinese", text="Hey what's up?")

    chat = ChatOpenAI(temperature=0.5, model_name="gpt-3.5-turbo")
    resp = chat.predict_messages(messages)
    print(resp.content) # 嗨，最近怎么样？</pre>
<div class="blog_h2"><span class="graybg">处理链</span></div>
<p>处理链可以连接模型、提示词，或者其它处理链。最简单的例子：连接语言模型和提示词：</p>
<pre class="crayon-plain-tag">from langchain import LLMChain

chain = LLMChain(llm=chat, prompt=chat_prompt)
resp: str = chain.run(language="Chinese", text="Hey what's up?")
print(resp)  # 嗨，最近怎么样？</pre>
<div class="blog_h2"><span class="graybg">代理</span></div>
<p>上面定义了一个非常简单的处理链：渲染提示模板、调用LLM的API。某些复杂的流程下，需要根据输入动态的选择操作。</p>
<p>代理（Agent）能够完成这种动态选择，它调用语言模型来分析输入，从而确定需要先做什么，再做什么。代理能够反复的选择、运行工具，直到获取最终答案。</p>
<p>加载一个代理需要以下要素：</p>
<ol>
<li>LLM或者Chat模型：这个模型提供自然语言理解能力，用于驱动代理</li>
<li>工具（集）：工具是执行特定任务的函数，例如进行Google搜索、数据库查找、执行Python REPL、调用其它处理链</li>
<li>代理的名称：这个名称表示使用的代理类。代理类使用参数化的提示词来调用模型，从而确定下一步该做什么</li>
</ol>
<p>例如serpapi这个工具，它能够发起搜索引擎检索：</p>
<pre class="crayon-plain-tag"># You should provide your serpapi key with environment variable SERPAPI_API_KEY

from langchain.agents import load_tools, initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI

if __name__ == "__main__":
    llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
    tools = load_tools(["serpapi"], llm=llm)
    agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
    result = agent.run(
        """
        Who is the president of the United States? What did he do last week?
        """
    )
    print(result)</pre>
<p>Verbose设置为True能够输出处理链的整个决策过程：</p>
<p style="padding-left: 30px;">&gt; Entering new chain...<br />I need to find out who the current president is and what he did last week. I can use the search tool for this.<br />Action: Search<br />Action Input: "current president of the United States"<br />Observation: Joe Biden<br />Thought:I now know the current president of the United States. I can use the search tool again to find out what he did last week.<br />Action: Search<br />Action Input: "Joe Biden last week"<br />Observation: The Supreme Court on Friday struck down President Joe Biden's plan to cancel up to $20,000 of student debt for tens of millions of Americans, thwarting a major ...<br />Thought:I now know the final answer.<br />Final Answer: Joe Biden is the current president of the United States. Last week, the Supreme Court struck down his plan to cancel up to $20,000 of student debt.</p>
<p style="padding-left: 30px;">&gt; Finished chain.<br />Joe Biden is the current president of the United States. Last week, the Supreme Court struck down his plan to cancel up to $20,000 of student debt.</p>
<p>可以看到，代理把问题划分为两个子任务，并且通过搜索网络来解答问题。</p>
<div class="blog_h2"><span class="graybg">记忆</span></div>
<p>默认情况下处理链和代理是无状态的，很多情况下，我们需要依赖先前和模型的交互（作为上下文）。Memory模块提供了这种记住先前交互的机制，它支持基于最新的输入/输出来更新状态，或者基于已存储的状态，或者contextualize下一次输入。</p>
<p>Memory模块有多种实现，最简单的基于内存缓冲 —— 简单的将最近的几次输入/输出，作为当前输入的前缀：</p>
<pre class="crayon-plain-tag">from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.prompts import SystemMessagePromptTemplate, ChatPromptTemplate, MessagesPlaceholder, \
    HumanMessagePromptTemplate

if __name__ == "__main__":
    llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
    prompt = ChatPromptTemplate.from_messages([
        SystemMessagePromptTemplate.from_template(
            """
            The following is a conversation between a human and an AI. The AI is talkative and 
            provides lots of specific details from its context. If the AI does not know the answer
            to a question, it truthfully says it doesn't know.
            """
        ),
        MessagesPlaceholder(variable_name="history"),
        HumanMessagePromptTemplate.from_template("{input}")
    ])
    memory = ConversationBufferMemory(return_messages=True)
    conversation = ConversationChain(memory=memory, prompt=prompt, llm=llm)
    print(conversation.predict(input="My name is Alex"))
    print(conversation.predict(input="What is my name?"))</pre>
<p>这种实现有个重要的缺点：可能随着对话历史的增长，占用空间越来越大，因而影响AI的性能（或者超过长度限制）。通常需要考虑pruning或者summarization以保证上下文完整的同时不影响性能。</p>
<div class="blog_h1"><span class="graybg">语言模型接口</span></div>
<div class="blog_h2"><span class="graybg">提示词</span></div>
<p>语言模型都是将文本作为输入的，这些文本就被称为提示词（Prompt）。</p>
<p>在语言模型应用程序中，提示词通常不是硬编码的，也不是让用户随意输入。一般情况下提示词由模板、样例、用户输入组成，LangChain提供了若干类用来构造提示词。</p>
<div class="blog_h3"><span class="graybg">提示词模板</span></div>
<p>模板用于可复用的构造提示词，它由静态文本和一系列占位符（输入变量）构成。模板内容可以包括：</p>
<ol>
<li>对语言模型的指令</li>
<li>帮助语言模型生成更好结果的若干示例</li>
<li>对语言模型的提问</li>
</ol>
<p>这里是一个最简单的例子：</p>
<pre class="crayon-plain-tag">from langchain import PromptTemplate


template = """/
You are a naming consultant for new companies.
What is a good name for a company that makes {product}?
"""

prompt = PromptTemplate.from_template(template)
prompt.format(product="colorful socks")</pre>
<p>上面的代码能够自动根据template中的占位符，来推断由哪些输入变量。手工声明变量的方式如下：</p>
<pre class="crayon-plain-tag">multiple_input_prompt = PromptTemplate(
    input_variables=["adjective", "content"], 
    template="Tell me a {adjective} joke about {content}."
)
multiple_input_prompt.format(adjective="funny", content="chickens")</pre>
<p>除了这种语言模型通用提示模板，还有针对对话模型的模板类：SystemMessagePromptTemplate、AIMessagePromptTemplate、HumanMessagePromptTemplate。它们的区别上文已经介绍过。一个或者多个这些模板，可以构成ChatPromptTemplate：</p>
<pre class="crayon-plain-tag">#             返回值是PromptValue
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])

# 渲染模板为消息
chat_prompt.format_prompt(input_language="English", ... ).to_messages()</pre>
<div class="blog_h3"><span class="graybg">定制提示词模板</span></div>
<p>内置的提示词模板类（包括两种类型，一个是基于字符串的，一个是基于Chat的），都是根据输入变量，进行提示内容的格式化。</p>
<p>如果你需要以输入变量为键，从第三方（例如数据库，或者Feast这样的Feature Store）获取信息并生成提示词，可以选择自定义提示词模板。</p>
<p>这里以基于字符串的模板类为例来说明如何自定义提示词模板。我们常常会选择继承StringPromptTemplate，但是实际上，自定义提示词模板只需要满足两点：</p>
<ol>
<li>提供<pre class="crayon-plain-tag">input_variables</pre>属性，定义支持的输入变量的名字</li>
<li>提供<pre class="crayon-plain-tag">format</pre>方法，接收关键字参数，用于根据指定的输入变量来生成提示词</li>
</ol>
<p>下面这个模板的例子，以函数名为输入变量，生成让语言模型来解释函数用途的提示：</p>
<pre class="crayon-plain-tag">from langchain.prompts import StringPromptTemplate
from pydantic import BaseModel, validator


class FunctionExplainerPromptTemplate(StringPromptTemplate, BaseModel):
    @validator("input_variables")
    def validate_input_variables(cls, v):
        """Validate that the input variables are correct."""
        if len(v) != 1 or "function_name" not in v:
            raise ValueError("function_name must be the only input_variable.")
        return v

    def format(self, **kwargs) -&gt; str:
        # Get the source code of the function
        source_code = get_source_code(kwargs["function_name"])

        # Generate the prompt to be sent to the language model
        prompt = f"""
        Given the function name and source code, generate an English language explanation of the function.
        Function Name: {kwargs["function_name"].__name__}
        Source Code:
        {source_code}
        Explanation:
        """
        return prompt

    def _prompt_type(self):
        return "function-explainer"



fn_explainer = FunctionExplainerPromptTemplate(input_variables=["function_name"])
prompt = fn_explainer.format(function_name=get_source_code)
print(prompt)</pre>
<div class="blog_h3"><span class="graybg">少量样本提示词模板</span></div>
<p>所谓少量样本（Few-shot）是指给语言模型提供几个例子（shots），以使其能够更好的理解你的意图并生成更好的响应。</p>
<p>样本可以说明期望的输出格式、得到最终输出的途径等等，其形式是比较自由的，语言模型能够理解其中的意图。</p>
<p>下面是一个样本集的例子：</p>
<pre class="crayon-plain-tag">from langchain.prompts.few_shot import FewShotPromptTemplate
from langchain.prompts.prompt import PromptTemplate

examples = [
  {
    "question": "Who lived longer, Muhammad Ali or Alan Turing?",
    "answer": 
"""
Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad Ali
"""
  },
  {
    "question": "When was the founder of craigslist born?",
    "answer": 
"""
Are follow up questions needed here: Yes.
Follow up: Who was the founder of craigslist?
Intermediate answer: Craigslist was founded by Craig Newmark.
Follow up: When was Craig Newmark born?
Intermediate answer: Craig Newmark was born on December 6, 1952.
So the final answer is: December 6, 1952
"""
  }
  ...
]</pre>
<p>基于上述样本集生成提示：</p>
<pre class="crayon-plain-tag">prompt = FewShotPromptTemplate(
    examples=examples, 
    example_prompt=example_prompt, 
    suffix="Question: {input}", 
    input_variables=["input"]
)

print(prompt.format(input="Who was the father of Mary Ball Washington?"))</pre>
<p>最终生成的提示是这样的：</p>
<p style="padding-left: 30px;">Question: Who lived longer, Muhammad Ali or Alan Turing?</p>
<p style="padding-left: 30px;">Are follow up questions needed here: Yes.<br /> Follow up: How old was Muhammad Ali when he died?<br /> Intermediate answer: Muhammad Ali was 74 years old when he died.<br /> Follow up: How old was Alan Turing when he died?<br /> Intermediate answer: Alan Turing was 41 years old when he died.<br /> So the final answer is: Muhammad Ali</p>
<p style="padding-left: 30px;">Question: When was the founder of craigslist born?</p>
<p style="padding-left: 30px;">Are follow up questions needed here: Yes.<br /> Follow up: Who was the founder of craigslist?<br /> Intermediate answer: Craigslist was founded by Craig Newmark.<br /> Follow up: When was Craig Newmark born?<br /> Intermediate answer: Craig Newmark was born on December 6, 1952.<br /> So the final answer is: December 6, 1952</p>
<p style="padding-left: 30px;">Question: Who was the father of Mary Ball Washington?</p>
<p>我们可能会根据用户输入的语义，选择一个最匹配的样本，而不是把所有样本一股脑的发给语言模型。这时候，可以使用样本选择器。</p>
<p>向量存储用于保存Embeddings，所谓Embeddings，是指语言元素（例如单词、短语）在向量空间的数学表示（也就是多维向量）。向量存储支持进行语义相似度的判断，样本选择器依赖于向量存储的能力，来选取和输入最匹配的样本：</p>
<pre class="crayon-plain-tag">from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings


example_selector = SemanticSimilarityExampleSelector.from_examples(
    # 候选样本列表
    examples,
    # 这是一个embedding class，用于生成embeddings
    OpenAIEmbeddings(),
    # 这是一个向量存储，用于存储embeddings并进行相似度搜索
    Chroma,
    # This is the number of examples to produce.
    k=1
)

# 选择相似的样本
question = "Who was the father of Mary Ball Washington?"
selected_examples = example_selector.select_examples({"question": question})</pre>
<p>FewShotPromptTemplate这个提示模板，支持传入一个样本选择器，生成最终提示词：</p>
<pre class="crayon-plain-tag">prompt = FewShotPromptTemplate(
    example_selector=example_selector, 
    example_prompt=example_prompt, 
    suffix="Question: {input}", 
    input_variables=["input"]
)

print(prompt.format(input="Who was the father of Mary Ball Washington?"))</pre>
<div class="blog_h3"><span class="graybg">对话模型和少量样本</span></div>
<p>目前LangChains没有为对话模型设计特别的抽象。在对话模型中，你只需要在输入消息中，提供交替的（Alternating）人类/AI消息，即可产生少量样本的效果：</p>
<pre class="crayon-plain-tag">template = "You are a helpful assistant that translates english to pirate."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
example_human = HumanMessagePromptTemplate.from_template("Hi")
example_ai = AIMessagePromptTemplate.from_template("Argh me mateys")
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

chat_prompt = ChatPromptTemplate.from_messages(
    #                       交替的消息作为样本
    [system_message_prompt, example_human, example_ai, human_message_prompt]
)</pre>
<p>对于OpenAI来说，可以使用系统消息，并用可选的name属性来“标记”样本：</p>
<pre class="crayon-plain-tag">template = "You are a helpful assistant that translates english to pirate."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
example_human = SystemMessagePromptTemplate.from_template(
    "Hi", additional_kwargs={"name": "example_user"}  # 样本的人类消息
)
example_ai = SystemMessagePromptTemplate.from_template(
    "Argh me mateys", additional_kwargs={"name": "example_assistant"} # 样本的AI消息
)
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)</pre>
<div class="blog_h3"><span class="graybg">模板格式和输出</span></div>
<p>默认情况下，PromptTemplate的字符串，被看作是Python的f-string，可以通过template_format指定其它格式，目前仅仅支持jinja2模板：</p>
<pre class="crayon-plain-tag">jinja2_template = "Tell me a {{ adjective }} joke about {{ content }}"
prompt_template = PromptTemplate.from_template(template=jinja2_template, template_format="jinja2")</pre>
<p>调用提示词模板的format*方法，可以得到字符串、消息列表或者ChatPromptValue：</p>
<pre class="crayon-plain-tag"># 输出为str
output = chat_prompt.format(input_language="English", output_language="French", text="I love programming.")
output = chat_prompt.format_prompt(input_language="English", output_language="French", text="I love programming.").to_string()

# 输出为ChatPromptValue
chat_prompt.format_prompt(input_language="English", output_language="French", text="I love programming.")

# 输出为List[BaseMessage]
chat_prompt.format_prompt(input_language="English", output_language="French", text="I love programming.").to_messages()</pre>
<div class="blog_h3"><span class="graybg">部分渲染模板</span></div>
<p>LangChain允许仅部分渲染提示词模板 —— 传入输入变量的子集，生成一个新模板，新模板仅仅使用那些尚未渲染的变量。</p>
<pre class="crayon-plain-tag">from langchain.prompts import PromptTemplate

prompt = PromptTemplate(template="{foo}{bar}", input_variables=["foo", "bar"])
partial_prompt = prompt.partial(foo="foo");
print(partial_prompt.format(bar="baz"))


prompt = PromptTemplate(template="{foo}{bar}", input_variables=["bar"], partial_variables={"foo": "foo"})
print(prompt.format(bar="baz"))</pre>
<p>除了将直接量赋予输入变量，还可以使用函数，这种方式可以让每次进行部分渲染，得到不同结果：</p>
<pre class="crayon-plain-tag">from datetime import datetime

def _get_datetime():
    now = datetime.now()
    return now.strftime("%m/%d/%Y, %H:%M:%S")


prompt = PromptTemplate(
    template="Tell me a {adjective} joke about the day {date}", 
    input_variables=["adjective", "date"]
);
partial_prompt = prompt.partial(date=_get_datetime)



prompt = PromptTemplate(
    template="Tell me a {adjective} joke about the day {date}", 
    input_variables=["adjective"],
    partial_variables={"date": _get_datetime}
);</pre>
<div class="blog_h3"><span class="graybg">从文件加载提示词模板</span></div>
<p>LangChain支持从JSON或者YAML文件中加载提示词。你可以将模板、样本放在一个文件中，也可以放在多个文件中并相互引用。</p>
<p>考虑下面这个YAML形式的提示词模板：</p>
<pre class="crayon-plain-tag">_type: prompt
input_variables:
  ["adjective", "content"]
template:
  Tell me a {adjective} joke about {content}.</pre>
<p>我们可以这样加载它：</p>
<pre class="crayon-plain-tag">from langchain.prompts import load_prompt
prompt = load_prompt("simple_prompt.yaml")
print(prompt.format(adjective="funny", content="chickens"))</pre>
<p>注意，不管是YAML还是JSON，都用上述方式加载，没有区别。</p>
<p>考虑下面这个提示词模板，它引用了在外部文件中定义的模板文本：</p>
<pre class="crayon-plain-tag">{
  "_type": "prompt",
  "input_variables": ["adjective", "content"],
  "template_path": "simple_template.txt"
}</pre><br />
<pre class="crayon-plain-tag">Tell me a {adjective} joke about {content}.</pre>
<p>LangChain会自动到相同目录下寻找txt文件。加载方式和上文一致。</p>
<p>除了PromptTemplate，我们也可用相同的方式加载FewShotPromptTemplate。下面这个例子，将少量样本放在独立文件中：</p>
<pre class="crayon-plain-tag">[
  {"input": "happy", "output": "sad"},
  {"input": "tall", "output": "short"}
]</pre><br />
<pre class="crayon-plain-tag"># 这个标注模板类型
_type: few_shot
input_variables:
  ["adjective"]
# 提示词前缀
prefix:
  Write antonyms for the following words.
# 样本模板
example_prompt:
  _type: prompt
  input_variables:
    ["input", "output"]
  template:
    "Input: {input}\nOutput: {output}"
# 指定样本文件
examples:
  examples.json
# 提示词后缀
suffix:
  "Input: {adjective}\nOutput:"</pre>
<p>加载模板：</p>
<pre class="crayon-plain-tag">prompt = load_prompt("few_shot_prompt.yaml")
print(prompt.format(adjective="funny"))</pre>
<p>渲染得到的结果如下：</p>
<p style="padding-left: 30px;">Write antonyms for the following words.</p>
<p style="padding-left: 30px;">Input: happy<br /> Output: sad</p>
<p style="padding-left: 30px;">Input: tall<br /> Output: short</p>
<p style="padding-left: 30px;">Input: funny<br /> Output:</p>
<div class="blog_h2"><span class="graybg">样本选择器 </span></div>
<p>当有大量样本时， 可能需要选择一些最匹配的放入到提示中，这是需要用到样本选择器。样本选择器的基类如下：</p>
<pre class="crayon-plain-tag">class BaseExampleSelector(ABC):
    """Interface for selecting examples to include in prompts."""

    @abstractmethod
    def select_examples(self, input_variables: Dict[str, str]) -&gt; List[dict]:
        """Select which examples to use based on the inputs."""</pre>
<p>唯一需要实现的方法，入参是输入变量，结果是选中样本的列表。</p>
<div class="blog_h3"><span class="graybg">基于长度选择</span></div>
<p>LengthBasedExampleSelector基于长度即字符数量进行选择，当你担心提示词超过上下文窗口大小（Token限制）时有用。如果用户输入太长，该选择器会选取较小的样本：</p>
<pre class="crayon-plain-tag">from langchain.prompts import PromptTemplate
from langchain.prompts import FewShotPromptTemplate
from langchain.prompts.example_selector import LengthBasedExampleSelector

examples = [
    {"input": "happy", "output": "sad"},
    {"input": "tall", "output": "short"},
    {"input": "energetic", "output": "lethargic"},
    {"input": "sunny", "output": "gloomy"},
    {"input": "windy", "output": "calm"},

example_prompt = PromptTemplate(
    input_variables=["input", "output"],
    template="Input: {input}\nOutput: {output}",
)
example_selector = LengthBasedExampleSelector(
    # 可选择样本集
    examples=examples, 
    # 提示模板
    example_prompt=example_prompt, 
    # 格式化后样本的长度，长度使用get_text_length 函数计算
    max_length=25,
    get_text_length: Callable[[str], int] = lambda x: len(re.split("\n| ", x))
)
dynamic_prompt = FewShotPromptTemplate(
    example_selector=example_selector,
    example_prompt=example_prompt,
    prefix="Give the antonym of every input",
    suffix="Input: {adjective}\nOutput:", 
    input_variables=["adjective"],
)</pre>
<div class="blog_h3"><span class="graybg">基于MMR选择</span></div>
<p>所谓最大边际相关性 maximal marginal relevance (MMR)是一种信息检索和文本摘要的技术，用于在<span style="background-color: #c0c0c0;">保持查询相关性的同时，最大限度地减少结果集中的冗余信息</span>。MMR的目的是在结果集中提供尽可能多的新信息，以增加用户获取有价值信息的机会。</p>
<p>MMR算法通过在查询相关性和结果间的新颖性之间进行权衡来实现这一目标。对于每个候选结果，MMR会计算与查询的相关性以及与已选择结果的相似性。然后，根据一个权衡参数（通常表示为λ），MMR会选择具有最大边际相关性的结果。这种方法有助于<span style="background-color: #c0c0c0;">在结果集中提供多样性，避免重复内容，同时确保结果与用户的查询相关</span>。</p>
<p>进行相似性判断时，MMR使用最大余弦相似性（greatest cosine similarity）。它计算样本、输入的Embeddings在高维空间的向量的夹角，这个夹角越小（也就是余弦越大）意味着相似度越高。</p>
<p>与此同时，MMR尽量保证这些样本之间的差异性。</p>
<pre class="crayon-plain-tag">example_selector = MaxMarginalRelevanceExampleSelector.from_examples(
    examples,
    # 产生Embeddings
    OpenAIEmbeddings(),
    # 选择一个向量存储类
    FAISS,
    # 选择的样本数量
    k=2,
)</pre>
<div class="blog_h3"><span class="graybg">基于n-gram重叠选择</span></div>
<p>n-gram overlap是另外一种衡量文本相似性的方法，用于比较两个文本序列中的n-gram序列。<span style="background-color: #c0c0c0;">n-gram是指在给定文本中连续出现的n个元素（通常是字符或单词）</span>。n-gram重叠计算的是两个文本序列中共享的n-gram数量，以评估它们之间的相似性。假设我们比较以下两个句子的单词级别的2-gram（bi-gram）：</p>
<ol style="list-style-type: undefined;">
<li>我喜欢吃苹果</li>
<li>我喜欢吃香蕉</li>
</ol>
<p>句子1的bi-grams：(我喜欢)，(喜欢吃)，(吃苹果) 句子2的bi-grams：(我喜欢)，(喜欢吃)，(吃香蕉)。这两个句子共享了2个bi-grams：(我喜欢)和(喜欢吃)。因此，它们的bi-gram重叠数量为2。</p>
<p>n-gram重叠通常用于自然语言处理任务，如文本分类、文本摘要、机器翻译评估和文档聚类等。通过计算n-gram重叠，可以评估文本之间的结构和内容相似性。n-gram重叠可能受到n的选择和文本长度的影响。</p>
<pre class="crayon-plain-tag">example_selector = NGramOverlapExampleSelector(
    examples=examples,
    example_prompt=example_prompt,
    # This is the threshold, at which selector stops.
    # It is set to -1.0 by default.
    threshold=-1.0,
    # For negative threshold:
    # Selector sorts examples by ngram overlap score, and excludes none.
    # For threshold greater than 1.0:
    # Selector excludes all examples, and returns an empty list.
    # For threshold equal to 0.0:
    # Selector sorts examples by ngram overlap score,
    # and excludes those with no ngram overlap with input.
)</pre>
<div class="blog_h3"><span class="graybg">基于相似度选择</span></div>
<p>这种方式仅仅用最大余弦相似性（greatest cosine similarity）来计算输入和样本的相似度，不考虑样本之间的差异性。</p>
<pre class="crayon-plain-tag">example_selector = SemanticSimilarityExampleSelector.from_examples(
    examples, 
    OpenAIEmbeddings(), 
    # 向量存储实现
    Chroma, 
    # 选择的样本数量
    k=1
)</pre>
<div class="blog_h2"><span class="graybg">关于语言模型</span></div>
<p>LangChain提供针对两类模型的接口：</p>
<ol>
<li>大语言模型（LLMs）：如上文所述，这类接口以文本为输入，文本为输出</li>
<li>对话模型：其是基于语言模型的封装，以消息列表为输入，消息为输出</li>
</ol>
<div class="blog_h2"><span class="graybg">LLM模型</span></div>
<p>LangChains不提供任何模型，它知识提供了操控各种语言模型的标准接口，支持的LLM提供者包括：OpenAI、Hugging Face、Cohere等。</p>
<div class="blog_h3"><span class="graybg">异步API</span></div>
<p>LangChain基于asyncio库实现了异步AI的支持：</p>
<pre class="crayon-plain-tag">async def async_generate(llm):
    resp = await llm.agenerate(["Hello, how are you?"])
    print(resp.generations[0][0].text)

llm = OpenAI(temperature=0.9)
tasks = [async_generate(llm) for _ in range(10)]
await asyncio.gather(*tasks)</pre>
<div class="blog_h3"><span class="graybg">封装新模型</span></div>
<p>要封装一个LangChain不支持的语言模型，你仅仅需要继承抽象类LLM即可，需要实现的方法包括：</p>
<ol>
<li>属性 _llm_type ，给这个模型一个类型名</li>
<li>_call 调用模型</li>
<li>_acall 异步调用模型</li>
<li>属性 _identifying_params，返回一个字典，用于打印模型实例的关键参数</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">from typing import Optional, List, Mapping, Any

from langchain.callbacks.manager import CallbackManagerForLLMRun, AsyncCallbackManagerForLLMRun
from langchain.llms.base import LLM


class CustomLLM(LLM):
    n: int

    @property
    def _llm_type(self) -&gt; str:
        return "custom"

    def _call(
            self,
            prompt: str,
            stop: Optional[List[str]] = None,
            run_manager: Optional[CallbackManagerForLLMRun] = None,
    ) -&gt; str:
        if stop is not None:
            raise ValueError("stop kwargs are not permitted.")
        return prompt[: self.n]

    async def _acall(self, prompt: str, stop: Optional[List[str]] = None,
                     run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, **kwargs: Any) -&gt; str:
        return self._call(prompt, stop, run_manager)

    @property
    def _identifying_params(self) -&gt; Mapping[str, Any]:
        """Get the identifying parameters."""
        return {"n": self.n}


if __name__ == "__main__":
    # 构造函数已经由基类定义
    llm = CustomLLM(n=10)

    print(llm("Hello, world!"))</pre>
<div class="blog_h3"><span class="graybg">FakeLLM</span></div>
<p>这个模型用于测试，它根据静态的仿冒来响应请求：</p>
<pre class="crayon-plain-tag">responses = ["Action: Python REPL\nAction Input: print(2 + 2)", "Final Answer: 4"]
llm = FakeListLLM(responses=responses)</pre>
<div class="blog_h3"><span class="graybg">HumanInputLLM</span></div>
<p>这个模型也用于测试或调试目的，它让人类去扮演LLM，从而可以测试Agent的行为：</p>
<pre class="crayon-plain-tag">from langchain.llms.human import HumanInputLLM

from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType

if __name__ == "__main__":
    tools = load_tools(["wikipedia"])
    llm = HumanInputLLM(
        # 这里用来格式化提示词
        # 使用Agent和工具时，喂给语言模型的提示词会自动生成
        # 你要模仿语言模型的行为（理解自然语言的能力、遵从提示词的要求）
        prompt_func=lambda prompt: print(
            f"\n===PROMPT START====\n{prompt}\n=====PROMPT END======"
        )
    )
    agent = initialize_agent(
        tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
    )

    agent.run("Who is Joe Biden?")</pre>
<p> 运行后，显示提示词：</p>
<p style="padding-left: 30px;">===PROMPT START====<br />真正的语言模型会理解这里的提示词，测试时人类要模仿语言模型的行为<br />Answer the following questions as best you can. You have access to the following tools:</p>
<p style="padding-left: 30px;">这里告诉语言模型，工具及其用途<br />Wikipedia: A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.</p>
<p style="padding-left: 30px;">这里告诉语言模型如何做，因为它的语言理解能力，应答中会给出足够的信息，让代理知道用什么输入调用什么工具<br />Use the following format:</p>
<p style="padding-left: 30px;">Question: the input question you must answer<br />Thought: you should always think about what to do<br />Action: the action to take, should be one of [Wikipedia]<br />Action Input: the input to the action<br />Observation: the result of the action<br />... (this Thought/Action/Action Input/Observation can repeat N times)<br />Thought: I now know the final answer<br />Final Answer: the final answer to the original input question</p>
<p style="padding-left: 30px;">Begin!</p>
<p style="padding-left: 30px;">Question: Who is Joe Biden?<br />Thought:<br />=====PROMPT END======</p>
<p>现在人类可以输入，这个输入就是作为语言模型的响应：</p>
<p style="padding-left: 30px;">I need to use a tool   注意这个是 Thought<br />Action: Wikipedia<br />Action Input: Joe Biden<br />Observation: Page: Joe Biden</p>
<p>由于上述响应，代理就知道需要用关键字Joe Biden来搜索维基百科了。工具会生成页面的摘要文本，代理则会基于这些文本，继续和语言模型交互。</p>
<p>人类扮演的模型可以继续进行上述Thought/Action/Action Input/Observation交互，并且在最终给出输入：</p>
<p style="padding-left: 30px;">Final Answer: Joe Biden is an old man</p>
<p>当看到 Final Answer后，代理会终止处理链。</p>
<div class="blog_h3"><span class="graybg">缓存</span></div>
<p>LangChain提供了针对LLM的缓存层，用于：</p>
<ol>
<li>减少对LLM API的调用，节省费用</li>
<li>针对相同问题，提升响应速度</li>
</ol>
<pre class="crayon-plain-tag">import langchain

from langchain import OpenAI

if __name__ == "__main__":
    llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2)
    # 内存缓存
    from langchain.cache import InMemoryCache

    langchain.llm_cache = InMemoryCache()
    print(llm.predict("Tell me a joke"))  # 慢
    print(llm.predict("Tell me a joke"))  # 快



# SQLite缓存
from langchain.cache import SQLiteCache
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")</pre>
<p>在处理链中，可以选择为特定节点关闭缓存：</p>
<pre class="crayon-plain-tag">import requests
from langchain import OpenAI
from langchain.text_splitter import CharacterTextSplitter
from langchain.docstore.document import Document
from langchain.chains.summarize import load_summarize_chain

if __name__ == "__main__":
    url = 'https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt'
    state_of_the_union = requests.get(url).text
    llm = OpenAI(model_name="text-davinci-002")
    # disable caching
    no_cache_llm = OpenAI(model_name="text-davinci-002", cache=False)

    text_splitter = CharacterTextSplitter()
    texts = text_splitter.split_text(state_of_the_union)

    # select the first 3 texts and build a document for each
    docs = [Document(page_content=t) for t in texts[:3]]
    # produce a summarization of the documents with map_reduce chain, the map step
    # will be cached ( using non-cahced llm). The reduce( combine ) step won't be frozen
    # because we are using a llm with cache disabled
    chain = load_summarize_chain(llm, chain_type="map_reduce", reduce_llm=no_cache_llm)

    # first run will be slow, but the map step will get cached
    print(chain.run(docs))

    # here we run it again, it will be substantially faster while producing a different answer
    # cause the combine step is not cached
    print(chain.run(docs))</pre>
<div class="blog_h3"><span class="graybg">从文件加载LLM配置</span></div>
<p>LangChain支持从文件实例化LLM所需的配置：</p>
<pre class="crayon-plain-tag">from langchain.llms import OpenAI
from langchain.llms.loading import load_llm

# load from JSON, notice that YAML is also acceptable
llm = load_llm("llm.json")

# persist the llm config to file
llm.save("llm.json")</pre>
<div class="blog_h3"><span class="graybg">生成流式响应</span></div>
<p>某些LLM模型支持提供流式响应，也就是不需要等待所有响应生成后再返回给客户端，我们使用的ChatGPT之类的聊天应用，都会使用这种响应模式。</p>
<p>LangChain支持OpenAI、ChatOpenAI、ChatAnthropic的流式响应。你需要编写一个<pre class="crayon-plain-tag">CallbackHandler</pre>回调，实现<pre class="crayon-plain-tag">on_llm_new_token</pre>方法。StreamingStdOutCallbackHandler是LangChain自带的一个实现：</p>
<pre class="crayon-plain-tag">class StreamingStdOutCallbackHandler(BaseCallbackHandler):
    """Callback handler for streaming. Only works with LLMs that support streaming."""

    def on_llm_new_token(self, token: str, **kwargs: Any) -&gt; None:
        """Run on new LLM token. Only available when streaming is enabled."""
        sys.stdout.write(token)
        sys.stdout.flush()</pre>
<p> 它简单的将新产生的Token写入标准输出：</p>
<pre class="crayon-plain-tag">if __name__ == "__main__":
    from langchain.llms import OpenAI
    from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

    llm = OpenAI(streaming=True, callbacks=[StreamingStdOutCallbackHandler()], temperature=0)
    resp = llm("Write me a song about sparkling water.")</pre>
<div class="blog_h3"><span class="graybg">跟踪Token用量</span></div>
<p>使用一个上下文管理器来包围你的代码，可以统计这些代码的Token用量，从而估算成本。目前仅仅支持OpenAI。</p>
<pre class="crayon-plain-tag">from langchain.llms import OpenAI
from langchain.callbacks import get_openai_callback

llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2)

with get_openai_callback() as cb:
    result = llm("Tell me a joke")
    print(cb)


with get_openai_callback() as cb:
    response = agent.run(
        "Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?"
    )
    print(f"Total Tokens: {cb.total_tokens}")
    print(f"Prompt Tokens: {cb.prompt_tokens}")
    print(f"Completion Tokens: {cb.completion_tokens}")
    print(f"Total Cost (USD): ${cb.total_cost}")</pre>
<div class="blog_h3"><span class="graybg">和其它LLM集成</span></div>
<p>详细的支持列表可以看<a href="https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai">官方文档</a>。</p>
<p>Hugging Face Hub是模型托管平台，它提供开源的120K模型、20K数据集、50K演示应用。LangChain支持直接使用Hugging Face上的模型。首先需要安装包：</p>
<pre class="crayon-plain-tag">pip install huggingface_hub</pre>
<p>设置你的API Token：</p>
<pre class="crayon-plain-tag">os.environ["HUGGINGFACEHUB_API_TOKEN"] = HUGGINGFACEHUB_API_TOKEN</pre>
<p>使用模型：</p>
<pre class="crayon-plain-tag">repo_id = "google/flan-t5-xxl
llm = HuggingFaceHub(repo_id=repo_id, model_kwargs={"temperature": 0.5, "max_length": 64})
llm_chain = LLMChain(prompt=prompt, llm=llm)</pre>
<p>Hugging Face上的模型，也可以被下载到你本地来运行。你需要先安装transformers包：</p>
<pre class="crayon-plain-tag">pip install transformers</pre>
<p>然后使用HuggingFacePipeline类来加载模型：</p>
<pre class="crayon-plain-tag">from langchain import HuggingFacePipeline

llm = HuggingFacePipeline.from_model_id(
    model_id="bigscience/bloom-1b7",
    task="text-generation",
    model_kwargs={"temperature": 0, "max_length": 64},
)</pre>
<p>注意，取决于你使用的模型的规模，下载可能消耗很长时间。当然，你本地机器也得有模型所需的硬件资源和软件。</p>
<p><a href="https://github.com/huggingface/text-generation-inference">Text Generation Inference</a>是一个开源服务软件，利用它，你可以在本地运行HuggingFace模型，并且暴露HTTP端点。LangChain可以连接到这种端点：</p>
<pre class="crayon-plain-tag">llm = HuggingFaceTextGenInference(
    inference_server_url="http://localhost:8010/",
    max_new_tokens=512,
    top_k=10,
    top_p=0.95,
    typical_p=0.95,
    temperature=0.01,
    repetition_penalty=1.03,
)
llm("What did foo say about bar?")</pre>
<div class="blog_h2"><span class="graybg">对话模型 </span></div>
<div class="blog_h3"><span class="graybg">缓存</span></div>
<p>用法和LLM模型一样：</p>
<pre class="crayon-plain-tag">import langchain
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI()

from langchain.cache import InMemoryCache
langchain.llm_cache = InMemoryCache()</pre>
<div class="blog_h3"><span class="graybg">HumanInputChatModel</span></div>
<p>用法和LLM版本的HumanInput模型类似：</p>
<pre class="crayon-plain-tag">from langchain.chat_models.human import HumanInputChatModel
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType

tools = load_tools(["wikipedia"])
llm = HumanInputChatModel()
agent = initialize_agent(
    tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)</pre>
<div class="blog_h3"><span class="graybg">生成流式响应</span></div>
<pre class="crayon-plain-tag">from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    HumanMessage,
)


from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
chat = ChatOpenAI(streaming=True, callbacks=[StreamingStdOutCallbackHandler()], temperature=0)
resp = chat([HumanMessage(content="Write me a song about sparkling water.")])</pre>
<div class="blog_h2"><span class="graybg">输出解析</span></div>
<p>默认情况下，语言模型输出自然语言文本。有时候你希望获得结构化的数据，便于在后续业务流程中继续处理。</p>
<p>LangChain提供了一系列的输出解析器（OutputParser），它们实现两类关键方法：</p>
<ol>
<li>获取格式指令：返回一个字符串，包含给语言模型的指令，用于指示语言模型去格式化自己的输出</li>
<li>解析：接受一段文本（来自语言模型的输出），解析为目标格式</li>
</ol>
<p>以及一个可选方法：</p>
<ol>
<li>使用提示词来解析：同时提供一个字符串（相当于模型响应）+ 提示词（相当于生成前述响应的提示词）。这个方法在输出解析器需要通过某种方式重试、修复输出的时候调用</li>
</ol>
<div class="blog_h3"><span class="graybg">JSON解析器</span></div>
<p>这个输出解析器，允许用户提供任意的JSON Schema，同时要求LLM的输出遵从该Schema。注意：能否输出格式正确的JSON，取决于模型，在OpenAI模型家族中DaVinci能可靠的生成JSON而Curie则不靠谱。</p>
<p>你需要定义一个Pydantic模型，来声明Schema：</p>
<pre class="crayon-plain-tag">from pydantic import BaseModel, Field, validator

# Define your desired data structure.
class Joke(BaseModel):
    setup: str = Field(description="question to set up a joke")
    punchline: str = Field(description="answer to resolve the joke")

    # You can add custom validation logic easily with Pydantic.
    @validator("setup")
    def question_ends_with_question_mark(cls, field):
        if field[-1] != "?":
            raise ValueError("Badly formed question!")
        return field</pre>
<p>使用输出解析器，来生成提示词的格式指令部分：</p>
<pre class="crayon-plain-tag">parser = PydanticOutputParser(pydantic_object=Joke)
instructions = parser.get_format_instructions()</pre>
<p>格式指令用自然语言提示LLM，该如何生成符合Schema的响应：</p>
<p style="padding-left: 30px;">The output should be formatted as a JSON instance that conforms to the JSON schema below.</p>
<p style="padding-left: 30px;">As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}}<br />the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.</p>
<p style="padding-left: 30px;">Here is the output schema:<br />```<br />{"properties": {"setup": {"title": "Setup", "description": "question to set up a joke", "type": "string"}, "punchline": {"title": "Punchline", "description": "answer to resolve the joke", "type": "string"}}, "required": ["setup", "punchline"]}<br />```</p>
<p> 使用上述格式指令，构建一个提示词模板：</p>
<pre class="crayon-plain-tag">prompt = PromptTemplate(
    template="Answer the user query.\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": instructions},
)

_input = prompt.format_prompt(query="Tell me a joke.")</pre>
<p> 完整的提示词：</p>
<p style="padding-left: 30px;">Answer the user query.<br />The output should be formatted as a JSON instance that conforms to the JSON schema below.</p>
<p style="padding-left: 30px;">As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}}<br />the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.</p>
<p style="padding-left: 30px;">Here is the output schema:<br />```<br />{"properties": {"setup": {"title": "Setup", "description": "question to set up a joke", "type": "string"}, "punchline": {"title": "Punchline", "description": "answer to resolve the joke", "type": "string"}}, "required": ["setup", "punchline"]}<br />```<br />Tell me a joke.</p>
<p> 生成响应：</p>
<pre class="crayon-plain-tag">output = model(_input.to_string())

joke: Joke = parser.parse(output)
print(joke.punchline)</pre>
<p>output即原始响应，为JSON形式：</p>
<p style="padding-left: 30px;">{"setup": "Why did the chicken cross the road?", "punchline": "To get to the other side!"}'</p>
<p>parse方法将其反序列化为Joke对象。</p>
<div class="blog_h3"><span class="graybg">自动修复解析器</span></div>
<p>这个解析器包装另外一个解析器，当后者解析失败，自动修复解析器会调用另外一个LLM来修复失败。该解析器会将格式错误的原始应答 + 格式指令一同发送给新的LLM：</p>
<pre class="crayon-plain-tag">class Actor(BaseModel):
    name: str = Field(description="name of an actor")
    film_names: List[str] = Field(description="list of names of films they starred in")
        
actor_query = "Generate the filmography for a random actor."

parser = PydanticOutputParser(pydantic_object=Actor)


# This is a mock of misformatted output, JSON properties must be double quoted.
misformatted = "{'name': 'Tom Hanks', 'film_names': ['Forrest Gump']}"


# parsing will fail with a JSONDecodeError
parser.parse(misformatted)


from langchain.output_parsers import OutputFixingParser
# here we wrap the original parser with OutputFixingParser, which will attempt to fix the output
new_parser = OutputFixingParser.from_llm(parser=parser, llm=ChatOpenAI())
new_parser.parse(misformatted)</pre>
<div class="blog_h3"><span class="graybg">重试解析器</span></div>
<p>某些情况下，模型给出的输出是不完整，而不是格式错误的。这种情况下可以尝试RetryOutputParser，它会将提示词 + 原始输出一起传递给LLM以尝试获得更好的应答：</p>
<pre class="crayon-plain-tag">class Action(BaseModel):
    action: str = Field(description="action to take")
    action_input: str = Field(description="input to the action")


parser = PydanticOutputParser(pydantic_object=Action)

template = """Based on the user question, provide an Action and Action Input for what step should be taken.
{format_instructions}
Question: {query}
Response:"""

prompt = PromptTemplate(
    template="Answer the user query.\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

# this is an incomplete output
bad_response = '{"action": "search"}'


from langchain.output_parsers import RetryWithErrorOutputParser
retry_parser = RetryWithErrorOutputParser.from_llm(
    parser=parser, llm=OpenAI(temperature=0)
)
retry_parser.parse_with_prompt(bad_response, prompt_value)
# Action(action='search', action_input='who is leo di caprios gf?'</pre>
<div class="blog_h3"><span class="graybg">其它解析器</span></div>
<p>CommaSeparatedListOutputParser用于生成一个列表，条目以逗号分隔：</p>
<pre class="crayon-plain-tag">output_parser = CommaSeparatedListOutputParser()

format_instructions = output_parser.get_format_instructions()
prompt = PromptTemplate(
    template="List five {subject}.\n{format_instructions}",
    input_variables=["subject"],
    partial_variables={"format_instructions": format_instructions}
)

_input = prompt.format(subject="ice cream flavors")</pre>
<p>DatetimeOutputParser用于将输出解析为日期/时间：</p>
<pre class="crayon-plain-tag">output_parser = DatetimeOutputParser()
template = """Answer the users question:

{question}

{format_instructions}"""
prompt = PromptTemplate.from_template(
    template,
    partial_variables={"format_instructions": output_parser.get_format_instructions()},
)

output = chain.run("around when was bitcoin founded?")
output_parser.parse(output)</pre>
<div class="blog_h1"><span class="graybg">数据连接</span></div>
<p>LLM一次完整的训练需要高昂的成本，这导致模型不能学到实时的知识，另外，一些私有领域的知识，也不能出现在训练集中。LLM具有In-Context学习的能力，允许用户在输入中注入知识，然后由LLM组织这些知识，生成符合用户预期的应答。</p>
<p>很多基于LLM的应用，都需要利用这种In-Context学习的能力，典型的是基于知识库的问答机器人。将所有知识，都以提示词的形式注入给LLM是不现实的，目前的模型都有Token数的限制。并且Token越多，成本越高，模型性能也越差。</p>
<p>LangChain的数据连接（Data Connection）给出了解决方案，整体上来说，需要在本地离线的对知识库进行处理、索引。在使用LLM时，在本地检索出和用户问题相关的知识片段，然后将这些知识片段作为提示词的一部分发送。</p>
<p>和数据连接有关的模块包括：</p>
<ol>
<li>文档加载器（Document loaders）：从各种源加载文档</li>
<li>文档转换器（Document transformers）：分割文档、剔除重复文档内容</li>
<li>文本嵌入模型（Text embedding models）：将非结构化的文本转换为Embeddings，即高维空间的向量（形式上为浮点数的列表）</li>
<li>向量存储库（Vector stores）：用于对上述Embeddings进行存储、检索</li>
<li>检索器（Retrievers）：用于检索数据，向量存储库可以作为检索器的一种后端</li>
</ol>
<div class="blog_h2"><span class="graybg">文档加载器</span></div>
<p>文档加载器负责将特定来源的数据加载为Document对象。Document是关联了元数据的文本。</p>
<div class="blog_h3"><span class="graybg">加载CSV</span></div>
<pre class="crayon-plain-tag">from langchain.document_loaders.csv_loader import CSVLoader

loader = CSVLoader(file_path='./example_data/mlb_teams_2012.csv')

loader = CSVLoader(file_path='./example_data/mlb_teams_2012.csv', csv_args={
    'delimiter': ',',
    'quotechar': '"',
    'fieldnames': ['MLB Team', 'Payroll in millions', 'Wins']
})

data = loader.load()</pre>
<div class="blog_h3"><span class="graybg">加载HTML</span></div>
<pre class="crayon-plain-tag">from langchain.document_loaders import UnstructuredHTMLLoader

loader = UnstructuredHTMLLoader("example_data/fake-content.html")
data = loader.load()</pre>
<p>可以使用BeautifulSoup4加载器，自动解析HTML中的文本到page_content，将页面的标题解析到metadata.title：</p>
<pre class="crayon-plain-tag">from langchain.document_loaders import BSHTMLLoader

loader = BSHTMLLoader("example_data/fake-content.html")
data = loader.load()
data</pre>
<div class="blog_h3"><span class="graybg">加载JSON</span></div>
<pre class="crayon-plain-tag">from langchain.document_loaders import JSONLoader

loader = JSONLoader(
    file_path='./example_data/facebook_chat.json',
    # 使用jq语法指定从JSON的什么字段抽取Document.page_content
    jq_schema='.messages[].content')

data = loader.load()</pre>
<p>如果源文档是JSON Lines格式（每行均为一个JSON），则需要指定 json_lines 为True：</p>
<pre class="crayon-plain-tag">loader = JSONLoader(
    file_path='./example_data/facebook_chat_messages.jsonl',
    jq_schema='.content',
    json_lines=True)

data = loader.load()</pre>
<p>除了page_content字段，还可以抽取任意元数据：</p>
<pre class="crayon-plain-tag">def metadata_func(record: dict, metadata: dict) -&gt; dict:

    metadata["sender_name"] = record.get("sender_name")
    metadata["timestamp_ms"] = record.get("timestamp_ms")

    return metadata


loader = JSONLoader(
    # {
    #   'messages': [
    #     {'content': 'Bye!', 'sender_name': 'User 2', 'timestamp_ms': 1675597571851}
    #   ]
    # }
    file_path='./example_data/facebook_chat.json',
    jq_schema='.messages[]',
    content_key="content",
    metadata_func=metadata_func
)

data = loader.load()</pre>
<div class="blog_h3"><span class="graybg">其它</span></div>
<p>LangChains支持从大量在线服务读取数据，具体参考<a href="https://python.langchain.com/docs/modules/data_connection/document_loaders">官方文档</a>。</p>
<div class="blog_h2"><span class="graybg">文档转换器</span></div>
<p>加载完文档后，通常需要对其进行转换以适合应用程序需要。其中最常见的例子是，将文档切割为较小的块（chunk）以适合模型的上下文大小窗口。</p>
<div class="blog_h3"><span class="graybg">如何分割文本</span></div>
<p>将长文档分割为小块时，需要处理很多潜在的复杂性。理想情况下，语义相关的片段，应该被分割在同一个块中。大体上来说，文本分割的工作方式如下：</p>
<ol>
<li>将文本分割为语义相关的小块，通常以句子为边界</li>
<li>将这些小块合并为更大的块，直到到达特定的大小</li>
<li>基于上面的块，外加前后的一些重叠，构建最终的块。这个操作的目的是保留块之间的上下文</li>
</ol>
<div class="blog_h3"><span class="graybg">RecursiveCharacterTextSplitter</span></div>
<p>默认推荐的文本分割器是RecursiveCharacterTextSplitter。该分割器接受一个分隔符列表（默认<pre class="crayon-plain-tag">["\n\n", "\n", " ", ""]</pre>），在确保块不过大的情况下，优先使用列表中前面的分隔符来分割块。该分割器提供了一些辅助的配置项：</p>
<ol>
<li><pre class="crayon-plain-tag">length_function</pre>：控制如何计算chunk的长度，默认是计算字符的个数，实际更常用的是计算Token的个数</li>
<li><pre class="crayon-plain-tag">chunk_size</pre>：允许的chunk的最大长度</li>
<li><pre class="crayon-plain-tag">chunk_overlap</pre>：两个相邻的chunk重叠部分的最大长度</li>
<li><pre class="crayon-plain-tag">add_start_index</pre>：是否包含chunk所在原始文档中的起始位置</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">with open('../../state_of_the_union.txt') as f:
    state_of_the_union = f.read()

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    # Set a really small chunk size, just to show.
    chunk_size = 100,
    chunk_overlap  = 20,
    length_function = len,
    add_start_index = True,
)

texts = text_splitter.create_documents([state_of_the_union])
print(texts[0])</pre>
<div class="blog_h3"><span class="graybg">分割代码</span></div>
<pre class="crayon-plain-tag">from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    Language,
)

# 获取特定语言的分隔符列表
python_splitter = RecursiveCharacterTextSplitter.get_separators_for_language(Language.PYTHON)
python_docs = python_splitter.create_documents([PYTHON_CODE])</pre>
<div class="blog_h3"><span class="graybg">MarkdownHeaderTextSplitter</span></div>
<p>嵌入处理过程需要同时考虑整体上下文、文本中句子和短语的相互关系，以保证生成质量更高的向量表示，精确的捕获文档的主题和含义。这要求具有相同上下文的文本，应该被分割在相同的块中。</p>
<p>类似于Markdown这样的格式，本身就通过标题级别划分了上下文，因此我们可以考虑基于这些标题级别进行分割，LangChains提供了MarkdownHeaderTextSplitter来处理Markdown：</p>
<pre class="crayon-plain-tag">markdown_document = "# Foo\n\n    ## Bar\n\nHi this is Jim\n\nHi this is Joe\n\n ### Boo \n\n Hi this is Lance \n\n ## Baz\n\n Hi this is Molly"

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
md_header_splits = markdown_splitter.split_text(markdown_document)
#  [Document(page_content='Hi this is Jim \nHi this is Joe', metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}),
#    Document(page_content='Hi this is Lance', metadata={'Header 1': 'Foo', 'Header 2': 'Bar', 'Header 3': 'Boo'}),
#    Document(page_content='Hi this is Molly', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'})]</pre>
<p>在上述基于标题级别分割的基础上，可以进一步分割：</p>
<pre class="crayon-plain-tag">from langchain.text_splitter import RecursiveCharacterTextSplitter
chunk_size = 250
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)

splits = text_splitter.split_documents(md_header_splits)</pre>
<div class="blog_h3"><span class="graybg">基于Token分割</span></div>
<p>LLM通常有Token数限制，我们分割的chunk不应该超过这个限制。 分词器（tokenizer）种类很多，计算chunk中有多少token时，应当使用和LLM一致的分词器。</p>
<p>OpenAI提供了基于BPE的分词器tiktoken，下面是其用法示例：</p>
<pre class="crayon-plain-tag">from langchain.text_splitter import CharacterTextSplitter

text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=100, chunk_overlap=0
)
texts = text_splitter.split_text(state_of_the_union)


# 或者

from langchain.text_splitter import TokenTextSplitter

text_splitter = TokenTextSplitter(chunk_size=10, chunk_overlap=0)

texts = text_splitter.split_text(state_of_the_union)</pre>
<p>spaCy是一个用于自然语言处理的库，LangChain也支持基于spaCy的分词器：</p>
<pre class="crayon-plain-tag"># pip install spacy

from langchain.text_splitter import SpacyTextSplitter

text_splitter = SpacyTextSplitter(chunk_size=1000)</pre>
<p>Hugging Face也提供了很多分词器，示例：</p>
<pre class="crayon-plain-tag">from transformers import GPT2TokenizerFast

tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")
text_splitter = CharacterTextSplitter.from_huggingface_tokenizer(
    tokenizer, chunk_size=100, chunk_overlap=0
)</pre>
<div class="blog_h2"><span class="graybg">Embedding</span></div>
<p>LangChain提供了和各种Embedding模型进行交互的标准化接口。上文我们提到过，Embedding是将文本转换为向量表示的过程，这样就可以将其存储在向量空间，并进行两个文本的相似度检索。</p>
<p>标准化接口提供了两个方法，其中之一用于嵌入文档：</p>
<pre class="crayon-plain-tag">from langchain.embeddings import OpenAIEmbeddings
embeddings_model = OpenAIEmbeddings(openai_api_key="...")

embeddings = embeddings_model.embed_documents(
    [
        "Hi there!",
        "Oh, hello!",
        "What's your name?",
        "My friends call me World",
        "Hello World!"
    ]
)</pre>
<p>另外一个用于嵌入查询：</p>
<pre class="crayon-plain-tag">embedded_query = embeddings_model.embed_query("What was the name mentioned in the conversation?")</pre>
<p>除了针对OpenAI的<a href="https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/openai">OpenAIEmbeddings</a>类，还有针对各种其它模型的封装，具体参考官方文档。</p>
<div class="blog_h2"><span class="graybg">向量存储</span></div>
<p>有了针对文档、查询的Embeddings，就可以进行相似度计算了 —— 找出和查询相似度最高的文档。向量存储提供了这种查询能力，同时它也负责存储文档的Embeddings。</p>
<p>向量存储的实现有很多，例如FAISS（Facebook AI Similarity Search)库，下面是初始化基于FAISS的向量存储的方法：</p>
<pre class="crayon-plain-tag"># pip install faiss-cpu

from langchain.document_loaders import TextLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS


raw_documents = TextLoader('...').load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
documents = text_splitter.split_documents(raw_documents)

embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(documents, embeddings)</pre>
<p>下面是执行相似度搜索的方法：</p>
<pre class="crayon-plain-tag"># 搜索文档
query = "What did the president say about Ketanji Brown Jackson"
docs = db.similarity_search(query)
print(docs[0].page_content)

# 基于Embedding搜索
embedding_vector = embeddings.embed_query(query)
docs = db.similarity_search_by_vector(embedding_vector)</pre>
<p>除了<a href="https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/faiss">FAISS</a>，LangChain提供了多种向量存储的封装，具体参考官方文档。</p>
<div class="blog_h2"><span class="graybg"> 检索器</span></div>
<p>检索器可以根据给定的非结构化查询，返回匹配的文档。检索器可以将向量存储作为后端，但是并非必须。检索器的基类很简单：</p>
<pre class="crayon-plain-tag">from abc import ABC, abstractmethod
from typing import Any, List
from langchain.schema import Document
from langchain.callbacks.manager import Callbacks

class BaseRetriever(ABC):
    ...
    def get_relevant_documents(
        self, query: str, *, callbacks: Callbacks = None, **kwargs: Any
    ) -&gt; List[Document]:
        """Retrieve documents relevant to a query.
        Args:
            query: string to find relevant documents for
            callbacks: Callback manager or list of callbacks
        Returns:
            List of relevant documents
        """
        ...

    async def aget_relevant_documents(
        self, query: str, *, callbacks: Callbacks = None, **kwargs: Any
    ) -&gt; List[Document]:
        """Asynchronously get documents relevant to a query.
        Args:
            query: string to find relevant documents for
            callbacks: Callback manager or list of callbacks
        Returns:
            List of relevant documents
        """
        ...</pre>
<p>也就是说检索器可以返回和查询相关的文档列表，相关性的定义取决于具体的检索器。</p>
<p>主要类型的检索器，还是基于向量存储的。LangChain默认情况下使用Chroma作为向量存储实现。下面的代码从文档加载器创建索引：</p>
<pre class="crayon-plain-tag">from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

from langchain.document_loaders import TextLoader
loader = TextLoader('...', encoding='utf8')

from langchain.indexes import VectorstoreIndexCreator
index = VectorstoreIndexCreator().from_loaders([loader])</pre>
<p>可以基于此索引来进行检索：</p>
<pre class="crayon-plain-tag">query = "What did the president say about Ketanji Brown Jackson"
index.query(query)</pre>
<p>下面的代码展示了如何从索引得到检索器接口：</p>
<pre class="crayon-plain-tag">index.vectorstore.as_retriever()</pre>
<p>VectorstoreIndexCreator内部，做了以下工作：</p>
<ol>
<li>将文档分割为块<br />
<pre class="crayon-plain-tag">documents = loader.load()

from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)</pre>
</li>
<li>为每个文档创建Embeddings、存储文档和Embeddings到向量存储<br />
<pre class="crayon-plain-tag">from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()

from langchain.vectorstores import Chroma
db = Chroma.from_documents(texts, embeddings)</pre>
</li>
<li>从向量存储得到检索器接口，并构造处理链：<br />
<pre class="crayon-plain-tag">retriever = db.as_retriever()
qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=retriever)</pre>
</li>
<li>基于索引的查询，其实就是调用处理链：<br />
<pre class="crayon-plain-tag">query = "What did the president say about Ketanji Brown Jackson"
qa.run(query)</pre>
</li>
</ol>
<p>你可以对VectorstoreIndexCreator进行定制化：</p>
<pre class="crayon-plain-tag">index_creator = VectorstoreIndexCreator(
    vectorstore_cls=Chroma,
    embedding=OpenAIEmbeddings(),
    text_splitter=CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
)</pre>
<div class="blog_h3"><span class="graybg">MultiQueryRetriever</span></div>
<p>如果Embedding没有很好的捕获数据的语义，那么查询文本中的细微变化，可能导致相似度检索结果的很大不同。提示词工程和微调有时候用来解决这种检索不准确的问题，但是处理起来可能比较繁琐。</p>
<p>MultiQueryRetriever可以自动化这个微调过程，它会调用LLM，从不同视角，从用户输入生成多个查询。然后，基于这些查询，分别获取相关文档，取并集作为最终结果。通过这种处理，某些情况下能够克服向量存储的那种基于距离的相似度判断的缺点，获取更加丰富的检索结果。</p>
<pre class="crayon-plain-tag"># Build a sample vectorDB
from langchain.vectorstores import Chroma
from langchain.document_loaders import WebBaseLoader
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Load blog post
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
data = loader.load()
    
# Split
text_splitter = RecursiveCharacterTextSplitter(chunk_size = 500, chunk_overlap = 0)
splits = text_splitter.split_documents(data)

# VectorDB
embedding = OpenAIEmbeddings()
vectordb = Chroma.from_documents(documents=splits,embedding=embedding)


from langchain.chat_models import ChatOpenAI
from langchain.retrievers.multi_query import MultiQueryRetriever
question="What are the approaches to Task Decomposition?"
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(retriever=vectordb.as_retriever(),llm=llm)
# Set logging for the queries
import logging
logging.basicConfig()
logging.getLogger('langchain.retrievers.multi_query').setLevel(logging.INFO)
unique_docs = retriever_from_llm.get_relevant_documents(query=question)
len(unique_docs)</pre>
<div class="blog_h3"><span class="graybg">上下文压缩 </span></div>
<p>你不知道用户会提供什么样的查询，真正和查询意图最相关的信息，可能会淹没在大量无关的文本中。将无关的文本返回给程序，进而对LLM发起调用，可能导致更高的成本或者生成较差的应答。</p>
<p>上下文压缩用于解决这个问题，它的思路很简单：不是把检索得到的文档直接返回，而是基于查询给出的上下文信息，进行压缩（去掉无关信息）。</p>
<p>下面的代码：</p>
<pre class="crayon-plain-tag">def pretty_print_docs(docs):
    print(f"\n{'-' * 100}\n".join([f"Document {i + 1}:\n\n" + d.page_content for i, d in enumerate(docs)]))


if __name__ == "__main__":
    from langchain.text_splitter import CharacterTextSplitter
    from langchain.embeddings import OpenAIEmbeddings
    from langchain.document_loaders import UnstructuredURLLoader
    from langchain.vectorstores import FAISS

    url = 'https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt'
    documents = UnstructuredURLLoader(urls=[url]).load()
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
    texts = text_splitter.split_documents(documents)
    retriever = FAISS.from_documents(texts, OpenAIEmbeddings()).as_retriever()

    docs = retriever.get_relevant_documents("What did the president say about Ketanji Brown Jackson")
    pretty_print_docs(docs)

    from langchain.llms import OpenAI
    from langchain.retrievers import ContextualCompressionRetriever
    from langchain.retrievers.document_compressors import LLMChainExtractor</pre>
<p>检索到的第一个结果是：</p>
<p style="padding-left: 30px;">Document 1:</p>
<p style="padding-left: 30px;">In state after state, new laws have been passed, not only to suppress the vote, but to subvert entire elections.</p>
<p style="padding-left: 30px;">We cannot let this happen.</p>
<p style="padding-left: 30px;">Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections.</p>
<p style="padding-left: 30px;">Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service.</p>
<p style="padding-left: 30px;">One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court.</p>
<p style="padding-left: 30px;">And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.</p>
<p>现在进行上下文压缩：</p>
<pre class="crayon-plain-tag">from langchain.llms import OpenAI
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(base_compressor=compressor, base_retriever=retriever)

compressed_docs = compression_retriever.get_relevant_documents("What did the president say about Ketanji Jackson Brown")
pretty_print_docs(compressed_docs)</pre>
<p> 结果长度变短：</p>
<p style="padding-left: 30px;">Document 1:</p>
<p style="padding-left: 30px;">"Tonight, I'd like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service.</p>
<p style="padding-left: 30px;">One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court.</p>
<p style="padding-left: 30px;">And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation's top legal minds, who will continue Justice Breyer's legacy of excellence."</p>
<p>上面代码中的LLMChainExtractor，仅仅负责检查检索器返回的原始结果集，决定哪些应该丢弃。它不会修改结果的内容。</p>
<p>针对每个结果，都调用一次LLM，成本较高且速度慢。EmbeddingsFilter提供一个更廉价和迅速的方案，它对文档和查询进行嵌入并仅仅返回和查询相似度足够高的那些文档：</p>
<pre class="crayon-plain-tag">from langchain.embeddings import OpenAIEmbeddings
from langchain.retrievers.document_compressors import EmbeddingsFilter

embeddings = OpenAIEmbeddings()
embeddings_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.76)
compression_retriever = ContextualCompressionRetriever(base_compressor=embeddings_filter, base_retriever=retriever)

compressed_docs = compression_retriever.get_relevant_documents("What did the president say about Ketanji Jackson Brown")</pre>
<div class="blog_h3"><span class="graybg">联用压缩和文档转换</span></div>
<p>利用DocumentCompressorPipeline类，可以构造一个流水线，编排多个压缩器以及BaseDocumentTransformers。后者不进行上下文压缩，仅仅负责简单的转换，例如可以用TextSplitters将文档分割为更小的片段，利用EmbeddingsRedundantFilter基于Embedding相似度剔除冗余的文档：</p>
<pre class="crayon-plain-tag">from langchain.document_transformers import EmbeddingsRedundantFilter
from langchain.retrievers.document_compressors import DocumentCompressorPipeline
from langchain.text_splitter import CharacterTextSplitter

# 首先分割为更小的块
splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=0, separator=". ")
# 然后剔除重复文档（块）
redundant_filter = EmbeddingsRedundantFilter(embeddings=embeddings)
# 最后根据相似度进行过滤
relevant_filter = EmbeddingsFilter(embeddings=embeddings, similarity_threshold=0.76)
pipeline_compressor = DocumentCompressorPipeline(
    transformers=[splitter, redundant_filter, relevant_filter]
)

compression_retriever = ContextualCompressionRetriever(base_compressor=pipeline_compressor, base_retriever=retriever)

compressed_docs = compression_retriever.get_relevant_documents("What did the president say about Ketanji Jackson Brown")</pre>
<div class="blog_h3"><span class="graybg">自查询</span></div>
<p>文档不但具有内容，还可以具有任意附加的元数据。元数据是一个字典，上文提到的文档加载器，可以在加载文档时抽取元数据。</p>
<p>所谓自查询的（Self-querying）检索器，能够对自身（通常是背后的向量存储）进行查询。具体来说，它能够从用户的查询输入中，抽取针对已存储的文档的元数据的过滤器，并应用这些过滤器。</p>
<p>打个比方，对于一个查询：What did bar say about foo。而文档具有元数据author，那么自查询检索器就能去匹配auther为bar的、内容为foo is a charming chap的文档。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">if __name__ == "__main__":
    # pip install lark chromadb
    from langchain.schema import Document
    from langchain.embeddings.openai import OpenAIEmbeddings
    from langchain.vectorstores import Chroma

    embeddings = OpenAIEmbeddings()
    docs = [
        Document(
            page_content="A bunch of scientists bring back dinosaurs and mayhem breaks loose",
            metadata={"year": 1993, "rating": 7.7, "genre": "science fiction"},
        ),
        Document(
            page_content="Leo DiCaprio gets lost in a dream within a dream within a dream within a ...",
            metadata={"year": 2010, "director": "Christopher Nolan", "rating": 8.2},
        ),
        Document(
            page_content="A psychologist / detective gets lost in a series of dreams within dreams within dreams and Inception reused the idea",
            metadata={"year": 2006, "director": "Satoshi Kon", "rating": 8.6},
        ),
        Document(
            page_content="A bunch of normal-sized women are supremely wholesome and some men pine after them",
            metadata={"year": 2019, "director": "Greta Gerwig", "rating": 8.3},
        ),
        Document(
            page_content="Toys come alive and have a blast doing so",
            metadata={"year": 1995, "genre": "animated"},
        ),
        Document(
            page_content="Three men walk into the Zone, three men walk out of the Zone",
            metadata={
                "year": 1979,
                "rating": 9.9,
                "director": "Andrei Tarkovsky",
                "genre": "science fiction",
                "rating": 9.9,
            },
        ),
    ]
    vectorstore = Chroma.from_documents(docs, embeddings)

    # instantiate the self-query retriever
    from langchain.llms import OpenAI
    from langchain.retrievers.self_query.base import SelfQueryRetriever
    from langchain.chains.query_constructor.base import AttributeInfo

    metadata_field_info = [
        AttributeInfo(
            name="genre",
            description="The genre of the movie",
            type="string or list[string]",
        ),
        AttributeInfo(
            name="year",
            description="The year the movie was released",
            type="integer",
        ),
        AttributeInfo(
            name="director",
            description="The name of the movie director",
            type="string",
        ),
        AttributeInfo(
            name="rating", description="A 1-10 rating for the movie", type="float"
        ),
    ]
    document_content_description = "Brief summary of a movie"
    llm = OpenAI(temperature=0)
    retriever = SelfQueryRetriever.from_llm(
        llm, vectorstore, document_content_description, metadata_field_info, verbose=True
    )

    # only specifies a relevant query
    docs = retriever.get_relevant_documents("What are some movies about dreams?")

    # only specifies a filter
    docs = retriever.get_relevant_documents("I want to watch a movie rated higher than 8.5")
    # A psychologist / detective gets lost in a series of dreams within dreams within dreams and Inception reused the idea

    # specifies both a query and a filter
    docs = retriever.get_relevant_documents("Has Greta Gerwig directed any movies about women")
    for doc in docs:
        print(doc.page_content)</pre>
<p>自查询检索器会自动分析用户输入和元数据的关联关系，并生成过滤器：</p>
<p style="padding-left: 30px;">query='dreams' filter=None limit=None<br />query=' ' filter=Comparison(comparator=&lt;Comparator.GT: 'gt'&gt;, attribute='rating', value=8.5) limit=None<br />query='women' filter=Comparison(comparator=&lt;Comparator.EQ: 'eq'&gt;, attribute='director', value='Greta Gerwig') limit=None</p>
<p>自查询检索器仅仅能应对比较简单的查询。比如：I'm looking for Greta Gerwig directed any movies about women, or anyone directed movies about dreams，就无法生成恰当的查询和过滤器。</p>
<div class="blog_h3"><span class="graybg">时间加权检索</span></div>
<p>这种检索器，在相似度检索的基础上，让最近被检索器命中过的文档，获得优先级。算法如下：</p>
<p style="padding-left: 30px;">semantic_similarity + (1.0 - decay_rate) ^ hours_passed</p>
<p>decay_rate为衰退率，控制时间因子的影响大小。hours_passed表示上一次该文档被检索器访问以来已经过去的时间。衰退率的范围为0-1，可以看到，过低或者过高的衰退率，都会快速、很大程度上削弱时间的影响，从而等价于简单的相似度检索。</p>
<pre class="crayon-plain-tag">import faiss

from datetime import datetime, timedelta
from langchain.docstore import InMemoryDocstore
from langchain.embeddings import OpenAIEmbeddings
from langchain.retrievers import TimeWeightedVectorStoreRetriever
from langchain.schema import Document
from langchain.vectorstores import FAISS

# Define your embedding model
embeddings_model = OpenAIEmbeddings()
# Initialize the vectorstore as empty
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(embeddings_model.embed_query, index, InMemoryDocstore({}), {})
retriever = TimeWeightedVectorStoreRetriever(vectorstore=vectorstore, decay_rate=.0000000000000000000000001, k=1)</pre>
<div class="blog_h3"><span class="graybg">基于向量存储 </span></div>
<p>从向量存储对象，都可以得到一个检索器：</p>
<pre class="crayon-plain-tag">retriever = db.as_retriever()</pre>
<p>使用这些检索器时，有一些通用的请求参数。</p>
<p>默认情况下，使用基于相似度的检索，可以改为基于最大边际相关性（MMR）的检索，如前文提到过的一样，MMR有利于去除重复的内容：</p>
<pre class="crayon-plain-tag">retriever = db.as_retriever(search_type="mmr")</pre>
<p>检索的时候，可以指定相似度分数的阈值：</p>
<pre class="crayon-plain-tag">retriever = db.as_retriever(search_type="similarity_score_threshold", search_kwargs={"score_threshold": .5})</pre>
<p>上述例子中，只有相似度评分高于0.5的文档被返回。</p>
<p>此外，你可以限定仅仅返回相似度最高的K个文档：</p>
<pre class="crayon-plain-tag">retriever = db.as_retriever(search_kwargs={"k": 1})</pre>
<div class="blog_h1"><span class="graybg">处理链 </span></div>
<p>对于简单的应用程序，仅仅使用一个语言模型（可能配合向量存储做信息检索）就足够了。更复杂的应用，可能需要联合使用多个语言模型或者调用任意的其它工具。</p>
<p>LangChain提供了Chain接口，用来编排对一系列工具的顺序化调用。这些工具可能包括LLM和其它Chain。这个接口非常简单：</p>
<pre class="crayon-plain-tag">class Chain(BaseModel, ABC):
    """Base interface that all chains should implement."""

    memory: BaseMemory
    callbacks: Callbacks

    def __call__(
        self,
        inputs: Any,
        return_only_outputs: bool = False,
        callbacks: Callbacks = None,
    ) -&gt; Dict[str, Any]:
        ...</pre>
<p>最简单的处理链的例子，我们在快速参考一章中介绍过，它将提示词模板和LLM调用链在一起。</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/langchain">利用LangChain和语言模型交互</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/langchain/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
