<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zekai.Felix</title><description>关于 Agent、多模态视频与模型后训练的学习、实践和思考。</description><link>https://gzhzk.github.io/</link><language>zh_CN</language><item><title>多想、多做、边试、边学</title><link>https://gzhzk.github.io/blogs/posts/thoughts-20260611/</link><guid isPermaLink="true">https://gzhzk.github.io/blogs/posts/thoughts-20260611/</guid><description>从 Agent Harness、Post-Training 和项目重构中学到的事：先跑起来不等于做对，AI 可以提方案，但判断的尺子必须在自己手里。</description><pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我已经有一段时间没写博客了，这段时间对我来说是比较忙、比较累，但也收获颇丰的三个月。写这篇博客，主要是记录我这段时间的所思所想和所做，谈谈自己的收获、教训和感悟。&lt;/p&gt;
&lt;p&gt;我是华工28届的一名本科生，现在处于大二下。这是一个很特殊的时间段，说早不早说晚也不晚，有很多事可以去做，也有很多选择留给自己去探索。&lt;/p&gt;
&lt;h2&gt;一、为什么选了算法&lt;/h2&gt;
&lt;p&gt;我并不是从一开始就确定走算法这条路的。&lt;/p&gt;
&lt;p&gt;大一有两门课考得很不理想（60 多 70 左右），那时候我意识到一个问题：我得尽早想清楚自己以后到底要做什么。开发是一个相对稳妥的选项，对本科生也友好，但我没有选。&lt;/p&gt;
&lt;p&gt;转折点发生在我大量使用 AI coding 工具的那段时间。我切身感受到，所谓功能应用层的代码对 AI 来说太轻松了（当然这是指大代码库的整体架构能力，现阶段 AI 还稍弱——也有可能是我没试过最好的模型，网上确实有人让 AI 重构代码库在极短时间内取得极好的效果）。一个方向如果 AI 能极快地完成大部分工作，那它对人的核心价值就所剩无几了。&lt;/p&gt;
&lt;p&gt;我不想把时间赌在一个 AI 越来越擅长的方向上。&lt;/p&gt;
&lt;p&gt;算法不一样。算法本质是数学与计算机的巧妙融合，很多原理根源于数学，也移植了不少计算机领域的宝贵思想。它对人的逻辑、抽象、创造、辩证这些能力要求更高，而这些恰恰是 AI 短期还难以替代的。&lt;/p&gt;
&lt;p&gt;就这样，我决定把接下来一段时间的重心放在大模型算法相关方向上。&lt;/p&gt;
&lt;h2&gt;二、这段时间我在做什么&lt;/h2&gt;
&lt;p&gt;这三个月我主要在三个方向上推进：agent harness、uni recommendation framework、post training pipeline。接下来具体讲讲。&lt;/p&gt;
&lt;h3&gt;Agent harness&lt;/h3&gt;
&lt;p&gt;我理解的 agent harness，就是为 agent 设计一个运行时系统——怎么编排上下文、怎么注入提示词和记忆、怎么管理工具的使用，是它的核心。往更全了说，还有规划能力的体现、子 agent 或集群的调度、skills/mcp 这些相对工程的模块。&lt;/p&gt;
&lt;p&gt;我一开始几乎什么都不懂。大部分思路源于我以前做小程序时积累的一些系统感，加上模仿主流 agent（Claude Code、OpenHands、DeerFlow 等）。我会看它们的主架构，结合真实应用场景揣摩每一部分设计的初衷和效果。&lt;/p&gt;
&lt;p&gt;不过说实话，刚开始那阵子，&lt;strong&gt;&quot;wow 这个模块看起来很高级，直接接上试试&quot;&lt;/strong&gt; 是我的真实状态。最典型的就是我直接模仿 DeerFlow 的中间件模式，搞了七八个中间件，拦截每轮对话看沙箱还在不在、记忆需不需要读写、上下文需不需要压缩——对于一个内核部分来说，这纯属又慢又复杂，华而不实。&lt;/p&gt;
&lt;p&gt;后来想清楚了：在 ReAct 里面加一些 try...except 不就完了吗？&lt;/p&gt;
&lt;h3&gt;从堆功能到砍功能&lt;/h3&gt;
&lt;p&gt;第一阶段踩完坑后，我进入&quot;形成层次化结构&quot;的阶段：什么是底层原子级能力、上下层之间的依赖关系、一次对话一轮任务需要经过哪些点。我开始把模块一个个搭起来，memory 那里我参考了 Karpathy 大神很火的 LLM Wiki，把记忆设计成简单分层（对话级、摘要级、扁平记忆、检索）。功能越来越完善，但跑起来又慢又复杂，轻中度任务给到的时候，处理得通但耗时长得离谱。&lt;/p&gt;
&lt;p&gt;这个时候我才回想起导师很早之前说过的一句话：&lt;strong&gt;要有一个 target 指引着你的整个设计流程。&lt;/strong&gt; 当时听了没感觉，觉得堆功能的过程中能用到更多能力，越全面越好。&lt;/p&gt;
&lt;p&gt;走到后面发现完了，成屎山了。&lt;/p&gt;
&lt;p&gt;那就要爆砍，思考哪些是必要、哪些是非必要。这个取舍的过程我觉得才是核心中的核心。我会经常让 AI 给出方案，看它的思路，但最后拍板的是我——AI 的检索能力和学识广度肯定要强我千倍万倍，但&lt;strong&gt;它不知道我现在这个项目最需要什么&lt;/strong&gt;。这个反复&quot;让 AI 提方案 → 自己判断取舍 → 砍掉冗余 → 再来一遍&quot;的过程，让我对整体架构设计有了更深的体感。&lt;/p&gt;
&lt;p&gt;重新和 AI 讨论核心运行时、必要的状态、必要的工具、主链路等等之后，整个系统清爽了很多。也渐渐积累了一些系统架构设计的经验——&lt;strong&gt;踩坑才出来的，重构的过程还是很痛苦的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;三、几个真实的教训&lt;/h2&gt;
&lt;p&gt;回过头看，这段时间我做对了什么、做错了什么，大概有这么几条。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，&quot;先跑起来&quot;和&quot;做对&quot;是两件事。&lt;/strong&gt; 我前期花了太多时间在模仿 DeerFlow 的中间件上，本质上是在用&quot;看起来高级&quot;代替&quot;真正需要&quot;。&lt;strong&gt;大道至简不是一句空话，是被屎山教育出来的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，AI 是好工具，但别让它替你做判断。&lt;/strong&gt; 我会让 AI 提方案，但&lt;strong&gt;判断的尺子必须在我自己手里&lt;/strong&gt;。AI 给的方案可能正确但不必要，可能技术上漂亮但工程上冗余。我现在养成一个习惯：AI 输出方案后，我会先问自己——&quot;这个方案是不是真的解决了我当前的问题？&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三，导师说的&quot;要有 target&quot; 真的是金玉良言。&lt;/strong&gt; 当时没听进去，栽了跟头才懂。target 不是空想出来的，是边做边明确、边砍边收敛的。下次再开新项目，我会先把 target 写下来贴在屏幕上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四，&quot;复刻式实战&quot;和&quot;0→1 实战&quot;是两回事。&lt;/strong&gt; 我这三个月严格说是站在 Claude Code、OpenHands、DeerFlow 这些巨人的肩膀上做的复刻式实战，过程中学到了很多，但离真正的 0→1 还有距离。这一点我会时刻提醒自己。&lt;/p&gt;
&lt;h2&gt;四、其他思考&lt;/h2&gt;
</content:encoded></item><item><title>Transformer 核心架构逐层拆解</title><link>https://gzhzk.github.io/blogs/posts/transformer/</link><guid isPermaLink="true">https://gzhzk.github.io/blogs/posts/transformer/</guid><description>从 Self-Attention 出发，逐层拆解 Transformer 的编码器、缩放点积注意力、多头注意力、掩码与位置编码，配合公式、架构图和 PyTorch 实现。</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Transformer 架构&lt;/strong&gt; 是2017年由 Vaswani 等八位 Google 大佬在论文《Attention is All You Need》中提出的&lt;strong&gt;基于自注意力机制（Self-Attention）的序列建模架构&lt;/strong&gt;，它完全抛弃了循环结构，转而完全依赖一种名为注意力（Attention）的机制来捕捉序列内的依赖关系，从而实现了真正意义上的&lt;strong&gt;并行计算&lt;/strong&gt;。它彻底改变了自然语言处理领域（Natural Language Processing, NLP）及其他序列任务的方式，对当今大语言模型（Large Language Model, LLM）的发展做出了跨时代的贡献。借此博客，分享一下我对 Transformer 架构的理解。希望对你有帮助！&lt;/p&gt;
&lt;h2&gt;Why Self-Attention?&lt;/h2&gt;
&lt;p&gt;当今大语言模型的一大核心任务是处理长文本，专业一点的说法是序列转档。在 Transformer 架构提出之前，传统的序列转档模型依托于&lt;strong&gt;循环神经网络（Recurrent Neural Network, RNN）&lt;/strong&gt;，它的原理并不复杂。例如，当你阅读这篇文章时，你对每个词的理解都建立在对前面词的理解之上，RNN 这种带有循环的网络，能够让信息得以保留（&lt;strong&gt;图1&lt;/strong&gt;）。从中可以直观看出 RNN 的核心计算思路是将模型在上一时间步产生的隐藏状态作为当前时间步输入的一部分，与当前的外部输入共同计算新的隐藏状态和输出。通过循环计算更新隐藏状态，再将这个状态传递给下一个时间步，如此逐步推进处理整个序列（由于具体原理篇幅较长，后续我会单独整理一篇关于 RNN 和 LSTM 模型的理解）。&lt;/p&gt;
&lt;p&gt;然而，这种逐步传递的方式存在显著缺陷：由于信息必须按时间顺序逐层传递，模型 &lt;strong&gt;难以捕捉长距离依赖&lt;/strong&gt;（距离越远的信息衰减越严重），且&lt;strong&gt;无法并行计算&lt;/strong&gt;（必须等上一步完成后才能进行下一步），导致训练效率低下。为了更直观地理解这个问题，不妨想象你正在做英语的阅读理解，通常有两种做题策略：一种是从头到尾逐字逐词阅读，必须读完前面才能慢慢理解后面的内容，如果文章很长，你可能会忘记开头的关键信息，导致做题时难以快速定位答案；而另一种则是先读问题，&lt;strong&gt;带着问题去扫描文章&lt;/strong&gt;，通过关键词快速找到最相关匹配度最高的段落或某一行，再提取其中的关键信息，这样无论文章多长，你都能直接跳到最高度相关的地方，既高效又能捕捉全局语义。&lt;/p&gt;
&lt;p&gt;或许正是受到这一想法的启发，Transformer 才引入了自注意力机制，让模型能够同时关注序列中的所有位置，直接建立任意两个位置之间的依赖关系，从而实现对长文本的高效并行建模。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blogs/images/1/RNN.png&quot; alt=&quot;RNN展开结构示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图1: RNN展开结构示意图&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;左侧为一个循环单元&quot;U&quot;，接收输入x&amp;lt;sub&amp;gt;t&amp;lt;/sub&amp;gt;和前一时刻隐藏状态h&amp;lt;sub&amp;gt;t-1&amp;lt;/sub&amp;gt;，输出当前隐藏状态h&amp;lt;sub&amp;gt;t&amp;lt;/sub&amp;gt;。右侧展示该单元沿时间轴的展开，揭示了RNN处理序列数据的方式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Transformer 整体架构：编码器（Encoder）+ 解码器（Decoder）&lt;/h2&gt;
&lt;p&gt;在上面我们已经了解到，自注意力机制能让序列模型突破 RNN 的“串行局限”，起到&lt;strong&gt;同时关注整个序列，动态捕捉长距离依赖&lt;/strong&gt;的关键作用，这将是 Transformer 架构的核心组成部分。但是，为了完成更复杂的任务，还需要考虑到方方面面的其他因素，其中有一点：计算机无法直接理解我们人类的语言，它最底层的结构决定了它只能理解数字、运算数字。那么，就需要通过某种方法将人类语言转换成机器可以理解的“机器语言”，而且还要保证“机器语言”带有我们想要表达的信息（即语义）。没错，在数学中的的确确就有这样一种既使用数字表示还能刻画特征的东西—— &lt;strong&gt;向量（Vector）&lt;/strong&gt;（具体原理涉及到自然语言处理中文本表征的相关知识，详见我的上一篇博客《NLP 文本表征：Word Embedding + Tokenizer + BPE 算法全解》）。&lt;/p&gt;
&lt;p&gt;在 &lt;strong&gt;Transformer&lt;/strong&gt; 的整体架构设计中（&lt;strong&gt;图2&lt;/strong&gt;），原作者设计了这两大模块：&lt;strong&gt;编码器（Encoder）+ 解码器（Decoder）&lt;/strong&gt;。从功能上看，编码器是深度理解，将源序列转化为一个&lt;strong&gt;上下文感知的向量序列&lt;/strong&gt;，并通过一系列复杂的向量运算在不断训练中逐渐提取出语义信息；而解码器是&lt;strong&gt;条件生成&lt;/strong&gt;，一边参考这些语义向量，一边逐步生成出目标序列。这种结构天然适配机器翻译、文本摘要等序列到序列任务，也正是当前大语言模型的核心基础。接下来，我们一个模块一个模块拆解，感受作者天才般的智慧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blogs/images/1/Transformer.png&quot; alt=&quot;Transformer 模型整体架构示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图2: Transformer 模型整体架构示意图&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;左侧为编码器（Encoder），右侧为解码器（Decoder），均为 N 层堆叠。输入侧以 Embedding 结合位置编码完成向量化；编码器通过自注意力与 FFN 提取上下文语义；解码器借助掩码自注意力 + 交互注意力 + FFN 生成目标序列；最后经线性层和 Softmax 输出预测概率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h2&gt;编码器结构：多头注意力、残差连接、层归一化、前馈网络&lt;/h2&gt;
&lt;h3&gt;核心创新点1：Scaled Dot-Product Attention&lt;/h3&gt;
&lt;p&gt;想象你正在做一篇英语阅读理解，有一个让做题变得又快又准的策略——带着问题找答案，这往往能帮助我们快速锁定文本中的关键信息，可能对着一篇上千词的文段，快速扫几眼就解决了，这其实就体现了注意力的分配。然而对于计算机，却难以快速理解。因此，我们需要一个机制，让模型能够基于输入动态地决定关注哪里。就像数据库查询：给定一个问题（Q），在所有记录的关键字（K）中查找匹配，然后返回对应的值（V）。下面的 &lt;strong&gt;图3&lt;/strong&gt; 呈现了注意力机制的精髓：&lt;strong&gt;缩放点积注意力（Scaled Dot-Product Attention）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blogs/images/1/QKV.png&quot; alt=&quot;缩放点积注意力（Scaled Dot-Product Attention）结构图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图3: 缩放点积注意力（Scaled Dot-Product Attention）结构示意图&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;这是 Transformer 模型中的核心注意力计算机制，其中左侧输入Q（Query）和K（Key），右侧输入V（Value）。箭头指示了从Q、K计算相似度分数，经缩放、可选的掩码、SoftMax归一化，最后与V加权求和的数据流向。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意力机制的核心思想是对齐（Alignment）&lt;/strong&gt;，即同一序列内(即自注意力或不同序列间(即交叉注意力)词语之间的关系。注意力机制本质上是在计算序列中每个位置与其他所有位置的相关性分数。这种对齐不是简单的单词匹配，而是语义和语法层面的深度关联。&lt;/p&gt;
&lt;p&gt;注意力机制的工作过程是这样的：&lt;/p&gt;
&lt;p&gt;（1）&lt;strong&gt;计算注意力权重&lt;/strong&gt;：首先明确一点，Q、K、V 三个矩阵都是利用原矩阵 X 做线性变换得到的，如 &lt;strong&gt;图4&lt;/strong&gt; 所示。得到这三个矩阵后，（先考虑单个 batch 的情况，形状是 seq_len × d_model，即每个矩阵的行表示序列中的每一个 token，列表示每个 token 对应的特征），为了使语义对齐，向量中的点积概念开始登场。其实很自然地，我们可以认为序列中的某一个 token(A) 的行向量q 去点乘其他所有 token(B, C, D, etc) 的列向量k 从而获得一个分数，这里分数的意思很明确，就是表示 token(A) 与其他所有 token 的相关性。比如按照 &lt;strong&gt;图5&lt;/strong&gt; 中的例子，用第三行的 went 的 q 向量（包含了 went 的所有查询语义特征，如&apos;我带有去某个地方的语义，需要关注地点名词&apos;、&apos;我往往表示人或物所做的动作，需要关注人或物&apos;）去与 The，group，home 的 k 向量的转置做向量点积运算。在计算点积时，went 的&apos;需要地点&apos;特征与 home 的&apos;是地点&apos;特征相乘得到高分，went 的&apos;需要主语&apos;特征与 group 的&apos;是主语&apos;特征相乘得到高分，这些高分的总和使得 went 与 home 和 group 的相关性都强。&lt;/p&gt;
&lt;p&gt;但是，这里有一个逻辑漏洞，点积将高维向量的所有维度信息压缩成一个标量，我们好像无法从这个单一得分中获知是哪些具体的语义特征（如“地点”或“主语”）主导了相关性判断。在前向传播时，点积确实是一个“黑箱”操作，输出一个代表整体相关性的分数。模型此刻并不关心里面0.73是0.72（地点维度贡献）+0.01，还是别的什么组合。怎么办呢？真正的魔法其实发生在反向传播。它按照梯度方向更新可学习的参数矩阵 W_q 和 W_k。在无数次的迭代中，一种高效的优化路径会逐渐浮现：要想显著提高点积总分，最有效的方法是让 q_went 在地点相关维度上获得高权重，同时让 k_home 在地点维度上也获得高权重。因为当这两个向量在地点维度上同时取较大值时，它们的乘积会对最终总和产生决定性影响。于是，通过梯度下降的“压力”，W_q 和 W_k 被调整，尽管点积计算仍输出一个混合值0.73，但其内部构成已被学习过程塑造成由“地点”维度主导。这也就解释了为什么 图4 中我提到变换矩阵都是学习得到的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blogs/images/1/attention-encoder-self-attention-qkv-transformation.png&quot; alt=&quot;QKV由原矩阵X经过线性变换结构图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图4: Q/K/V由原矩阵X经过线性变换示意图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;图源：
&lt;a href=&quot;https://github.com/chrisvdweth/selene/blob/master/notebooks/images/illustrations/attention/attention-encoder-self-attention-qkv-transformation.png&quot;&gt;chrisvdweth/selene/attention-encoder-self-attention-qkv-transformation.png&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这是得到 Q、K、V 矩阵的关键步骤，对原矩阵 X 做线性变换，从而使原矩阵具备了不同的功能（查询、键、值），方便后续计算注意力的处理。注意，这里的变换矩阵 W_q、W_k、W_v 都是后续学习得到的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blogs/images/1/attention-encoder-self-attention-scores.png&quot; alt=&quot;Q与K的转置做点积运算图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图5: Q与K的转置做点积运算示意图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;图源：
&lt;a href=&quot;https://github.com/chrisvdweth/selene/blob/master/notebooks/images/illustrations/attention/attention-encoder-self-attention-scores.png&quot;&gt;chrisvdweth/selene/attention-encoder-self-attention-scores.png&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;$$
\text{Attention}(Q, K, V) = \text{softmax}\left( \frac{QK^T}{\sqrt{d_k}} \right) V
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn.functional as F

def scaled_dot_product_attention(Q, K, V, mask=None):
    &quot;&quot;&quot;
    缩放点积注意力
    参数含义：
        Q：查询张量，shape (batch_size, num_heads, seq_len_q, d_k)
        K：键张量，shape (batch_size, num_heads, seq_len_k, d_k)
        V：值张量，shape (batch_size, num_heads, seq_len_k, d_v)  一般情况下，d_v = d_k
        mask：掩码张量，shape 可与 scores 广播，布尔类型，True 的位置将被掩盖（设为 -inf）
    返回：
        output：注意力输出，shape (batch_size, num_heads, seq_len_q, d_v)
        attention_weight：注意力权重，shape (batch_size, num_heads, seq_len_q, seq_len_k)
    &quot;&quot;&quot;
    # 获取最后一个维度的大小，即每个注意力头的特征维度，缩放时使用
    d_k = Q.size(-1)
    # 计算 Q 与 K 的点积，得到的scores形状：(batch_size, num_heads, seq_len_q, seq_len_k)
    # 并缩放，缩放保持方差稳定，使训练更稳定，防止梯度消失
    scores = torch.matmul(Q, K.transpose(-1, -2)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
    print(scores.shape)
    # 应用掩码
    if mask is not None:
        # mask是布尔张量，True的位置需要被掩盖，掩码位置的值设为负无穷
        scores = scores.masked_fill(mask, float(&apos;-inf&apos;))    
    # 经过 softmax 得到注意力权重，dim=-1表示在最后一个维度（seq_len_k）上进行softmax
    attention_weight = F.softmax(scores, dim=-1)
    # 加权求和得到输出
    output = torch.matmul(attention_weight, V)

    return attention_weight, output

if __name__ == &quot;__main__&quot;:
    batch_size = 2
    num_heads = 1
    seq_len = 4
    d_k = 8
    d_v = d_k

    # 随机生成 Q, K, V (batch_size, num_heads, seq_len, d_k)
    torch.manual_seed(42)
    Q = torch.randn(batch_size, num_heads, seq_len, d_k)
    K = torch.randn(batch_size, num_heads, seq_len, d_k)
    V = torch.randn(batch_size, num_heads, seq_len, d_v)

    attention_weight, output = scaled_dot_product_attention(Q, K, V)
    print(attention_weight)
    print(output)

    &quot;&quot;&quot;
        填充掩码 Padding Mask
        构造 padding mask：有效位置为 False，填充位置为 True（需要掩盖）
        在 batch 处理中，不同序列往往长度不同，需要用 padding 来补齐到与最长句子相同的长度
        填充掩码的作用是：防止模型学习无意义的填充符；提高并行时的计算效率
        假设序列长度的真实值： batch 0 的前3位有效，batch 1的前2位有效
    &quot;&quot;&quot;
    valid_lens = torch.tensor([3, 2])
    padding_mask = torch.arange(seq_len).expand(batch_size, seq_len) &amp;gt;= valid_lens.unsqueeze(1)
    padding_mask = padding_mask.unsqueeze(1).unsqueeze(2)
    attn_weight_pad, output_pad = scaled_dot_product_attention(Q, K, V, mask=padding_mask)
    print(attn_weight_pad)
    print(output_pad)
    
    &quot;&quot;&quot; 
        因果掩码 Casual Mask
        构造 casual mask：有效位置为 False，填充位置为 True（需要掩盖）；在序列处理中，后续位置需要掩盖，防止模型看到未来的信息
        因果掩码的作用是：保证生成第t个词时，只能基于前t-1个词；训练时模拟推理时的条件
        生成上三角矩阵，对角线以上为 True（需要掩盖）
    &quot;&quot;&quot;
    casual_mask = torch.triu(torch.ones(seq_len, seq_len, dtype=torch.bool), diagonal=1)
    casual_mask = casual_mask.unsqueeze(0).unsqueeze(0)
    attn_weight_causal, output_causal = scaled_dot_product_attention(Q, K, V, mask=casual_mask)
    print(attn_weight_causal)
    print(output_causal)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心创新点2：Multi-Head Attention&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/blogs/images/1/VKQ.png&quot; alt=&quot;多头注意力（Multi-Head Attention）结构图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图5: 多头注意力（Multi-Head Attention）结构示意图&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在多头自注意力机制中，输入的V（Value）、K（Key）和Q（Query）向量分别经过多个独立的&quot;Linear&quot;线性变换模块，然后并行进入多个&quot;Scaled Dot-Product Attention&quot;模块进行计算。各注意力头的输出通过&quot;Concat&quot;模块拼接，最后再经过一个&quot;Linear&quot;模块进行线性变换融合。箭头清晰地显示了从输入到输出的整个数据流动过程。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\text{MultiHead}(Q, K, V) &amp;amp;= \operatorname{Concat}\left( \text{head}&lt;em&gt;{1}, \ldots, \text{head}&lt;/em&gt;{h} \right) W^{O} \
&amp;amp;\text{ where } \text{head}&lt;em&gt;{i} = \operatorname{Attention}\left( Q W&lt;/em&gt;{i}^{Q}, K W_{i}^{K}, V W_{i}^{V} \right)
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;解码器结构：掩码注意力、交叉注意力、解码流程&lt;/h2&gt;
&lt;h2&gt;位置编码：给序列注入位置信息&lt;/h2&gt;
&lt;p&gt;$$
\begin{aligned}
PE_{(pos,2i)} &amp;amp;= \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \
PE_{(pos,2i+1)} &amp;amp;= \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;编码器与解码器适用场景：BERT系、GPT系架构区分&lt;/h2&gt;
</content:encoded></item><item><title>NLP 文本表征：Word Embedding + Tokenizer + BPE 算法全解</title><link>https://gzhzk.github.io/blogs/posts/nlp-text-representations/</link><guid isPermaLink="true">https://gzhzk.github.io/blogs/posts/nlp-text-representations/</guid><description>从 One-Hot、词袋和 TF-IDF 到 Word Embedding、Tokenizer 与 BPE，系统理解文本如何被映射到连续语义空间，并附完整代码实现。</description><pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2013 年，Mikolov 等人提出 &lt;strong&gt;Word2Vec&lt;/strong&gt;，将文字映射为连续向量，让语义相似的词在空间中彼此靠近——这便是 &lt;strong&gt;Word Embedding&lt;/strong&gt;。它解决了传统稀疏表示（One-Hot、词袋）无法捕捉语义关联的痛点，从此成为连接文本与深度学习的基础桥梁。但词级方案面临 OOV、词表膨胀等缺陷，现代大模型转而采用 &lt;strong&gt;Tokenizer + BPE 子词分词&lt;/strong&gt;。借此博客，系统梳理 Embedding 的技术演进，从原理到代码。&lt;/p&gt;
&lt;h2&gt;从“符号鸿沟”到“语义空间”&lt;/h2&gt;
&lt;p&gt;让计算机理解语言，首先要解决一个根本矛盾：人类感知的是「语义」，而计算机处理的是「符号」。当你输入「猫」和「猫咪」，人类知道它们相似；但对计算机而言，这是两个毫无关联的编码。这便是自然语言处理中的「符号鸿沟」——离散符号与连续语义之间的断裂。Embedding 正是跨越这道鸿沟的核心桥梁：它将离散的自然语言符号映射到连续的向量空间，使得语义相似的对象在空间中彼此靠近。这个简洁的思想，支撑起了从 Word2Vec 到 ChatGPT 的所有语言模型。&lt;/p&gt;
&lt;p&gt;在 Transformer 体系中，Word Embedding 处于输入层的核心位置，负责完成从 Token 到向量的转换，为后续注意力机制与前馈网络提供可计算的语义表示。然而，传统词级嵌入面临 OOV（未登录词）、词表膨胀等难题，现代大模型普遍采用 Tokenizer + 子词分词（如 BPE）来构建更高效的词表与嵌入层。本文将系统梳理 Word Embedding 的技术演进，从 One-Hot 的稀疏困境，到 Word2Vec 的分布式表示，再到 BPE 子词分词的精妙设计，提供完整可运行的代码实现，帮助你构建从文本预处理到模型输入的全链路认知。&lt;/p&gt;
&lt;h2&gt;此前的经典方法：从统计到预训练&lt;/h2&gt;
&lt;h3&gt;One-Hot 编码&lt;/h3&gt;
&lt;p&gt;One-Hot 编码，顾名思义是为每个分类特征的每个可能值创建一个独立的二进制特征。在任意时刻，只有一个特征被激活（标记为 1），其余全部为 0。词表多大，向量就多大。例如，「猫」→ [1,0,0,0]，「狗」→ [0,1,0,0]，「鸟」→ [0,0,1,0]，「鱼」→ [0,0,0,1]。但是，这样操作存在不小的问题。首先就是当词的数量很大时，词向量的维度会爆炸；另外，词向量之间两两正交，无法准确刻画语义的相关性。&lt;/p&gt;
&lt;h3&gt;词袋模型&lt;/h3&gt;
&lt;p&gt;词袋模型（Bag-of-Words，BoW）的核心假设是：文档的语义由其包含的词汇决定，与词汇出现的顺序无关。它将一篇文档表示为一个「词频向量」——向量的每个维度对应词表中的一个词，维度的值是该词在文档中出现的次数。&lt;/p&gt;
&lt;h3&gt;TF-IDF&lt;/h3&gt;
&lt;p&gt;TF-IDF（词频-逆文档频率）是一种用于信息检索与文本挖掘的经典加权技术，由 Salton 等人在 1970 年代提出。它同时考虑&lt;strong&gt;词在当前文档中的出现频率&lt;/strong&gt;（TF）和&lt;strong&gt;该词在所有文档中的罕见程度&lt;/strong&gt;（IDF），从而筛选出对当前文档具有代表性的词汇。&lt;/p&gt;
&lt;p&gt;直观理解：一个词在本文档中出现很多（TF 高），但在所有文档中都常见（IDF 低），则区分度低；反之如果只在少数文档中高频出现，则很可能是该文档的关键词。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TF（词频）&lt;/strong&gt;：衡量词对单篇文档的重要程度&lt;/p&gt;
&lt;p&gt;$$
\text{TF}(t, d) = \frac{\text{词 } t \text{ 在文档 } d \text{ 中出现的次数}}{\text{文档 } d \text{ 中所有词的总次数}}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IDF（逆文档频率）&lt;/strong&gt;：衡量词对整个语料库的区分能力&lt;/p&gt;
&lt;p&gt;$$
\text{IDF}(t) = \ln\left(\frac{\text{语料库总文档数} + 1}{\text{包含词 } t \text{ 的文档数} + 1}\right) + 1
$$&lt;/p&gt;
&lt;p&gt;（加 1 是为了避免分母为 0）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终权重&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\text{TF-IDF}(t, d) = \text{TF}(t, d) \times \text{IDF}(t)
$$&lt;/p&gt;
&lt;p&gt;举例：假设语料库有 1000 篇文档，其中 50 篇包含&quot;深度学习&quot;，&quot;深度学习&quot;在某篇文档中出现 5 次，该文档总词数 200。则：&lt;/p&gt;
&lt;p&gt;TF = 5/200 = 0.025，IDF = ln(1001/51) + 1 ≈ 3.01，TF-IDF ≈ 0.075。&lt;/p&gt;
&lt;p&gt;而&quot;的&quot;&quot;是&quot;这类停用词几乎每篇文档都出现，IDF → 1，TF-IDF 接近 0，天然被过滤。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from collections import Counter
import math

def compute_tf(token_ids):
    &quot;&quot;&quot;计算词频 TF&quot;&quot;&quot;
    total = len(token_ids)
    counter = Counter(token_ids)
    return {word: count / total for word, count in counter.items()}

def compute_idf(corpus_tokenized, vocab):
    &quot;&quot;&quot;计算逆文档频率 IDF
    corpus_tokenized: List[List[str]]，每篇文档的分词列表
    &quot;&quot;&quot;
    N = len(corpus_tokenized)
    idf = {}
    for word in vocab:
        # 统计包含该词的文档数
        df = sum(1 for doc in corpus_tokenized if word in doc)
        idf[word] = math.log((N + 1) / (df + 1)) + 1
    return idf

def compute_tfidf(corpus_tokenized, vocab):
    &quot;&quot;&quot;计算 TF-IDF 矩阵&quot;&quot;&quot;
    idf = compute_idf(corpus_tokenized, vocab)
    tfidf_matrix = []
    for doc in corpus_tokenized:
        tf = compute_tf(doc)
        tfidf = {word: tf.get(word, 0) * idf[word] for word in vocab}
        tfidf_matrix.append(tfidf)
    return tfidf_matrix

# 示例
corpus = [
    [&quot;深度&quot;, &quot;学习&quot;, &quot;是&quot;, &quot;人工&quot;, &quot;智能&quot;, &quot;的&quot;, &quot;核心&quot;],
    [&quot;机器&quot;, &quot;学习&quot;, &quot;算法&quot;, &quot;在&quot;, &quot;工业&quot;, &quot;应用&quot;, &quot;广泛&quot;],
    [&quot;深度&quot;, &quot;学习&quot;, &quot;模型&quot;, &quot;参数&quot;, &quot;多&quot;, &quot;训练&quot;, &quot;慢&quot;],
]
vocab = list(set(word for doc in corpus for word in doc))
tfidf = compute_tfidf(corpus, vocab)
print(&quot;文档0 TF-IDF（按权重排序）：&quot;, sorted(tfidf[0].items(), key=lambda x: x[1], reverse=True)[:3])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;共现矩阵&lt;/h3&gt;
&lt;p&gt;共现矩阵（Co-occurrence Matrix）的核心思想是：&lt;strong&gt;语义相近的词往往在相似的上下文中出现&lt;/strong&gt;。它统计词与词在固定窗口内共同出现的频次，形成一个对称矩阵。&lt;/p&gt;
&lt;p&gt;例如语料库只有三句话：&quot;The cat sat on the mat&quot;、&quot;The dog ran in the garden&quot;、&quot;Cats and dogs are animals&quot;。设定窗口大小为 2（前后各看 1 个词），统计每对词共同出现的次数：&lt;/p&gt;
&lt;p&gt;$$
M_{ij} = \text{词 } i \text{ 和词 } j \text{ 在窗口内共同出现的次数}
$$&lt;/p&gt;
&lt;p&gt;从共现矩阵可以挖掘语义关系：&quot;cat&quot;和&quot;cats&quot;在相似上下文中出现，矩阵中对应的行/列分布相似；&quot;dog&quot;和&quot;cat&quot;经常出现在相似的动物语境中，距离也较近。&lt;/p&gt;
&lt;p&gt;但直接用原始共现矩阵有两个问题：高频词（如&quot;the&quot;）共现次数虚高；矩阵稀疏且维度等于词表大小（通常数万×数万）。解决方案是对矩阵做**奇异值分解（SVD）**降维，保留最重要的 k 维特征，得到词的稠密向量表示——这其实就是 LSA（潜在语义分析）的思路。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
from collections import Counter

def build_cooccurrence_matrix(corpus, vocab, window_size=2):
    &quot;&quot;&quot;构建共现矩阵&quot;&quot;&quot;
    word2idx = {w: i for i, w in enumerate(vocab)}
    n = len(vocab)
    matrix = np.zeros((n, n), dtype=np.float32)

    for doc in corpus:
        for i, word in enumerate(doc):
            # 窗口范围
            start = max(0, i - window_size)
            end = min(len(doc), i + window_size + 1)
            for j in range(start, end):
                if i != j:
                    matrix[word2idx[word]][word2idx[doc[j]]] += 1

    return matrix

def svd_reduce(matrix, k=50):
    &quot;&quot;&quot;SVD 降维得到词向量&quot;&quot;&quot;
    U, S, Vt = np.linalg.svd(matrix, full_matrices=False)
    # 取前 k 维，U * sqrt(S) 作为词向量
    word_vectors = U[:, :k] * np.sqrt(S[:k])
    return word_vectors

corpus = [
    [&quot;the&quot;, &quot;cat&quot;, &quot;sat&quot;, &quot;on&quot;, &quot;the&quot;, &quot;mat&quot;],
    [&quot;the&quot;, &quot;dog&quot;, &quot;ran&quot;, &quot;in&quot;, &quot;the&quot;, &quot;garden&quot;],
]
vocab = list(set(word for doc in corpus for word in doc))
matrix = build_cooccurrence_matrix(corpus, vocab)
vectors = svd_reduce(matrix, k=10)
print(f&quot;词向量形状: {vectors.shape}&quot;)  # (词表大小, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四种方法的对比总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;One-Hot&lt;/th&gt;
&lt;th&gt;词袋模型（BoW）&lt;/th&gt;
&lt;th&gt;TF-IDF&lt;/th&gt;
&lt;th&gt;共现矩阵 + SVD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;维度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;词表大小&lt;/td&gt;
&lt;td&gt;词表大小&lt;/td&gt;
&lt;td&gt;词表大小&lt;/td&gt;
&lt;td&gt;降维后维度 k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;稀疏性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极度稀疏&lt;/td&gt;
&lt;td&gt;稀疏&lt;/td&gt;
&lt;td&gt;稀疏&lt;/td&gt;
&lt;td&gt;低（降维后）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;语义捕捉&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;弱&lt;/td&gt;
&lt;td&gt;中等（浅层语义）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;共现信息&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;计算成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;中高（SVD）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;主要用途&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;基础基准&lt;/td&gt;
&lt;td&gt;文本分类特征&lt;/td&gt;
&lt;td&gt;关键词提取&lt;/td&gt;
&lt;td&gt;语义相似性（早期）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看到，这四种方法有一个共同局限：&lt;strong&gt;每个词只有唯一一个固定向量&lt;/strong&gt;，与上下文无关。这导致&quot;bank&quot;（银行）和&quot;bank&quot;（河岸）无法区分，歧义问题无法解决。Word2Vec 通过神经网络从大规模语料中学习语境相关的稠密表示，才真正开启了现代 Embedding 时代。&lt;/p&gt;
&lt;h2&gt;现代 NLP 预处理：Tokenizer 与 BPE 算法&lt;/h2&gt;
&lt;p&gt;前面几种方法有一个共同局限：每个词只有唯一一个固定向量，无法处理歧义问题。而词表过大又导致 OOV（未登录词）严重——这两种困境在子词分词（Subword Tokenization）面前迎刃而解，而 BPE（Byte Pair Encoding）正是子词分词最经典的代表。&lt;/p&gt;
&lt;h3&gt;核心思想：用子词粒度解决 OOV&lt;/h3&gt;
&lt;p&gt;先用一个直观的例子理解 BPE 为什么能解决 OOV：&lt;/p&gt;
&lt;p&gt;假设词表里只有 [&quot;high&quot;, &quot;higher&quot;, &quot;low&quot;, &quot;lower&quot;, &quot;new&quot;]，没有 newest。整词方案会对 newest 报 OOV。但 BPE 词表可能包含 [&quot;new&quot;, &quot;est&quot;, &quot;er&quot;, ...] 这些子词片段。那么 newest 就能被切分为 new + est，两个片段都在词表中——&lt;strong&gt;即使整词 OOV，子词仍然有效&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;高频词保持整词形态（节省表示长度），低频词拆解为子词组合（泛化能力强），这是 BPE 的核心设计哲学。&lt;/p&gt;
&lt;h3&gt;BPE 算法原理&lt;/h3&gt;
&lt;p&gt;BPE 原是 1994 年用于数据压缩的算法，核心思想极为简洁——&lt;strong&gt;贪心迭代合并最高频的相邻字符对&lt;/strong&gt;。具体步骤：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1：初始化字符级词表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将训练语料中每个词拆成单个字符（加上结束符 &amp;lt;/w&amp;gt; 表示词尾），以字符为初始 token。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;high&quot;    → h i g h &amp;lt;/w&amp;gt;
&quot;higher&quot;  → h i g h e r &amp;lt;/w&amp;gt;
&quot;low&quot;     → l o w &amp;lt;/w&amp;gt;
&quot;lower&quot;   → l o w e r &amp;lt;/w&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2：贪心合并最高频字节对&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;统计所有相邻字符对的频次，找到最高频的一对（如 h + i），将语料中所有 hi 合并为新符号 hi。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;合并后：
&quot;high&quot;    → hi g h &amp;lt;/w&amp;gt;
&quot;higher&quot;  → hi g h e r &amp;lt;/w&amp;gt;
&quot;low&quot;     → l o w &amp;lt;/w&amp;gt;
&quot;lower&quot;   → l o w e r &amp;lt;/w&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3：重复迭代 N 次&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每轮都重新统计频次、合并最高频对，不断产生新的子词符号。合并优先级：字符对 &amp;gt; 单字符 &amp;gt; 子词组合（因为子词是由字符对合并而来，所以字符对优先级最高）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from collections import Counter

def get_stats(vocab):
    &quot;&quot;&quot;统计所有相邻字节对的频次
    vocab: dict，{word: frequency}，word 内字符以空格分隔
    &quot;&quot;&quot;
    pairs = Counter()
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols) - 1):
            pairs[(symbols[i], symbols[i + 1])] += freq
    return pairs

def merge_vocab(pair, vocab):
    &quot;&quot;&quot;将最高频字节对合并为新符号，替换 vocab 中所有出现&quot;&quot;&quot;
    bigram = &apos; &apos;.join(pair)          # e.g. &quot;e s&quot;
    merged = &apos;&apos;.join(pair)           # e.g. &quot;es&quot;
    new_vocab = {}
    for word in vocab:
        new_word = word.replace(bigram, merged)
        new_vocab[new_word] = vocab[word]
    return new_vocab

# ── 演示完整训练流程 ──
vocab = {
    &apos;h i g h &amp;lt;/w&amp;gt;&apos;: 5,
    &apos;h i g h e r &amp;lt;/w&amp;gt;&apos;: 2,
    &apos;l o w &amp;lt;/w&amp;gt;&apos;: 3,
    &apos;l o w e r &amp;lt;/w&amp;gt;&apos;: 2,
}

# 迭代 10 次合并
num_merges = 10
for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best_pair = pairs.most_common(1)[0][0]
    vocab = merge_vocab(best_pair, vocab)
    print(f&quot;第{i+1}步合并 {best_pair} → {&apos;&apos;.join(best_pair)}&quot;)

print(&quot;\n最终词表（部分）：&quot;)
for word in list(vocab.keys())[:5]:
    print(f&quot;  {word}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;运行结果：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第1步合并 (&apos;h&apos;, &apos;i&apos;) → hi
第2步合并 (&apos;e&apos;, &apos;r&apos;) → er
第3步合并 (&apos;hi&apos;, &apos;g&apos;) → hig
第4步合并 (&apos;l&apos;, &apos;o&apos;) → lo
第5步合并 (&apos;lo&apos;, &apos;w&apos;) → low
...

最终词表（部分）：
  hig h &amp;lt;/w&amp;gt;
  hig h er &amp;lt;/w&amp;gt;
  low &amp;lt;/w&amp;gt;
  low er &amp;lt;/w&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到：高频片段（如 low）在合并中逐渐固化为独立 token，低频词通过子词组合表示。&lt;/p&gt;
&lt;h3&gt;BPE 分词：给定合并规则，对任意词分词&lt;/h3&gt;
&lt;p&gt;重点来了：训练得到合并规则（merge list）后，怎么用它分词？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心原则：按优先级从高到低，贪婪从左到右应用合并规则&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def tokenize(text, merges):
    &quot;&quot;&quot;给定 merge 规则，对文本进行 BPE 分词
    merges: List[(char, char)]，按优先级排序的合并操作序列
    &quot;&quot;&quot;
    # 初始化：每个字符 + 词结束符
    tokens = list(text) + [&apos;&amp;lt;/w&amp;gt;&apos;]

    # 按优先级顺序（先合并的规则优先）应用
    for pair in merges:
        # 从左到右扫描，看当前上下文能否匹配 pair
        i = 0
        while i &amp;lt; len(tokens) - 1:
            if tokens[i] == pair[0] and tokens[i + 1] == pair[1]:
                # 匹配：合并为一个 token，跳过已合并部分
                tokens[i] = pair[0] + pair[1]
                del tokens[i + 1]
                # 不移动 i，继续检查当前位置（可能形成新的可合并对）
            else:
                i += 1

    return tokens

# 假设从语料中学到的合并序列（优先级从高到低）
merges = [
    (&apos;h&apos;, &apos;i&apos;),    # 第1步：最高优先级
    (&apos;i&apos;, &apos;g&apos;),
    (&apos;e&apos;, &apos;r&apos;),
    (&apos;l&apos;, &apos;o&apos;),
    (&apos;o&apos;, &apos;w&apos;),
]

text = &quot;high&quot;
result = tokenize(text, merges)
print(f&quot;&apos;{text}&apos; 分词结果: {result}&quot;)   # [&apos;hi&apos;, &apos;g&apos;, &apos;h&apos;, &apos;&amp;lt;/w&amp;gt;&apos;]

text2 = &quot;hello&quot;   # OOV
result2 = tokenize(text2, merges)
print(f&quot;&apos;{text2}&apos; 分词结果(OOV): {result2}&quot;)   # [&apos;h&apos;, &apos;el&apos;, &apos;lo&apos;, &apos;&amp;lt;/w&amp;gt;&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分词流程的要点是：&lt;strong&gt;每次合并后不急于前进，而是继续检查当前位置&lt;/strong&gt;（因为刚合并出来的新符号可能可以和下一个符号再次合并）。这是 BPE 贪婪但高效的体现——每一步都做到当前最优，不回溯。&lt;/p&gt;
&lt;p&gt;可以看到，high 最终被切为 hi + g + h + &amp;lt;/w&amp;gt;，因为我们在 merge 规则里没有 hi → hig 这条（需要更高频才能出现），所以 hi 是最大可合并单元。相比之下，如果词表里有 high，分词就会停在 high + &amp;lt;/w&amp;gt;——这正是 BPE 的特性：高频词尽量保持完整，低频词才拆解。&lt;/p&gt;
&lt;p&gt;真实场景中（如 GPT-2 / LLaMA），词表通常包含数万个 merge 规则，对应数万个子词 token。OOV 词通过字符级逐步拆分总能被表示——这是 BPE 相比整词分词的根本优势。&lt;/p&gt;
&lt;h2&gt;Transformer 中的 Word Embedding 原理&lt;/h2&gt;
&lt;p&gt;在 Transformer 架构中，Word Embedding 处于输入层核心位置，与位置编码（Position Encoding）相加后共同构成输入向量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结构示意&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Token序列  →  Tokenizer  →  Token IDs  →  Embedding层  →  Token Embeddings
                                                            +
                                                       Position Encoding
                                                            =
                                                    输入向量 h₀
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Embedding 层本质上是一个 vocab_size × d_model 的可学习参数矩阵 $W_E$。每个 token ID 对应矩阵中的一行，查询该行即得到该 token 的稠密向量表示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

class TokenEmbedding(nn.Module):
    &quot;&quot;&quot;Token Embedding 层&quot;&quot;&quot;
    def __init__(self, vocab_size, d_model):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.d_model = d_model

    def forward(self, token_ids):
        # token_ids: (batch_size, seq_len)
        # 输出: (batch_size, seq_len, d_model)
        return self.embedding(token_ids) * math.sqrt(self.d_model)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;乘以 $\sqrt{d_{model}}$ 是原论文的一个 trick：Embedding 的方差通常较小，直接与位置编码相加会导致位置编码的信息被稀释，乘上 $\sqrt{d_{model}}$ 可以让两者的量级更加匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完整的嵌入层（含位置编码）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import math

class TransformerEmbedding(nn.Module):
    &quot;&quot;&quot;Embedding = Token Embedding + Position Encoding&quot;&quot;&quot;
    def __init__(self, vocab_size, d_model, max_len=5000, dropout=0.1):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, d_model)
        self.pos_emb = self._generate_positional_encoding(max_len, d_model)
        self.dropout = nn.Dropout(p=dropout)
        self.d_model = d_model

    def _generate_positional_encoding(self, max_len, d_model):
        &quot;&quot;&quot;生成固定的位置编码（正弦版本）&quot;&quot;&quot;
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)  # (max_len, 1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2, dtype=torch.float) *
            (-math.log(10000.0) / d_model)
        )  # (d_model/2,)
        pe[:, 0::2] = torch.sin(position * div_term)       # 偶数维度
        pe[:, 1::2] = torch.cos(position * div_term)       # 奇数维度
        pe = pe.unsqueeze(0)  # (1, max_len, d_model)，便于广播
        return pe

    def forward(self, token_ids):
        seq_len = token_ids.size(1)
        # Token Embedding 并缩放
        token_emb = self.token_emb(token_ids) * math.sqrt(self.d_model)
        # 加上位置编码（取前 seq_len 行）
        pos_emb = self.pos_emb[:, :seq_len, :]
        return self.dropout(token_emb + pos_emb)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整代码实现（从文本到 Embedding 输出）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn
import math
from collections import Counter

# ─────────────── 1. 简易 BPE 分词器 ───────────────

class SimpleBPE:
    &quot;&quot;&quot;简化 BPE 实现（用于演示原理）&quot;&quot;&quot;
    def __init__(self, vocab, merges):
        # vocab: word -&amp;gt; freq 字典
        # merges: List[(char, char)] 合并操作序列
        self.vocab = vocab
        self.merges = merges
        self.tokenizer = self._build_tokenizer()

    def _build_tokenizer(self):
        return self.merges  # 直接存储合并规则

    def encode(self, text):
        &quot;&quot;&quot;对文本进行 BPE 分词&quot;&quot;&quot;
        tokens = list(text) + [&apos;&amp;lt;/w&amp;gt;&apos;]
        for merge in self.merges:
            new_tokens = []
            i = 0
            while i &amp;lt; len(tokens):
                if i &amp;lt; len(tokens) - 1 and tokens[i] == merge[0] and tokens[i+1] == merge[1]:
                    new_tokens.append(merge[0] + merge[1])
                    i += 2
                else:
                    new_tokens.append(tokens[i])
                    i += 1
            tokens = new_tokens
        return tokens

    def decode(self, tokens):
        return &apos;&apos;.join(t).replace(&apos;&amp;lt;/w&amp;gt;&apos;, &apos;&apos;)

# ─────────────── 2. 词表映射 ───────────────

class Tokenizer:
    &quot;&quot;&quot;Tokenizer：将 token 序列转为 ID 序列&quot;&quot;&quot;
    def __init__(self, bpe):
        self.bpe = bpe
        vocab_set = set()
        for word in bpe.vocab:
            tokens = word.split()
            vocab_set.update(tokens)
        # 加入 merge 后产生的新符号
        for m in bpe.merges:
            vocab_set.add(m[0] + m[1])
        self.vocab = [&apos;&amp;lt;pad&amp;gt;&apos;, &apos;&amp;lt;unk&amp;gt;&apos;] + sorted(vocab_set)
        self.word2idx = {w: i for i, w in enumerate(self.vocab)}
        self.pad_idx = 0
        self.unk_idx = 1

    def encode(self, text):
        tokens = self.bpe.encode(text)
        return [self.word2idx.get(t, self.unk_idx) for t in tokens]

    def decode(self, ids):
        tokens = [self.vocab[i] for i in ids]
        return self.bpe.decode(tokens)

# ─────────────── 3. Transformer Embedding ───────────────

class TransformerEmbedding(nn.Module):
    def __init__(self, vocab_size, d_model, max_len=512, dropout=0.1):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, d_model, padding_idx=0)
        self.pos_emb = self._generate_positional_encoding(max_len, d_model)
        self.dropout = nn.Dropout(dropout)
        self.d_model = d_model

    def _generate_positional_encoding(self, max_len, d_model):
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2, dtype=torch.float) *
            (-math.log(10000.0) / d_model)
        )
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        return pe.unsqueeze(0)  # (1, max_len, d_model)

    def forward(self, token_ids):
        seq_len = token_ids.size(1)
        token_emb = self.token_emb(token_ids) * math.sqrt(self.d_model)
        pos_emb = self.pos_emb[:, :seq_len, :].to(token_ids.device)
        return self.dropout(token_emb + pos_emb)

# ─────────────── 4. 端到端演示 ───────────────

def demo():
    # 准备语料
    vocab = {
        &apos;h i g h &amp;lt;/w&amp;gt;&apos;: 5,
        &apos;l o w &amp;lt;/w&amp;gt;&apos;: 3,
        &apos;l o w e r &amp;lt;/w&amp;gt;&apos;: 2,
        &apos;n e w &amp;lt;/w&amp;gt;&apos;: 4,
    }
    # 模拟学习到的合并规则（真实场景需从语料统计得到）
    merges = [(&apos;e&apos;, &apos;r&apos;), (&apos;l&apos;, &apos;o&apos;), (&apos;o&apos;, &apos;w&apos;)]

    bpe = SimpleBPE(vocab, merges)
    tokenizer = Tokenizer(bpe)

    # 输入文本
    text = &quot;higher&quot;
    token_ids = tokenizer.encode(text)
    print(f&quot;原文: &apos;{text}&apos;&quot;)
    print(f&quot;分词: {bpe.encode(text)}&quot;)
    print(f&quot;Token IDs: {token_ids}&quot;)

    # 转为 Tensor
    token_ids_tensor = torch.tensor([token_ids])
    print(f&quot;Tensor shape: {token_ids_tensor.shape}&quot;)

    # Embedding
    d_model = 128
    vocab_size = len(tokenizer.vocab)
    embed = TransformerEmbedding(vocab_size, d_model)

    output = embed(token_ids_tensor)
    print(f&quot;Embedding 输出形状: {output.shape}&quot;)  # (1, seq_len, 128)
    print(f&quot;输出向量示例（第一个token）:\n{output[0, 0, :10]}&quot;)

if __name__ == &quot;__main__&quot;:
    demo()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;现代演进与重点解读&lt;/h2&gt;
&lt;p&gt;BPE 之后，主流分词方案不断演进：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WordPiece&lt;/strong&gt;（BERT 采用）：与 BPE 类似，但合并标准是&lt;strong&gt;最大化训练数据的似然&lt;/strong&gt;，而非单纯统计频次。它会优先合并那些使语料整体似然提升最多的字节对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SentencePiece&lt;/strong&gt;（LLaMA 等采用）：将整个句子作为字节序列，直接在未分词的原始语料上学习 BPE，不依赖空格信息，对中文、日文等无显式词边界的语言更友好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Embedding 的演进&lt;/strong&gt;：从静态 Word2Vec 到语境化（Contextualized）表示——ELMo 的双向 LSTM、GPT 的 Transformer Decoder、再到 BERT 的 MLM 预训练，Embedding 的含义从&quot;查表&quot;演变为&quot;根据上下文动态计算&quot;，这直接催生了 GPT 系列和 BERT 系列两大主流大模型路线。&lt;/p&gt;
&lt;h2&gt;拓展动手实现&lt;/h2&gt;
&lt;p&gt;如果想进一步深入，可以尝试以下几个方向：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Word2Vec 实现&lt;/strong&gt;：用 Skip-gram + 负采样在小型语料上训练词向量，用 gensim 库加载或自己实现，感受 CBOW/Skip-gram 的核心前向传播。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;完整的 BPE 训练&lt;/strong&gt;：对真实语料（如 Wikipedia 摘要）实现完整的 BPE 词表学习流程，观察不同 merge 次数对词表大小的影响。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BERT Tokenizer 对比&lt;/strong&gt;：用 transformers 库加载 BERT 和 GPT-2 的 Tokenizer，对比中英文混合文本的分词差异，理解 WordPiece 和 SentencePiece 的实际区别。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结与展望&lt;/h2&gt;
&lt;p&gt;从 One-Hot 到 Transformer Embedding，文本表征经历了从&lt;strong&gt;稀疏到稠密&lt;/strong&gt;、从&lt;strong&gt;离散到连续&lt;/strong&gt;、从&lt;strong&gt;静态到语境化&lt;/strong&gt;的演进。这一条演进链的每一步都有其必然性：&lt;/p&gt;
&lt;p&gt;固定向量无法处理歧义 → Word2Vec 通过神经网络学习稠密语义表示；整词粒度无法解决 OOV → BPE 子词分词优雅地平衡词表大小与表达能力；静态向量无法捕捉上下文 → Transformer 的自注意力机制让模型能够根据输入动态计算语境感知表示。&lt;/p&gt;
&lt;p&gt;理解了 Embedding 的前世今生，再回头去看 Transformer 的输入层，你会感受到作者在设计上的深思熟虑：Embedding 不是孤立存在的，它与位置编码、注意力机制共同构成了现代大语言模型的基石。&lt;/p&gt;
</content:encoded></item></channel></rss>