最近在上机器翻译的课程。作为机器翻译领域的统计学模型的开山模型，姑且记录一下吧。背景一段时间之前，机器翻译主要靠专家手写语法规则，这样做机器翻译太慢、太贵、太难维护，比如我们看下面两句话：Time flies like an arrow.Fruit flies like a banana.这两句话的结构极其相似，唯一的区别便是用词不同，词的含义不同。如果想要通过写规则，从语法句法上将这两句话翻译成中文，就会很麻烦。随着技术的不断发展，我们获得了大量的平行语料，所谓平行语料，就是意义相同的句子，用两种语言书写，比如：{ "zh-cn": "我是一只猫", "en-US": "I am a cat", "ja": "私は猫が好き" }那么学者就想，可否应用统计学的方法，从双语语料中自动学习出对应关系？学者发现，不同语言的句子之间，词语之间是有一个大致的对应关系的。比如在我们的例子中，从中文到英语，单词之间是严格对应的，但是从中文到日语，会发现有些语法助词，比如「は」和「が」，没有一个很好的对应关系。所以我们需要在工程上做一点设计。核心思想学者们首先不去想如何解决这个问题，它们首先想，什么是一个好的机器翻译系统。这很简单，给定平行语料，对于一个意义的句子，源端目标端分别是$s$和$t$，那么对于一个理想的机器翻译系统$M$，它必然满足：$$ M(s)=t $$即给定源端，它会输出一个最合适的目标端句子，那么模型的参数就相当于：$$ \theta =\arg\max P(f\mid e) $$让理想的目标端句子出现的概率最高即可，但是当我们部署系统之后，源语言句子和目标语言句子几乎不可能在语料库中出现过，因此这个概率无法直接从语料库统计得到。所以我们不直接学习分布，而是先找到共现关系！我们先引入一个变量，叫做对齐变量：$a_j=i$，表示源端的第$i$个词，会翻译成目标端的第$j$词。那么有些词没有很好的对应关系，就把它们看作是由空白词（记作null）生成过来的。比如：SRC: null 我 喜欢 猫TGT：私 は 猫 が 好き对于目标端（TGT）的「私」，是由源端（SRC）的「我」一次生成过来的，从0开始索引，这就是第一个词……按照这个规律，我们能得到对齐变量为$a=(1,0,3,0,2)$。数学推导根据全概率公式，我们的目标：$$ P(e\mid f) = \sum P(f,a\mid e) $$然后把对齐变量打开，用$t(f_j|e_{a_j})$表示从$e_{a_j}$，源端的第$a_j$个词，翻译到目标端的第$j$个词的概率（$l$为源端句子长，$m$为目标端句子长）：$$ P(e\mid f) = \sum_{a_1=0}^l\sum_{a_2=0}^l\cdots\sum_{a_m=0}^l\frac{1}{(l+1)^m}\prod_{j=1}^mt(f_j|e_{a_j}) $$直接计算这个公式，复杂度太高，做一个简化，我们先考虑目标和源端都有两个词的情况：$$ P(f\mid e)=\sum_{a_1=0}^l\sum_{a_2=0}^lt(f_1|e_{a_1})t(f_2|e_{a_2}) $$根据高等数学的知识，级数前后两项分别只于自己的求和下标有关，拆开：$$ P(f\mid e)=\sum_{a_1=0}^l\sum_{a_2=0}^lt(f_1|e_{a_1})t(f_2|e_{a_2})=(\sum_{a_1=0}^l(f_1|e_i))(\sum_{a_2=0}^l(f_2|e_i)) $$Good，现在推导到$m$个词的情况：$$ P(f\mid e)=\frac{1}{(l+1)^m}\prod_{j=1}^m\sum_{i=0}^lt(f_j|e_i) $$这个公式的感性理解就是：目标词$f_j$可以由源句中任意一个词生成，所以它的概率是所有可能来源的贡献之和。整个目标句由每个目标词独立生成，所以再把每个目标词的概率乘起来。所以，我们的问题从「构造翻译系统」，变成了「找到一个最大化概率的词对齐关系」！但是……不知道怎么去学习！训练推导训练这里就用EM算法，严格的推导需要拉格朗日乘子法，但是没学过太难了看不懂，用一个比较直觉的方法！假设有一个新人进了汉化组，新人什么都不懂，所以觉得：猫 可能是 cats猫 也可能是 like猫 甚至可能是 I看字幕总结一下规律：猫 和 cats 总是一起出现好き 和 like 总是一起出现犬 和 dogs 总是一起出现那么就把：猫 -> cats 的概率高一点好き -> like 的概率高一点犬 -> dogs 的概率高一点最后就能学到一个真正的对应关系。落到形式上，假设我们有双语平行语料：$D=\{(e^{(s)},f^{(s)})\}_{s=1}^S$，我们的目标是找一个概率分布，让训练目标端句子出现概率最大：$$ L(t)=\prod_{s=1}^SP(f^{(s)}\mid e^{(s)}) $$取对数积变和，因为加法远比乘法好处理：$$ l(t)=\log L(t)=\sum_{s=1}^S\log P(f^{(s)}\mid e^{(s)}) $$把后面的概率用上面的推导代入：$$ l(t)=\sum_{s=1}^S\log [\frac{1}{(l_s+1)^{m_s}}\prod_{j=1}^{m_s}\sum_{i=0}^{l_s}t(f_j^{(s)}\mid e_i^{(s)})] $$拆开：$$ l(t)=\sum_{s=1}^S[-m_s\log(l_s+1)+\sum_{j=1}^{m_s}\log\sum_{i=0}^{l_s}t(f_j^{(s)}\mid e_i^{(s)})] $$加和的前一项和句子长度有关，和优化没关，要优化的就只有后面一项，约束为$\sum_ft(f\mid e)=1$。这之后我们就用E步，估计隐变量的概率，也就是「在当前模型看来，每种词对齐概率是多少呀？」，随后用M步更新参数，「重新估计一下概率吧？」。EM算法的思想就是，「当模型里有看不见的隐变量时，先根据当前参数“猜”隐变量的分布，再用这个猜出来的分布重新估计参数。不断重复」。E步我们要求的概率是：$P(a\mid e,f;\theta^{old})$，首先根据「每个词独立生成」的假设，和贝叶斯公式：$$ P(a_j=i\mid f,e)=\frac{P(a_j=i,f_j\mid e)}{\sum_{i'=0}^lP(a_j=i',f_j|e)} $$然后根据条件概率的知识：$$ P(a_j=i,f_j\mid e)=P(a_j=i\mid e)P(f_j\mid a_j=i,e) $$以及我们第一次迭代的时候假设，每个词翻译到目标词的概率都是均等的：$$ P(a_j=i\mid e)=\frac{1}{l+1} $$那么上面的公式变成：$$ P(a_j=i,f_j\mid e)=\frac{1}{l+1}t(f_j\mid e_i) $$代入到上面的式子，上下约去$\frac{1}{l+1}$：$$ P(a_j=i\mid f,e)=\frac{t(f_j\mid e_i)}{\sum_{i'=0}^lt(f_j\mid e_i')} $$这是一个类似Softmax的形式，其含义为：一个源词$e_i$对目标词$f_j$的责任，等于它的翻译概率除以所有候选源词的翻译概率总和。举个例子，假设我们的例子：SRC: null 我 喜欢 猫TGT：私 は 猫 が 好き对于日语的「猫」，从「null」、「我」、「喜欢」和「猫（中文）」翻译到它的概率分别是0.01、0.02、0.05和0.88,那么：$$ P(a_j=猫\mid f,e)=\frac{0.88}{0.01+0.02+0.05+0.88}=0.92 $$这个0.92我们称之为软计数。到达M步，如果我们已知词对齐的话，只需要计算：$$ t(f|e)=\frac{\text{count}(e,f)}{\sum_{f'}\text{count}(e,f')} $$但是，我们不知道真实的词对齐关系，就用软计数代替$\text{count}$去更新。总结有没有方便记忆的方法呀……公式看不懂一点！当然有了！因为翻译不只是翻译，也可以是广义的序列任务！假设你有一集电视剧/电影/番剧。你要根据视频生成最有可能的弹幕/即时评论列表。有很多弹幕，但是你很笨，不知道这些弹幕要在什么时候发出去。那么可以做这样的事情：1.把这个视频切分成若干片段。（分词）2.尝试对应片段和弹幕。（加入对齐变量和E步）3.根据已有的训练集，大量片段和弹幕，慢慢找到共现规律。（M步）4.训练完成。效果：名场面 ← 反派登场 ？？？？？？ ← 主角做出反人类的举动
最近正在从事一些对Agent的研究，首先研究了一下相关的数据集，发现颇有意思，这次就根据组会汇报的草稿，总结成一个文档的形式。主要回答两个问题：现有的Agent数据集如何分类它们覆盖了哪些任务场景在开始调研之前，我觉得有必要给读者讲述一下所谓Agent究竟是什么。Agent这个概念我在之前的文章中其实是解释过的，简单来说就是「可以使用工具的AI」。知道这一点就够了。发展阶段总体来说，Agent数据集的发展方向分为三个阶段：早期：主要评测静态任务，集中于单能力、短链路的问题评估。中期：引入了可复现环境，开始测试稍稍复杂一些的能力。近期：开始强调动态性，更加贴近真实办公场景。从 2023 年至 2024 年，Agent 数据集总体上仍以单项能力评估为主。常见拆分维度包括工具调用、网页浏览、软件工程、GUI 操作和多步推理等。2024年末发布的 TheAgentCompany 可视为一个重要转折点（示例如图 2所示）：它不再仅评估某一工具能力，而是模拟员工工作流程，涵盖网页浏览、代码编写、程序运行等任务。与此同时，这一阶段仍存在任务偏短、动态性不足等问题。到 2025 年，Agent 数据集明显向场景化与长程化演进。以网络搜索方向为例，任务从短链路网页检索转向长程信息搜集： OpenAI 的 BrowseComp包含 1266 个需持续浏览互联网的问题；Mind2Web 2 提供 130 个真实长程网页任务，并引入 Agent-as-a-judge 评估复杂且时变的答案；字节发布的 WideSearch则聚焦大规模信息搜集，包含 200 个中英文任务，结果显示多数系统成功率接近 0%，最好约为 5%。进入 2026 年后，数据集进一步强调动态性、异步性与真实工作属性。 GAIA2将环境设定为可独立演化的时间线系统，要求 Agent 在时间约束、噪声事件、动态变化与协作条件下行动。示例任务包括「如果三分钟无人回复则叫车」与「找出当前时刻朋友最多的城市」等。针对这个阶段变化，我的想法是，未来的方法类工作，其瓶颈一定不只在模型本身，更在于系统该如何组织多个Agent、工具和记忆。分类学Agent数据集可以分为四类：能力数据集：关注单项能力，环境状态少。比如HumanEval用于评估 Agent 在编程任务上的表现：给定编程问题，通过准确率等基础指标衡量模型性能。沙箱类数据集：在能力数据集的基础上，增加了可复现环境的内容，通常也有自动评分系统。以 WebArena 为例，其通过可复现的 Web 环境（如网页快照）观察模型在复杂任务中的表现，例如要求 Agent 在历史页面快照中完成检索和决策任务。工作流数据集：通常是多应用、多步骤、流程依赖的数据集。例如 TheAgentCompany引入通讯与工作流软件，使 Agent既要完成协作沟通，也要完成流程性任务。其它专项数据集：比如安全领域的数据集AgentHarm，以及评估记忆能力的数据集MemoryArena等。这个分类背后的演进脉络也和我们之前所说的一致：从「单一能力评测」到「简化环境中的复合能力评测」，再到最近的「复杂环境中的真实环境评测」。通俗一点来说，评测对象从「模型答对了没有」逐步过渡到了「系统能否持续完成复杂的目标」。我认为这个转折的一个标志性数据集是TheAgentCompany数据集，它的一个Data Sample如下所示：{ "task_id": "admin-arrange-meeting-rooms", "role": "Administrator", "instruction_summary": "根据多个团队在下周二提交的会议时间，计算最少需要预订多少个会议室；把答案发送给 Chen Xinyi，并写入 /workspace/ans.txt。", "required_capabilities": [ "时间区间重叠计算", "资源调度", "RocketChat 通信", "文件写入" ], "expected_output": { "chat_message": "最少会议室数量", "file": "/workspace/ans.txt" } }Agent不仅需要进行文件的读写，还要进行计算和通信。当然，这个数据集在目前看来，仍然是有些简单和朴素了，不过放在过去那个全都是评估简单问题的时代，的确有着划时代的意义。任务场景最多的任务场景，就是网络环境数据集，也就是评估Web Agent的数据集，比如GAIA，通过联网检索+多模态的信息整合；WebLINX则将网络导航和多轮对话结合到了一起。还有OS环境下的任务场景，比如OSWorld，要求Agent具有在桌面环境进行鼠标移动、点击的能力，完成一些任务，比如Office套件的操作等。至于广受关注的Coding环境，则有SWE-Bench系列，主要评估代码仓库Issue和一些编程问题的能力；LongCLI-Bench则更关注长程的编码任务。
最近有工作需要用到这个东西，姑且研究一下。代码太多，没法一行一行审计，就挑重要的说一说吧。不排除以讹传讹的可能性。之前写过一篇【RethinkAI】浅思Agent，研究得还是不够深入。接入层：Channels配置过OpenClaw的小伙伴可能知道，OpenClaw是允许通过各种即时通讯软件(IM)控制的，也允许本地的TUI/Web UI等方法交互。假设我们配置好了OpenClaw+某一个IM（比如Telegram或者飞书），那么信息首先通过接入层的这些IM，然后通过各个IM的原生方法，将信息传到本地的Gateway中。比如WhatsApp会使用Baileys Web，Telegram会走grammY。假设我们配置好了OpenClaw+本地TUI，那么信息就通过WebSocket+JSON的形式传给Gateway。总而言之，这一步做的事情，就是将各个渠道信息，汇总给Gateway，进行后续处理。中控层：GatewayGateway这一层算是比较重要的一层了，这一层长期运行，负责维护本地OpenClaw和所有Channel之间的连接、管理会话的状态、接收消息、发出任务指令、权限认证管理。当消息从channel从外部渠道涌入后，Gateway层会：消息识别与Session选择：把消息归入某个会话上下文。Agent选择（可选）：常规来说，OpenClaw每个session的运行是串行运行的，如果开启了Multi-agent Routing，那么Gateway还会将信息进行指派，分派给特定的agent进行处理。进入队列（可选）：如果当前的session中，已经在跑了，那么Gateway会判断是否能并发跑，如果不能立即执行，就排队；否则就开始跑。这里的目的是，优先避免多个执行流同时改写同一会话状态。上下文组装：真正运行前，Gateway会把当前的输入和相关的上下文组合起来。开跑！：进入了模型推理和工具执行步骤，也就是实际开始运行了。流式返回结果：Gateway会将结果以流式传输的形式，传递给原渠道。这个过程中，消息可以有很多，所以，Gateway也会对消息进行排队处理、并发处理。总而言之，这一步做的事情，就是对信息进行验证、整理并执行。运行层：Multi-agent Routing与Agent运行层Agent运行层这里，就比较直观了，我们可以通过观察日志窥见这里运行的细节，下面的日志我做了很大的简化，去除了一些无关痛痒的信息，比如时间戳等：{"type":"message","message":{"role":"user","content":[{"type":"text","text":"Sender (untrusted metadata):\n```json\n{\n \"label\": \"openclaw-tui\",\n \"id\": \"openclaw-tui\",\n \"name\": \"openclaw-tui\",\n \"username\": \"openclaw-tui\"\n}\n```\n\n[Mon 2026-03-30 13:48 GMT+8] Can you create a directory named boo in my home dir? Create a hello world program in it."}]}} {"type":"message","message":{"role":"assistant","content":[{"type":"toolCall","id":"functions.exec:0","name":"exec","arguments":{"command":"mkdir -p ~/boo && cat > ~/boo/hello.py << 'EOF'\n#!/usr/bin/env python3\nprint(\"Hello, World!\")\nEOF\nchmod +x ~/boo/hello.py","ask":"on-miss"}}],"api":"openai-completions","provider":"custom-dashscope-aliyuncs-com","model":"kimi-k2.5","stopReason":"toolUse",}} {"type":"message","message":{"role":"toolResult","toolCallId":"functions.exec:0","toolName":"exec","content":[{"type":"text","text":"(no output)"}],"details":{"status":"completed","exitCode":0,"durationMs":10,"aggregated":"","cwd":"/home/iwakura/.openclaw/workspace"},"isError":false}} {"type":"message","message":{"role":"assistant","content":[{"type":"text","text":"Done! Created:\n\n- **Directory:** `~/boo/`\n- **File:** `~/boo/hello.py` — a simple Python \"Hello, World!\" program\n\nWant me to run it to verify it works?"}],"api":"openai-completions","provider":"custom-dashscope-aliyuncs-com","model":"kimi-k2.5","stopReason":"stop"}} {"type":"compaction","summary":"## Goal\n(no goal established - empty conversation)\n\n## Constraints & Preferences\n- (none)\n\n## Progress\n### Done\n- (none)\n\n### In Progress\n- (none)\n\n### Blocked\n- (none)\n\n## Key Decisions\n- (none)\n\n## Next Steps\n1. (awaiting user input to begin conversation)\n\n## Critical Context\n- (none)"}这里我们看到：第一行的JSON是表示传进来的信息，通过Channels层和Gateway层处理得到的部分信息，我们发现，在信息之前，加上了一些元数据。第二行是Agent调用本地工具。第三行是Agent调用工具的结果。第四行是Agent返回的信息。最后一行是compaction，也就是压缩，这是为了尽量规避Context Rot的问题、降低上下文膨胀、保留任务状态等目标，将上下文做结构化摘要，以更短、更稳定的形式保留任务目标、约束、进度和下一步。这就是Agent Loop的运行流程：信息处理-给出指令-运行指令-返回结果-返回信息。正如我在之前那篇文章中说的那样：这里我们可以窥探到全貌，工作流程是：需求输入 - 云端模型处理 - 返回内容和指令 - 执行指令 - 根据指令结果判断正误。AI 还是那个 AI，那个接收不定长文本，输出不定长文本的 AI，只是 OpenClaw 对输出的结果进行了处理，提取出了工具，然后在终端执行。那么Multi-agent Routing又是什么个东西？诸君可以将之理解为一个「按规则选大脑」的东西，同一个OpenClaw可能跟着不同的IM，比如同时接Telegram、飞书、Slack等，这个时候就需要选择，根据不同的IM，选择不同的Agent，从而获得不同的上下文等等。Multi-agent Routing 更像一个路由器：它可以根据消息来源、会话、身份、规则或显式配置，把请求导向不同 agent。按 IM 分流只是其中一种例子。这里和NLP领域的传统Multi-agent Debate不同，这里没有Debate，更像是一个办事处，根据不同的需求选择进入不同的屋子，每个屋子里只有一个办事人员。当然，这些工作人员通常是不会走Multi-agent Debate那种交叉通信的，除非明确启用这一点。这里可以参见官方文档多智能体路由。记忆层与Skills那有读者可能会问了，「哎呀！那么这说的Memory和Skills又是个什么东西呀！」，别急。这两个东西其实没什么特别高大上的东西，它们就是Markdown文件！Memory顾名思义，就是记忆啦，OpenClaw的记忆存储两类文件：长期记忆和短期记忆，短期记忆通常是以天为单位的，长期记忆就是长期的（不然呢……），记录更加稳定的，对于用户的描述。这些文件可以是AI维护，你也可以自己修改。在OpenClaw中，这些Markdown形式的记忆会被分块，然后存储到一个SQLite数据库中，从而依靠向量化的检索，加速调用。Skills就是一个Markdown文件，包含了对于任务的描述，其实就是一个精细化的Prompt模板。后记到这里，我觉得该讲的都差不多讲了，粗粗地过了一遍，如果想要深入了解，那么就要去深挖OpenClaw的代码，不过那个代码库可是极其庞大。OpenClaw的具体的架构流程也就是如此：IM/TUI给消息 - Channels层接收传递给Gateway层 - Gateway层将消息给到对应Session的对应Agent - 排队 - 组装上下文 - 调用云端API进行处理 - 运行工具 - 运行结果回传云端 - 给出返回信息 - 压缩上下文 - 将信息回传给IM/TUI。
引子最近的俱乐部群，很多时间都在讨论Coding Agent的相关技术。这让我很诧异，在我的印象中，Agent是一个Reinforcement Learning的terminology。RL的主体是一个Agent，Agent通过与环境互动，从而获得奖惩，优化自身的参数，达到最优的表现。但是，在讨论中我发现，如今这个术语发生了很巧妙的变化。本文就来记录一下，我认知中的术语迁移缘由，和我对AI Agent的看法。术语迁移：什么是AI Agent？所谓AI Agent，就是一个基于大模型的，能够自主规划、调用工具、完成复杂任务的智能体。AI Agent这个词，需要拆成两个词来看——AI和Agent。在现在的语境下，AI基本上等同于自然语言处理领域(NLP)的大语言模型(LLM)。在ChatGPT刚出现那一阵，很多人就把这类技术叫做AI了。实际上，大模型是，且仅是这样一个东西——接收一个不定长的文本，输出一个不定长的文本，仅此而已。而Agent——正如同我们在引子里所说的那样——是一个来自于强化学习领域的术语。强化学习的部分理论可以参见我的博文【AI】强化学习常用算法小记（上）。强化学习和一般的学习方法不同，聚焦于「模型与环境之间的交互」上，从而获得「奖惩」，作为学习依据。而在AI Agent这个词中，剥夺去了「奖惩」这一部分，更强调「模型本身能够与环境进行交互」，能够使用外部工具，能够接收一个模糊的需求，输出一个详细的解决方案来。工作原理：AI Agent其内如何？这个东西究竟是怎么工作的呢？一个AI Agent需要本地和云端两个部分，本地需要部署相应的框架，比如OpenClaw等，而云端则需要购买AI使用权限。举例来说，假设我本地有一个AI Agent，接入了硅基流动的DeepSeek V3模型，然后，在session中输入：Create a file named a.txt for me然后，它就在本地创建了一个名为a.txt的文件。这里我们发现，AI Agent和AI不同，它的输出不仅仅是「不定长的文本」，而是真真切切在本地做了一些操作。那么这是怎么做到的呢？还是以OpenClaw为例，我们查找session历史可以发现：{"type":"thinking","thinking":"The user wants me to create a file named \"a.txt\". This is a straightforward request. I should use the write tool to create this file.\n\nSince they didn't specify any content, I'll create an empty file or add a simple placeholder. Let me just create it with empty content or a minimal placeholder.\n\nI'll create the file in the workspace directory: XXX\n\nAccording to instructions, default behavior is to just do routine tasks without narrating. This is a simple file creation, so I should just do it.","thinkingSignature":"reasoning_content"}, ... {"type":"thinking","thinking":"PowerShell doesn't use echo. > for creating files. Let me try the PowerShell way.","thinkingSignature":"reasoning_content"},{"type":"toolCall","id":"","name":"exec","arguments":{"command":"Out-File -FilePath a.txt"}} ... {"type":"thinking","thinking":"Good, that should have created an empty file. Let me verify it exists.","thinkingSignature":"reasoning_content"}这里我们可以窥探到全貌，工作流程是：需求输入-云端模型处理-返回内容和指令-执行指令-根据指令结果判断正误。AI还是那个AI，那个接收不定长文本，输出不定长文本的AI，只是OpenClaw对输出的结果进行了处理，提取出了工具，然后在终端执行。联想时刻：早已有之的思想文章写到这里，我突然想起来了刚进实验室做科研的时候，读到的一篇文章——Graph-ToolFormer: To Empower LLMs with Graph Reasoning Ability via Prompt Augmented by ChatGPT，那个时候笔者还在进行知识图谱-大模型的相关研究。那个时候就有一种借助知识图谱辅助思考的方法，给定知识图谱，让大模型输出相应的SPARQL，然后在知识图谱上检索，把内容塞到模型的context中，进行推理。这部分没什么意思，暂且按下不表。小小实战：做一个自己的AI Agent这个技术难度并不大，我们用Qwen-0.6B模型作为基础模型，做一个终端小Agent。首先clone模型：git lfs install git clone https://huggingface.co/Qwen/Qwen3-0.6B然后光速搓一个加载模型的demo：from transformers import AutoTokenizer, AutoModelForCausalLM import argparse import torch import os def get_answer(input_sent: str, tokenizer, model): input_dict = [ {"role": "user", "content": input_sent} ] text = tokenizer.apply_chat_template( input_dict, tokenize=False, add_generation_prompt=True, enable_thinking=True ) inputs = tokenizer([text], return_tensors="pt").to(model.device) generated_ids = model.generate( **inputs, max_new_tokens=4096, ) output_ids = generated_ids[0][len(inputs.input_ids[0]):].tolist() try: index = len(output_ids) - output_ids[::-1].index(151668) except ValueError: index = 0 content = tokenizer.decode(output_ids[index:], skip_special_tokens=True).strip("\n") return content if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--model_name_or_path", "-m", type=str, default=os.path.join("..", "Qwen3-0.6B")) args = parser.parse_args() tokenizer = AutoTokenizer.from_pretrained(args.model_name_or_path) model = AutoModelForCausalLM.from_pretrained( args.model_name_or_path, dtype=torch.bfloat16, device_map="auto", ) while True: print("Please input your question:") input_sent = input() print(get_answer(input_sent, tokenizer, model))到这里，我们算是极其粗糙地模拟好了「需求输入-云端模型处理-返回内容和指令」的过程：Please input your question: What should I do to build a building? Building a building involves several key steps and considerations. Here’s a structured approach: 1. **Define the Purpose**: Determine the type of building (e.g., home, office, commercial) and its intended use. 2. **Research and Planning**: Study the location, climate, and zoning regulations to ensure compliance. 3. **Design and Planning**: Create a detailed blueprint, including architectural design, materials, and layout. 4. **Budget and Materials**: Plan your budget and select suitable materials based on cost and durability. 5. **Purchase and Secure Funding**: Secure permits, contracts, and funding (e.g., loans or grants). 6. **Construction**: Begin construction, ensuring quality and adherence to design specifications. 7. **Testing and Completion**: Test the building’s functionality and complete the project. **Key Tips**: - Avoid common mistakes like ignoring permits or choosing inferior materials. - Prioritize safety and sustainability in design. Let me know if you need guidance for a specific project!在这之后，我们要编写一个执行指令的过程，执行指令分为两部：从输出中提取出要执行的指令和执行指令。为了能够高效地提取出指令，我们需要和LLM做约定，通常需要通过prompt约束LLM输出的格式：def get_answer(input_sent: str, tokenizer, model): input_dict = [ {"role": "system", "content": "You are a helpful assistant. You need to generate the result as json format, do not generate any other text. An acceptable example is {\"command\": ""}."}, {"role": "user", "content":input_sent} ] ...然后写一个比较粗糙的提取函数：def filter_out_json_part_of_answer(raw_answer: str): if "{" not in raw_answer or "}" not in raw_answer: return "", False start_index = raw_answer.find("{") end_index = raw_answer.find("}") + 1 try: command_dict = json.loads(raw_answer[start_index:end_index]) except json.JSONDecodeError: return "", False return list(command_dict.values())[0], True主函数中：... while True: print("Please input your question:") input_sent = input() while True: raw_answer = get_answer(input_sent, tokenizer, model) answer, is_json = filter_out_json_part_of_answer(raw_answer) if is_json: print(answer) break else: print("Error occurs!")实测：Please input your question: Create a text file in Bash touch filename.txt然后我们直接执行这个命令即可，在主函数中：... while True: print("Please input your question:") input_sent = input() while True: raw_answer = get_answer(input_sent, tokenizer, model) answer, is_json = filter_out_json_part_of_answer(raw_answer) if is_json: print(answer) break else: print("Error occurs!") try: os.system(answer) except Exception as e: print(e)即可。实际上AI Agent的思想是很简单的，在大模型输出的文本层和实际的工具层之间，做了一个转接，从而使大模型能够利用工具。末尾浅思AI Agent这个技术的痛点就在于——它依赖于云端的AI模型提供商。离了提供商，这些Agent基本上就用不了了。这就引出了另一个问题——AI目前还没有办法很好地在民用级显卡上部署。民用级显卡通常也就4G到8G左右，除去日常使用，剩下的显存根本不够7-24部署一个大模型的。而利用云端，则会带来大量的token开销。AI Agent缩短了「笨蛋和聪明人之间的距离」，而代价就是「笨蛋要花更多的钱」。等什么时候，AI能够不这么吃算力，AI Agent就会真正成为一个颠覆性的技术，将彻底改变世界。技术上讲，AI Agent也面临着一个问题，这也是大模型的问题——Context Rot，随着Context不断增大，Model的Performance会降低，如何让模型保有长期记忆，也是很重要的一点。现在的解决方案有向量召回、Skills等方法，这些方法各有利弊——或许我会出一期新的RethinkAI讨论讨论。
引子大模型的幻觉问题一直是AI领域的痛点之一，如何去解决这个问题？两个比较简单的方法，分别是“开卷考试”和“开颅手术”。本篇就记录一些自己的学习笔记和思考。RAG所谓RAG，就是Retrieve and Generate的简称，即检索后生成技术。其实RAG本身就是一种类似于搜索引擎的技术，既然大语言模型的上下文长度是有限的，那么我们就把一些外部的知识库进行切分，切分成一系列知识，然后让大模型针对问题有需取用即可。这就是一种开卷考试的方法。在前几年，RAG也是研究的热点之一。不过这并不是今天讨论的重点。在我看来，RAG就是这么简单且有效的技术之一。知识编辑如果说RAG是开卷考试，那么知识编辑就是一种开颅手术的方法。知识编辑的问题建模实际上是一个极其简单的公式，假设所有的知识可以建模为三元组的形式，如$(h,r,t)$，那么知识编辑的目的是将某些知识进行更新，更新为$(h,r,t’)$。知识编辑的方法有三大类：外部记忆法、全局优化法以及局部修改法。其中，外部记忆法是指保持模型原有的权重不变，通过外部的结构将知识与大模型结合起来。这种外部的结构可以是一些额外的模型，比如FFN，也可以是外部的知识库。全局优化法是指，对于模型的全部参数进行一次整体的更新，从而达到更新知识的目的。但是，和SFT和全参数微调不同的是，这类方法的训练过程通常被妥善设计，从而不会影响原有的知识。局部修改法的第一步是识别出某一特定知识所在的神经元/权重，然后通过一些方法进行修改。从而达到修改知识的目的。和RAG不同的是，知识编辑不仅仅包括RAG的类似方法（不完全一致），也包括对模型内部进行处理的一些方法。对于外部记忆法来说，比较有代表性的例子是MeLLo，它的工作流程很好地体现了外部记忆法的模式，即通过冻结大语言模型的参数，一切的操作都在外部进行。这个方法主要是为了解决多跳QA问题的。工作流程如下：将一个多跳的QA问题转化为若干子问题。让大语言模型回答这些子问题，提供一个暂时的答案。通过外部知识库检索相关的知识，确定这些答案的正确性。如果答案与外部的知识有冲突，就重新回答。这类方法在我看来，和RAG并没有特别大的差异，唯一的不同之处在于，这些方法通常是选择性地调用外部知识，而非通常RAG方法的一直调用。对于全局优化法来说，比较有代表性的是RECT，该模型通过正则化，提取出每一次修改最大的参数，进行参数更新。对于局部修改法来说，比较有代表性的方法是MEMITCSK。作者观察大模型每次预测的结果，通过一系列结构获取每次预测的过程中，对结果产生最大影响的权重，随后进行修改。也有一些方法是对模型结构进行修改，比如融合门控单元，通过某一阈值决定走外部记忆模块还是原先记忆模块（WISE）。总结这两个方法都能在一定程度上缓解大语言模型的幻觉问题，但是这两个方法都没有解决这个问题。除此之外，这两个方法也没有办法解释为什么会有幻觉现象。在这之外，这两者的效果依赖于子任务和人工提供知识的匹配程度，在一定程度上也是受限制的。
引子在Transformer架构流行之前，自然语言的处理，尤其是NER（命名实体识别）等任务，通常是利用传统的序列模型处理的，比如隐马尔可夫模型、条件随机场等。实际上，这些模型的假设通常是——模型的未来状态只与当前状态有关。这类模型的发展以HMM（隐马尔可夫模型）为起始，经过MEMM（最大熵马尔可夫模型），到CRF（条件随机场）为止。这些模型在现在已经很少使用了，不过依然有着意义。本篇就来探讨一下HMM和MEMM这两个模型。CRF放在新一期讲。马尔可夫假设假设我们有一系列的随机状态发生的概率$P(X_t=x_k)$，其中$t$表示不同的时刻，$X_t=x_k$表示$t$时刻的随机变量取值为$x_k$，那么马尔可夫假设就是这样一个式子：$$ P(X_t=x_k|X_1=x_{k1}, X_2=x_{k2},\cdots,X_{t-1}=x_{kt-1})=P(X_t=x_k|X_{t-1}=x_{kt-1}) $$用语言解释，就是，下一时刻的随机状态的取值，只与当前时刻有关。基础设定我们还是回到自然语言这个设定上来，很多时候，我们需要对序列做标注任务，比如词性的标注、分词。这次我们就用分词任务来说明。这次就用B和I两类字符做分词。所谓B就是一个词的开始（Beginning），也就是提示要分割的地方，在标注B的字符前面分割，而I就是表示“处在一个词的内部”（Intermediate），也就是这个字符前面无需分割，举个例子，对于“我爱吃薯片”这句话来说，如果这五个字符分别被标注为BBIBI，那么最后的切分方式就是“/我/爱吃/薯片”。HMM：隐马尔可夫模型分词显然是在一句话内生效的，那么我们可以把每个字符设为$C_i$，每个字符的分割方式设为$S_i$，其中，$i$表示在序列的位置（index），那么一个句子的分割方式的概率就是$P(S_1S_2\cdots S_n)$，这很好理解。如果我们认定马尔可夫假设，并且认定每个字符的分割方式与和当前字符有关，而当前字符的概率与前一个字符有关，那么我们由浅入深，先考虑前两个字符分割方式与整个句子的联合概率：$$ P(S_1S_2C_1C_2)=P(C_1)P(S_1|C_1)P(C_2|C_1)P(S_2|C_2) $$前三个字符：$$ P(S_1S_2S_3C_1C_2C_3)=P(S_1|C_1)P(C_2|C_1)P(S_2|C_2)P(C_3|C_2)P(S_3|C_3) $$不难看出，每多考虑一个字符，就会增加两项——$P(C_{t}|C_{t-1})$和$P(S_t|C_t)$，这两项分别是在前一个字符的影响下，当前字符的概率；以及在当前字符的影响下，当前字符的分割方式概率。那么我们就可以建模成这样一个网络：其中，我们称字符之间的概率为转移概率，而字符到切割方式的概率为发射概率。当然，之所以HMM是“隐”马尔可夫模型，就表明，实际中，$C$不一定就是字符，可能是一些隐信息。总而言之，HMM依靠两个极其强的假设：当前字符的出现只与前一个字符有关。当前字符的分割方式只与当前字符有关。MEMM：最大熵马尔可夫模型通过HMM不难看出，“当前字符的分割方式只与当前字符有关“这一假设未免过于强了，举例来说，”我是南京市长“和”这是南京市长江大桥“两句中，”长“字的切割方式是与上下文有关联的，所以，一个字符的分割方式不仅与当前字符有关，也与之前的字符或者标注方式有关。MEMM就是为了解决这个问题的，MEMM对条件概率进行建模：$$ P(S_1S_2\cdots S_n|C_1C_2\cdots C_n)=\prod P(S_t|S_{t-1},C_t) $$如式子所示，概率的计算考虑到了序列的前一个元素的分割方式对于当前字符的分割方式影响，这个模型的结构实际上是这样的：当然，到这里可能会有一个疑问：为什么要求”当前字符的概率“而非”当前字符生成的概率“？我们往下看。累乘的每一项的计算方法是：$$ P(S_t|S_{t-1},C_t)=\frac{\exp{\sum_{i} \lambda_if_i(C_t,S_t)}}{Z(S_{t-1}, C_{t})} $$其中，$f_i$是自设置的特征函数，数量可以自行确定，而$\lambda_i$则是参数，可以理解为权重。分母则是一个归一化的因子，因为概率的取值为$[0,1]$，所以需要一个归一化项，确保和为1。这个公式是来源于最大熵原理的，所以该模型也叫做MEMM。
前言计算机没有办法直接理解、存储自然语言。在AI之外的领域，字符串操作通常是以各种字符编码来进行运算的，比如ASCII码、UTF-8编码等等。在AI中，我们也需要一种利用计算机技术，将自然语言转化为计算机能够理解的表示的方法。通常，我们会将自然语言进行两步操作——分词和词嵌入。其中，分词是指将一个句子分成若干基本单元，词嵌入则是将分割到的基本单元转化为向量。分词所谓分词，就是将句子变成若干基本单元，用作输入的句子处理以及输出。分词算法的要求是：高效。算法不能太复杂。可拓展性。要很好地兼容没有在训练数据中出现的词。我们首先从英语分词入手，看看我们究竟要解决什么问题。想象一下，我们要对语言进行建模，就要处理一个篇章非常大的文段。我们知道，英语是依靠空格进行单词间分隔的，那么我们依靠空格对单词进行分割如何？这是一种极其简单、易于实现的分割方法。但是，这个分词方法有很强的扩展性吗？并没有，除此之外，这个方法有个很致命的问题——同一个单词有很多种表示。比如这一句话中——“The show shows some significant things, showing the show shows lots of things.”——出现了show、shows、showing三种表示，但是它们都是由同一个词变化而来的。这种分词方法会极大扩张词表的大小，造成单词表示的膨胀。除此之外，这种分词方法会导致模型永远没法生成没见过的单词，用专业术语来说，就是没法很好处理OOV（Out-Of-Vocabulary）的情况。针对一词多表示的问题，研究者们提出了一种新的分词算法——Subword分词。这个算法对于一个单词，会将之拆分为前缀、子词和后缀，比如show、shows、showing这三个单词在词表中，会变成show、-s、-ing三个基本单元。这个分词方法导师很好解决了一次多表示的问题，但是，对于OOV问题还是束手无策的。那么，有人就会说了，为什么一定要用分“词”呢？像计算机一样，直接用字符表示是否可以？这便是character-level tokenization。这个方法能够很好地限制词表的大小，也能够解决OOV问题——没有一个单词不是字母组成的。一切都好，对吧？实际上，在一段时间之前，我也觉得这个方法非常好。不过，我们想一下，Transformer模型的输出是逐字符输出的，根据前面的内容预测后面的内容。如果用字符表示，那么一个文段就会占据过多的空间，没有办法很好地生成长文段。网上很多资料说，character-level tokenization没办法学习到字之间的信息——全是狗屁！只要输入够多，字之间的关系很好学习。我们亟需一个折衷的分词方案。这个方案就是BPE——Byte-Pair Encoding。这个算法的流程是这样的：在每个单词后面增加一个分隔符。用Character-level Tokenization构建一个初始词表。计算频率最高的字符模式，将新字符融入词表。重复3，直到下一最高频字符模式频率为1。举个例子，我们的语料是——“The thread shows the meaning.”，首先我们将之处理为“The/thread/shows/the/meaning”。首先构建起最基础的词表：词表频率t3h4e4r1a2d1s2o1w1m1n2i1g1随后寻找到最高频的字符模式：词表频率t3-3=0h4-3=1e4r1a2d1s2o1w1m1n2i1g1th3然后：词表频率h1e4-2-2=0r1a2-2=0d1s2o1w1m1n2i1g1th3-2=1the2ea2这便是我们的词表了：词表频率h1r1d1s2o1w1m1n2i1g1th1the2ea2这个方法在char-level和word-level中找到了一个平衡点，只要输入语料够大，就能够学到语言的基础模式，不仅能够规避OOV问题，也能够让词表不至于过于庞大。当然，这个分词方法对于英语非常有效，但是对于其他语言则无能为力。于是，后来出现了一个新的分词方法——BBPE（Byte-level BPE），通过在文本的Unicode编码基础上进行BPE，支持了几乎全部的语言以及Emoji。词嵌入通过分词，我们将语料转化为了一些基础单元，现在我们要做的是，将这些基本单元转化为向量形式，这些向量就被叫做词向量。一个很显然的嵌入方法是one-hot编码。比如词表中有3个词：我、爱、你。那么这三个词的向量表示为：$(1,0,0),(0,1,0),(0,0,1)$。one-hot就是指向量中只有1位是1，其它都是0。这种编码的好处是简单、方便，但是其问题也显而易见——这种编码会占用大量的空间。比如，如果一个词表很大，那么处理它的模型的参数也就很多，我们需要一种更稠密的嵌入方式。当然，one-hot编码也完全忽视了单词的语义信息。在词向量空间中，含义相近的单词，它们的向量表示也需要尽可能地靠近。一个很重要的嵌入方式是word2vec。这种嵌入方式不仅让词向量维度变低，也能够照顾到词向量的语义信息。word2vec的两类模型是Skip-gram和CBOW（Continuous Bag-of-Words，连续词袋）。word2vec本质上是一个FFN，一个输入层为$V$维，隐藏层为$H$维，输出层为$V$维的FFN，其中$V$是词表大小，$H$是词向量目标维度。Skip-gram模型，是借助当前位置的单个词预测周边单词，从而获得当前位置单词词向量的过程。首先，我们将单词的one-hot表示输入FFN，通过隐藏层得到词向量，然后在输出层经过Softmax函数的运算，获得$V$维向量，其中每个数字代表该数字下标所对应单词是当前单词的上下文的概率。比如输出向量的第$i$个数字是0.5，那么就代表词表中第$i$个单词是当前单词上下文的概率为0.5。CBOW模型，是借助周边单词预测当前位置单词，从而获得当前位置单词词向量的过程。首先，将周边单词都通过隐藏层进行运算，将结果相加，通过输出层进行运算，经过Softmax得到一个向量，这个向量的最大概率下标就是预测的单词。
前言作为NLP领域的最重要模型之一，Transformer有着强大的性能。为什么要引入TransformerTransformer这一模型与称呼，来源于2017年的一篇论文——《Attention is All You Need》。在2017年左右，NLP领域的代表性模型无非是RNN系列和CNN系列。RNN系列的模型的计算过程是以迭代整个序列为基础的，想要在其基础上进行并行计算是很困难的，处理长序列所需的时间很多。除此之外，RNN也具有梯度爆炸/梯度消失现象，难以捕捉长距离的词元依赖关系。CNN系列的模型固然可以很好捕捉长序列信息，但其有一个十分致命的缺点，即其卷积核的大小是固定的，难以自适应捕捉不同序列的语义距离。对于简单句来说，卷积核的大小可能太大了——而相同大小的卷积核，对于长难句来说，又太小。Transformer架构就解决了这两类模型的问题——通过注意力机制解决了难以并行计算、梯度爆炸/消失的问题，通过注意力机制中的权重设置，能够自适应捕捉不同序列的语义距离。简单来说，就是将RNN的迭代过程，变成了矩阵的乘法运算；将CNN的卷积过程，化作了注意力机制模块中，矩阵的参数。除此之外，Transformer也具有架构统一、易于拓展的优点。Transformer的首要任务其实和RNN和CNN无异，那就是学习到语言的特征。注意力机制广义的注意力机制，其本质就是矩阵的乘法，其通用结构是：$$ \text{Score}([x_1,x_2,\cdots,x_n])=\mathbf{W}[x_1,x_2,\cdots,x_n] $$其中，$\mathbf{W}$是注意力矩阵，是一个可学习的参数。通过数据集训练这个矩阵，从而得到向量中每一个元素的重要性。Transformer的结构Transformer的结构如下图所示：嵌入模块首先，这里的Input Embedding和Output Embedding都是“词嵌入”的技术，即将自然语言转化为向量的过程。这一步是NLP最基础的技术之一，因为计算机没有办法直接理解自然语言，需要将之转化为数字表示才可理解。有关这个技术，我会在后续的文章中讨论。编码器我们从左往右看，左边的灰色框框被称作“编码器”（Encoder）。其目的是将自然语言的向量表示，转化为模型内部的表示。这个模块接收一个输入，吐出一个输出。实际上，第一个编码器模块的输入就是自然语言的向量。当向量进入编码器后，首先经过一个多头注意力机制模块（Multi-Head Attention Block）。多头注意力机制模块，实际上就是若干个注意力机制模块进行拼接。每一个注意力机制模块都应用下面的公式进行计算：$$ \text{Attention}(\mathbf{Q},\mathbf{K},\mathbf{V})=\text{Softmax}(\frac{\mathbf{Q}\mathbf{K}^T}{\sqrt{d_k}})\mathbf{V} $$我们按照每一部分字母来解释。首先，$\mathbf{Q},\mathbf{K},\mathbf{V}$是由输入$\mathbf{I}$经过运算得到的矩阵：$$ \mathbf{Q}=\mathbf{W}_Q \mathbf{I},\mathbf{Q}\in\mathbb{R}^{n\times d_k} $$$$ \mathbf{K}=\mathbf{W}_K \mathbf{I},\mathbf{K}\in\mathbb{R}^{m\times d_k} $$$$ \mathbf{V}=\mathbf{W}_V \mathbf{I},\mathbf{V}\in\mathbb{R}^{m\times d_v} $$这三个字母分别代表query、key和value，是作者借用的搜索领域的名词。我们不妨考虑一下搜索引擎是怎么运作的，在搜索引擎中输入一个关键字（query），然后蹦出来一堆页面的标题（key），找到符合心意的标题，点进去，就得到了相应的内容（value）。回到我们的任务上，我们的任务是对语言进行建模，给定一个自然语言的句子，我们不妨让key和这个句子一致，那么我们的内容也需要和key一致。输入是什么，输出就是什么，这样才能捕捉到语言的内部规律——当捕捉到语言内部规律时，给定一个自然语言句子，其输出必须要和源端一致，否则就是偏差了。输入所乘的矩阵，实际上是一个投影操作，将输入分别投影到query、key和value空间。我们观察Softmax函数的分母，表面上看是一个矩阵相乘，实际上，这是一个求向量间相似度的操作，还记得余弦相似度怎么算吗：$$ \text{CosSim}(\vec{a},\vec{b})=\frac{ab^T}{||a||||b||} $$分母其实也是这个含义，根据矩阵乘法的规则，我们知道，结果矩阵中的每一个元素，都是乘子两个矩阵的行与列的积，实际上，分子所计算的，就是query矩阵和key矩阵中每一行元素之间的相似度。分子结果矩阵的第一行第二列元素，就是query矩阵的第一行向量和key矩阵第二行向量之间的相似度。至于分母，其实它就是一个scaling factor，用于控制元素的缩放，求每一行向量的开销过大，不妨用一个现有的常数做scaling。这个结果做一个Softmax，实际上是一个normalization（归一化）操作，将数据都缩放到0-1这个量级，便于统一处理。因为key和value是一一对应的关系，最后的结果和value相乘，就得到了每一个位置上，每个value单元的重要性。还是按照矩阵乘法的规则，Softmax的函数结果矩阵，第一行便是query的第一行向量和所有key之间的相似度关系，这个时候和value的列向量相乘，就得到了这个相似度关系权重下，每一个value单元的权重。这是一个注意力机制模块的工作方法，多头注意力机制就是并行处理很多很多个注意力机制模块，每个模块的参数都是独立的，通过不同的参数学习到不同的注意力信息。在得到了这个注意力矩阵后，我们发现，这个结果要经历一个Add and Norm层，广义来看，这是一个十分著名的结构——ResNet：$$ \text{Output}(x)=x+f(x) $$这一结构能够缓解梯度爆炸和梯度消失的现象，其中的Norm其实上是Layer Norm，层归一化操作。对于每一个头的每一个输出矩阵，都按照行向量进行归一化，这能够稳定同一value下，不同元素所占的比重。Add and Norm层，实际上就是为了稳定输出矩阵中，每一行的比重。在这之后，进入了一个FFN模块，对刚刚得到的矩阵进行进一步的信息提取，在实际的模型架构中，这一模块先做了升维操作，随后做了降维操作，先提供更丰富的信息表达程度，然后进行凝练——这就像是一个先把书读厚，再读薄的过程。解码器当输入进入层层Encoder，最终得到一个输出之后，它会被输入解码器链中。具体来说，这个输出会输入解码器链的每一层。Decoder的输出，除了这个编码器计算出来的结果外，还有一个右移的输出值，这个输出值——用机器翻译来说——便是目标语言的句子了。这里，这个目标端的句子首先进入了一个掩码注意力机制模块中。何为掩码，这可得好好解释一番。Transformer的预测过程是一个通过序列$[x_1,x_2,\cdots,x_n]$预测$[x_1,x_2,\cdots,x_n,x_{n+1}]$的过程。这就要求，每一个词元的状态，只和它之前的词元有关。所以，在计算注意力的时候，需要和它前面的矩阵计算注意力。所以，注意力机制计算的时候，需要用到掩码。随后，将这个注意力矩阵作为实际的内容——value矩阵，按照之前的方法运算即可。位置编码到这里，结束了吗？并没有，我们剩下一个模块还没有介绍——位置编码模块（Positional Encoding）。因为到目前为止，我们没有办法捕捉到位置信息，也就是说，交换两个词元的位置，最终的结果似乎并没有太大变化。“我爱任天堂”和“任天堂爱我”，不加入位置编码，没法区分这两个句子的差异。在原论文中，所使用的位置编码是：$$ \text{PE}_{(\text{pos}, 2i)}=\sin(\frac{\text{pos}}{10000^{\frac{2i}{d_{model}}}}) $$$$ \text{PE}_{(\text{pos}, 2i+1)}=\cos(\frac{\text{pos}}{10000^{\frac{2i}{d_{model}}}}) $$其中，$\text{pos}$是位置，$i$是维度数。这样的好处是，可以获得相对的位置信息，这是因为，正余弦函数的加和性质：$$ \sin(a+b)=\sin(a)\cos(b)+\cos(a)\sin(b) $$$$ \cos(a+b)=\cos(a)\cos(b)-\sin(a)\sin(b) $$后记着实花了我一大番功夫缕清各种细节。不得不说，Transformer算是我心目中最伟大的模型之一了，简单、有效。
前言虽说我是搞NLP的，但是老实说，这些基础的Seq2Seq模型我也没有仔细思考过。序列到序列模型引入自然语言是以序列表示的。和普通的点到点任务（比如加法器）不同，自然语言的序列，每个元素之间是有关系的。什么意思？比如这句话：“我是一个学生”，其中“我”和“学生”显然是相关的，“是”表达了一个关系，表达的是“我”和“学生”之间的关系。为了处理这种元素彼此之间有依赖性的格式，相比于使用单纯的神经元层拼接，不得不想出一种新的模型架构。这种架构需要满足几个条件：能够捕捉到序列之间的关系。能够接受变长输入与输出。第一点很好理解，第二点需要解释一下。在语言处理领域，这一点无比重要，因为语言是变长的，所以需要能够接受变长输入。除此之外，输出也要满足可变长性。那么为什么需要一个“新的”模型架构呢？因为传统的模型拼接，往往每一层的参数是确定的，输出层的参数也是确定的，这并不利于处理序列信息。这便是序列到序列任务。直觉的一闪：RNN对于字符串的处理，容易想到的一个算法便是“滑窗算法”，这个算法常用于寻找字符串的重复元素。没有错！序列是变长的，但是，我们可以将序列拆成等长的小块，然后进行分块处理。处理完每一个小块后，输出两个东西，一个是呈现给用户的、作为结果的输出；另一个是传递给下一时间步，处理下一个小块的信息。那么，既然输入是定长的，就可以依靠传统的模型进行处理啦！这便是最基本的RNN（循环神经网络）的思想，其架构图如下：RNN块内部的结构为：值得注意的是，RNN是应用同一个RNN模块对序列进行处理，从而获得整个序列的信息，这表明，只有一套参数。RNN Block的公式可以表达为：$$ \text{Process}(\mathbf{I},\mathbf{P})=f(\mathbf{W}_1\mathbf{I}+\mathbf{W}_2\mathbf{P}+\mathbf{B}_1) $$$$ \text{Output}(\mathbf{I},\mathbf{P})=g(\mathbf{W}_3\text{Process}(\mathbf{I}, \mathbf{P})+\mathbf{B}_2) $$那么，我们来看看RNN的梯度该如何计算，以Process Layer的权重举例子：$$ \frac{\partial L}{\partial \mathbf{W}_2}=\frac{\partial L}{\partial f}\frac{\partial f}{\partial \mathbf{W}_2} $$这个时候，需要注意：$$ \frac{\partial f}{\partial \mathbf{W}_2} = \frac{\partial f}{\partial \mathbf{W}_2\mathbf{P}}\frac{\partial \mathbf{W}_2\mathbf{P}}{\partial \mathbf{W}_2} $$其中，由于RNN是一个时序模型，所以：$$ \frac{\partial \mathbf{W}_2\mathbf{P}}{\partial \mathbf{W}_2}=\mathbf{P}+\mathbf{W}_2\frac{\partial \mathbf{P}}{\partial \mathbf{W}_2} $$这就完了吗？并没有！因为：$$ \frac{\partial \mathbf{P}}{\partial \mathbf{W}_2} = \frac{\partial \mathbf{P}}{\partial \mathbf{P}_{-1}}\frac{\partial \mathbf{P}_{-1}}{\partial \mathbf{W}_2} $$其中，$\mathbf{P}_{-1}$是指上一个时间步的Process Layer结果。从这个公式可以看出来，梯度计算的结果，和之前所有的状态都有关。如果$\frac{\partial \mathbf{P}}{\partial \mathbf{P}_{-1}}$为一个大于1的数，那么会造成梯度的指数级增加，如果小于1，就会造成指数级减小，因为：$$ \lim_{x\to \infty}1.1^x=\infty $$$$ \lim_{x\to\infty}0.9^x=0 $$这便是RNN的梯度爆炸和梯度消失现象，这两个现象注定了RNN对于长序列的处理是有问题的。不过，我们也可以用一些方法缓解这个问题，我们通常应用：$$ f(x)=\tanh(x) $$假设发生了梯度爆炸，那么：$$ \frac{\partial \text{Process}}{\partial \text{Process}_{-1}} = (1-\text{Process}^2)\mathbf{W}_2 $$值得注意的是，当$\mathbf{W}_{2}$很大的时候，根据$\tanh$函数的性质，上面这个式子反而很小，反之亦然，这就在一定程度上避免了RNN的梯度爆炸和梯度消失现象。一点改进：LSTM为了改进这两个问题，研究者们研究出了一个新的架构——LSTM，其结构如下：这是一个比较复杂的结构，其公式为：$$ f_t=\sigma(\mathbf{W}_fx_t+\mathbf{U}_fh_{t-1}) $$$$ i_t=\sigma(\mathbf{W}_ix_t+\mathbf{U}_ih_{t-1}) $$$$ o_t = \sigma(\mathbf{W}_ox_t+\mathbf{U}_oh_{t-1}) $$$$ \hat{c}_t = \tanh(\mathbf{W}_cx_t+\mathbf{U}_ch_{t-1}) $$$$ c_t=f_t\circ c_{t-1}+i_t \circ \hat{c}_t $$$$ h_t=o_t\circ \tanh(c_t) $$其中，$f_t,i_t,o_t$我们称作遗忘门、输入门和输出门。我们关注于对前面信息的处理：$$ \frac{\partial c_t}{\partial c_{t-1}}=f_t+c_{t-1}\frac{\partial f_t}{\partial c_{t-1}}+\hat{c}_t\frac{\partial i_t}{\partial c_{t-1}}+i_t\frac{\partial \hat{c}_t}{\partial c_{t-1}} $$我们知道，$\tanh$和$\text{Sigmoid}$函数的求导格式差不多，都能满足梯度很大时，削弱影响。统一的问题这种模型由于需要循环处理，所以没有办法进行并行计算，也就难以应用GPU进行优化。
前言本篇主要记录激活函数、基本的神经元模型以及反向传播。激活函数的引入与神经元模型在神经网络中，一个神经元象征着这样的运算:$$ \text{BaseNeuron}(\mathbf{I})=\mathbf{WI}+\mathbf{B} $$其中，$\mathbf{I}$代表输入的矩阵（本文只讨论二维情况），$\mathbf{W}$和$\mathbf{B}$是神经网络的两个参数——权重和偏置。当然只依靠$\text{BaseNeuron}$是不够的，因为多层这样的结构和一层无异，比如，两层的计算可以变成：$$ \text{BaseNeuron}_1(\text{BaseNeuron}_2(\mathbf{I}))=\mathbf{W}_1(\mathbf{W}_2\mathbf{I}+\mathbf{B}_2)+\mathbf{B_1} $$根据矩阵乘法的分配律，上式可变成：$$ \text{BaseNeuron}_1(\text{BaseNeuron}_2(\mathbf{I}))=\mathbf{W}_1\mathbf{W}_2\mathbf{I}+\mathbf{W}_1\mathbf{B}_2+\mathbf{B}_1 $$这依然符合$\text{BaseNeuron}$的基本形式，两层的计算，可以通过一个新的层达成目的，新层的权重为$\mathbf{W}_1\mathbf{W}_2$，偏置为$\mathbf{W}_1\mathbf{B}_2+\mathbf{B}_1$即可。这背后的原理是：“线性变换的线性变换依然是一个线性变换”，这一点可以通过线性变换的概念进行证明——因为线性变换是在一个线性空间内做的运算，没有办法“逃出”这个空间。在现实世界中，并非所有的事物都可以用线性变换来解释，比如线性变换没有办法解释异或，也没有办法拟合正余弦等函数。这个时候，我们就需要一个工具，来创造出非线性变换——这便是激活函数。借用这个工具，我们的神经元模型变成了：$$ \text{Neuron}(\mathbf{I})=f(\mathbf{WI}+\mathbf{B}) $$其中，$f(\cdot)$是一个非线性的函数，是我们的激活函数。其作用很简单——向一个线性模型中引入非线性。只要是非线性函数，就可以用作激活函数，只是具体的效果有待考量而已。反向传播模型的层数可以很多，那么，在梯度下降训练过程中，就需要一种方法来计算出每一层的梯度。比如我们的模型有两层：$$ \text{Model}(\mathbf{I})=\text{Neuron}_2\text{Neuron}_1(\mathbf{I})=f_2(\mathbf{W}_2f_1\mathbf{(\mathbf{W}_1\mathbf{I}+\mathbf{B}}_1)+\mathbf{B}_2) $$我们需要求取每一层的梯度，具体来说，是针对于权重和偏置的梯度。当然，只有输出还不够，按照梯度下降的思想，我们需要一个损失函数，求损失函数对每一个参数的导数，亦即$\frac{\partial{L}}{\partial \mathbf{W}_1}$之类。这里，我们要用到的工具是——链式法则。对于$\frac{\partial{L}}{\partial \mathbf{W}_2}$而言，它等于：$$ \frac{\partial{L}}{\partial \mathbf{W}_2}=\frac{\partial{L}}{\partial f_2}\frac{\partial{f_2}}{\partial \mathbf{W}_2} $$所以，我们可以从输出层向输入层逐层求导，因为，前面的层，需要后面层的参数做运算。反向传播就是这么简单的东西——先求出后面一层的全部参数值，然后通过链式法则进行运算。激活函数的几个例子一个好的激活函数需要满足几个条件：形式简单。求导方便。其中，这两点都是因为，我们常用的训练架构，依然是基于梯度下降的。一个形式简单的激活函数，不仅方便理解、分析和编码，也能让计算速度变快；一个求导方便的激活函数，就利于梯度下降。比较常用的激活函数是Sigmoid函数，其公式为：$$ \text{Sigmoid}(x)=\frac{1}{1+e^{-x}} $$易知其导数为：$$ S'(x)=S(x)(1-S(x)) $$其中，$S(x)$即$\text{Sigmoid}(x)$。这个函数的值域为$[0,1]$，常用于二分类任务输出层。因为二分类通常以0、1为标签。对于多分类来说，通常使用Softmax函数，其公式为：$$ \text{Softmax}(\mathbf{X})=\frac{e^{x_k}}{\sum_{i=1}^ne^{x_i}} $$其实，这个函数通常和交叉熵损失函数一起求导。交叉熵损失函数的公式为：$$ L(p,y) = -\sum_{i=1}^ny_iln(p_i) $$其中，$p,y$分别是预测值和真实值，其中，$p_i=\text{Softmax}(\mathbf{X}_i)$。如果求$\frac{\partial L}{\partial \mathbf{X}}$，那么：$$ \frac{\partial L}{\partial \mathbf{X}} = \frac{\partial L}{\partial \text{Softmax}}\frac{\partial \text{Softmax}}{\partial \mathbf{X}} $$又可以变为：$$ [\frac{\partial L}{\partial \mathbf{X}}]_k = [0,0,\cdots,-\frac{1}{\text{Softmax}(x_k)},\cdots0,0]\times \mathbf{M} $$那么，$\mathbf{M}$是个什么，其实，它是一个矩阵！而且是一个只有一列有元素的矩阵，具体的推导参考我的另一篇blog。这个矩阵只有第$k$列有元素，且：当$k\neq i$时，元素为$-a_ka_i$当$k=i$时，元素为$a_k-a_k^2$。其中，$a_i$为$\text{Softmax}(x_i)$，所以，最后我们得到了一个行向量，拼凑起来：$$ \frac{\partial L}{\partial \mathbf{X}} = \text{Softmax}(\mathbf{X})-\mathbf{Y} $$还有一类很常用的激活函数，是以$\text{ReLU}$为基础的：$$ \text{ReLU}(x)=\max(0,x) $$这个函数很简单，但是有一个问题，就是会出现一个无法求导的点——$(0,0)$。此外，这种函数由于在负数区，梯度始终为0，会出现一个神经元梯度为0，后面所有的梯度都为0的情况。在这个函数的基础上，有了很多改进，比如，将负数区从0变为$0.01x$，就可以规避梯度为0带来的影响。这些函数的作用，都是为线性系统带来非线性。