LLM 注意力机制可视化

查看原文 HN 讨论

文章摘要

一个能用鼠标悬停看懂的注意力可视化

Isham Faizal(HN 上的 ifz)在 2026 年 9 月 7 日发布了这个交互式演示。出发点很朴素:基于 Transformer 的大语言模型在生成阶段能够从此前任意一个 token 中提取信息,但它必须有所选择——如果每个 token 对生成的影响都一样,模型就没什么效果了。这个「决定某个 token 对下一个 token 影响多大」的过程,是可以被画出来的。

使用方式是:在生成出的文本上点击或悬停任意一个 token,页面就会把此前影响了这次生成的那些 token 高亮出来。

作者对简化程度的坦白

文章里有一段加星号的说明,作者把这个可视化的局限性讲得很清楚:「影响」这个词可能不完全准确,因为这个可视化被高度简化了。它计算的是注意力权重乘以 value 向量的模长,在所有注意力头上聚合,再跨所有层求和;这个数值被用来控制此前 token 的不透明度,最大值始终为 1,其余按比例插值。

作者说,为了把可视化压缩成「每个历史 token 一个数值」,大量信息被丢掉了;正因如此,他刚开始做的时候其实以为它可能根本不可读。但结果是它真的能呈现出一些有趣的模式。

三个被作者点名的观察

  1. 逐字复制的部分最抢眼。在默认的「办公室搬迁摘要」示例里,把鼠标悬停在被原样复制的地址和日期上,原始数据会非常明显地凸显出来,因为生成的 token 从源数据中取走了大量信息。

  2. 这解释了一件作者此前觉得反直觉的事。如果 LLM 是通过概率预测下一个 token 来工作的,它们为什么那么擅长「复制粘贴」?纯靠随机,难道不会迟早出错吗?但看到这个机制之后就明白了:它不是从某个有限的内部状态预测整个序列,而是因为可以访问全部历史 token,所以复制时只需决定从哪些历史 token 中取材,出错概率因此可以非常低。在「调试一个求平均值函数」的例子里,可以看到这个只有 6 亿参数的小模型能轻松复现整个 JS 函数,只改动本来要改的地方(不过它并没有能力自己找出问题,所以需要一些提示)。

  3. 信息融合的例子。在「办公室搬迁摘要」里悬停 “Existing access cards and phone numbers remain” 中的 “remain”,可以看到它同时从 “Existing employee access cards will work” 里的 “work” 和 “company phone numbers will stay the same” 里的 “stay the same” 中取材——它把两个短语的信息结合起来了,作者觉得这一点相当酷。

实现上遇到的三个坑

可视化本身是一个相当基础的 React 应用,用 Transformers.js 生成文本。但因为需要从模型里掏出更多数据,它不能用常规的生成循环,作者只好把生成循环重新写了一遍(他坦白说这部分是 vibe-code 出来的),以便追踪需要可视化的数值。

第二个坑是体积。即便用了较小的模型,它仍有数百 MB;让用户等它下载完才看到东西是行不通的,所以作者预生成了一批 prompt,可以即时加载查看。

第三个坑最技术性:可视化需要的一些中间值并没有被定义为模型输出。如果用 Python 的 ML 库,访问这些值仍然很容易;但 Transformers.js 用的是包含整个计算图的 .onnx 文件,模型加载和计算逻辑在 wasm 里实现,除了预定义输出之外没有简单的办法访问任何东西。作者最后写了一个小脚本,对 onnx 文件做了刚好够用的修改来暴露这些内部值——代价是不能再用常规的 .onnx 模型,为了支持浏览器内生成,他把一个被插桩过的模型单独上传到了自己的 Hugging Face 仓库并让应用指向那里。代码开源在 GitHub。

HN 评论精华

这条 Show HN 拿到 173 分但只有 32 条评论,讨论规模不大,质量却不低——作者 ifz 几乎回复了每一条实质性提问。

最重要的技术质疑

顶楼第一条就是 sva_ 的直接质疑:「我高度怀疑『向量模长大 = 影响大』这个过于简单的想法。」作者的回应相当坦诚:我不反对这个说法,我也确实加了整整一段说明;对我来说这更像一个漂亮的可视化,而不是可以用来解释 LLM 行为的东西——即便简化了很多,它仍然能展现一些有趣的模式。smallmancontrov 用一句「一分钱一分货」把想深入的人引向了 transformer-circuits 上关于 attention QK 的正式研究。

stared 追问具体公式:层和头这么多,要做出一个能契合人类直觉的选择其实很棘手,是加权平均吗?还是消融测试?作者给出了完整答案:非常简单,基本上就是 value 向量的模长,用 QK 点积加权,在所有注意力头和所有层上求和。他补充说,动手前他以为需要大量实验才能找到一个可理解的方案,结果这个简单计算已经能显示出一些模式了。stared 的回应是:「不错!有时候最简单的方法效果最好。」

改进建议

wopak 提出了一个尖锐的问题:你是否担心后层的注意力会仅仅因为前面层数更多、贡献项更多而被淹没?作者说他可能会加一些控制项来限制哪些层参与求和,也许能揭示更多模式——现在只有简单的相关性是可见的。nullbio 建议不要把所有注意力头加总,而是给不同的头用不同颜色,背景由堆叠的色层组成,这样就能看到每个头各自如何关注 token。

UX 反馈:一致指向「单步执行」

mncharity 写了一份认真的 UX 报告:他想逐步查看注意力状态,但动画跑得太快;于是尝试暂停——结果在 Chromium/Linux 上按暂停并不会暂停,而是把动画重置回播放前的状态,当前的注意力高亮消失了;再按播放又从头开始。他建议做成常规的「暂停即暂停、播放即续播」,或者提供 YouTube 那样的 0.25 倍速选项,甚至更好的是单步按钮。作者解释说,可以在任意词上悬停/点击/轻触来聚焦它,这才是他设想的主要使用方式,自动播放是后来才加的,只是为了让页面加载时更吸引眼球。Mugshelf 也独立指出了同一点:加载时自动播放是个真实的 UX 陷阱,「逐个 token 单步执行才是人们真正学会注意力机制的方式,单步控制比自动播放有用得多」。

教学价值

这是评论区最一致的正面评价。fuddle 说他读过好几本书、看过好多视频来了解注意力机制,现在他懂了,而这是他见过的最清晰的例子。MCP123 的反馈最实用:「太好了,谢谢,我周五就要教这个东西,时机完美。注意力机制很难解释得直观,因为那套加权方案对建立直觉帮助不大,有这样的可视化帮助很大。请别把这个页面挪走,我要链接它!」sshussain270 认为它比 Bertviz 更简单,适合理解推理过程和表层机制,对新学习者是很好的起点——作者回复说他此前不知道 Bertviz,看起来也很有意思。

一段关于注意力复杂度的科普(以及一次公开纠错)

ex-aws-dude 自称对 LLM 了解不多,问了一个基础问题:这是不是意味着计算量随上下文大小呈 N² 增长,因为每个 token 都要追踪它与所有其他 token 的关系?acedTrex 简短确认「完全自注意力的话是的」。TomatoCo 给出了一个更细致但有误的回答:「是,但因为有 KV cache 所以不是——由于 token 不会被未来的 token 修改,你可以缓存此前 token 的含义,这让整个上下文的总开销变成线性(或者说每次前向传播是常数)。」

libraryofbabel 做了一次很有价值的公开纠正:这是不正确的。即使有 KV cache,解码阶段生成每个额外 token 的前向传播计算量仍按 O(N) 增长(没有 KV cache 则是 O(N²));生成 N 个 token 的总开销带 cache 是 O(N²),不带是 O(N³)。之所以单次前向传播是 O(N),是因为新 token 仍然必须「关注」此前每一个 token,这需要 N 次点积——缓存的 key 向量与新位置的 query 向量之间。同时你还有 N 次内存读取(K 和 V),而这很可能才是真正的瓶颈(解码是内存受限的)。他给出的实用结论是:即便 cache 是热的,能避免长上下文就尽量避免——你会被按「cache read」token 多收钱。 TomatoCo 随后诚实地承认自己的理解可能有误。

scottcodie 提到你也可以从图像模型里挖注意力,「非常有趣」。lhk931122 提了一个此串未被解答的深层问题:注意力能否解释模型为什么这样输出?他说自己见过很多相关争论,并觉得图像的注意力可视化效果没那么好。