TurboQuant一月后:实现方案、争议与实用效果
本文原文为西班牙语,借助AI翻译。
本文原文为西班牙语,借助AI翻译。
NLTagger是苹果的情感分析API。它集成在iOS和macOS中,在设备本地运行,不需要服务器,只需三行代码即可使用。当你搜索"Swift情感分析"时,它是第一个出现的结果。 对于任何开发者在日常工作中会写的文本,它返回以下结果: 消息 NLTagger 实际情况 “delete the temp file” -0.8 中性指令 “ok” -0.8 中性确认 “run make test” -0.6 中性指令 “commit and push” -0.4 中性指令 “great job, thanks!” +1.0 正面(正确) “this is fucking broken” -1.0 负面(正确) 评分范围从-1.0(极其负面)到+1.0(极其正面)。根据苹果的评判,“delete the temp file"与"this is fucking broken"具有几乎相同的情感强度。而"ok”——英语中最中性的回应——得分是**-0.8**。 这些不是编造的数字。这些是在任何运行macOS 14+的Mac上都能重现的结果。 如何重现 import NaturalLanguage let tagger = NLTagger(tagSchemes: [.sentimentScore]) tagger.string = "delete the temp file" let (tag, _) = tagger.tag( at: tagger.string!.startIndex, unit: .paragraph, scheme: .sentimentScore ) print(tag?.rawValue ?? "nil") // "-0.8" 八行Swift代码。结果是确定性的:相同的文本在每次执行、每个设备上都产生相同的分数。这不是随机错误,而是系统性偏见。 ...
想象一下,你正在构建一个分析开发团队对话的应用程序。你希望检测团队的基调是否健康,或者是否存在压力的迹象。你决定使用Apple的NLTagger,因为它本来就有、免费、支持设备端运行,并且不需要服务器。三行代码搞定,开箱即用。 第一惊喜:句子“kill the process and restart the daemon”(杀掉进程并重启守护进程)的得分是**-0.6**。负面,几乎可以说是敌意满满。接着,“Fatal error in memory allocation”(内存分配出现致命错误)的得分是**-0.8**。而“crash report uploaded successfully”(崩溃报告成功上传)——这是字面上的好消息——得分却是**-0.4**。 你的团队健康状态检测工具刚刚判断出你的开发人员处于情绪崩溃的边缘。而实际上他们只是重启了一个服务而已。 问题所在:为另一个世界设计的模型 NLTagger的.sentimentScore方法会返回一个介于-1.0(非常负面)到1.0(非常正面)之间的值。Apple在iOS 13 / macOS 10.15中引入了它,它依赖于系统集成的设备端模型,无法个性化配置,也无法查看其训练数据。 简而言之:这是一个黑盒子,它给你一个数字并希望你信任它。 对于它最初设计的用途来说,它的表现是相当不错的:例如产品评论、社交媒体评论,以及消费领域的文本,在这些场景中,“horrible”意味着糟糕,“fantastic”意味着极好。这是它的领域。而当你将它应用到其他领域时,问题就出现了。 实验:技术文本 vs. 日常文本 我们来测试一下。这是纯Swift代码,可以直接运行在Playground中: import NaturalLanguage func sentiment(of text: String) -> Double { let tagger = NLTagger(tagSchemes: [.sentimentScore]) tagger.string = text let (tag, _) = tagger.tag( at: text.startIndex, unit: .paragraph, scheme: .sentimentScore ) return Double(tag?.rawValue ?? "0") ?? 0 } --- 现在我们输入一些程序员在日常会说的话: | 句子 | 得分范围 | 实际情感 | | ---------------------------------------- | ----------- | ------------------------------------- | | "Kill the background process" | -0.5 ~ -0.7 | 中性(技术指令) | | "Fatal error: index out of range" | -0.7 ~ -0.9 | 中性(编译器消息) | | "Abort the deployment" | -0.4 ~ -0.6 | 中性(操作性决策) | | "Destroy the old database" | -0.5 ~ -0.7 | 中性(维护操作) | | "The crash was caused by a null pointer" | -0.5 ~ -0.8 | 中性(问题诊断) | | "Successfully deployed to production" | +0.3 ~ +0.5 | 正面 | 注意:这里有六个看似正常的技术场景句子,其中五个都被判断为负面。唯一的正面例句是包含术语“successfully”的,模型将其识别为消费评价中的术语。 再来看看模型在它被训练的日常领域中的表现: | 句子 | 得分范围 | 实际情感 | | ------------------------------------ | ----------- | ------------------- | | "This product is terrible" | -0.7 ~ -0.9 | 负面(正确) | | "I love this app, it's amazing" | +0.7 ~ +0.9 | 正面(正确) | | "The food was okay, nothing special" | -0.1 ~ +0.1 | 中性(正确) | 这组句子全都判断得非常准确。实际上,模型本身问题不大。问题在于,它被放在了错误的上下文中。 ## 问题根源:词汇偏见 `NLTagger`的模型并不能理解语义。它并不知道“kill a process”是一个技术隐喻,而“kill a person”是暴力行为。对它来说,“kill”就是一个带负面意味的单词,仅此而已。 这种现象被称为“词汇偏见”(_lexical bias_)——模型基于单个单词(或短语)的极性打分,而没有理解上下文。这就好比一个翻译软件将 "I'm dying of laughter" 翻译成了真正的紧急医疗状况。 技术语言充满了在日常语言中很负面的词: - **kill**、**terminate**、**abort**、**destroy**——常见于进程操作 - **fatal**、**critical**、**severe**——日志级别 - **crash**、**panic**、**fault**——系统事件 - **dead**、**zombie**、**orphan**——进程状态 - **reject**、**deny**、**block**——访问控制 - **break**、**interrupt**、**suspend**——流控制 任何使用这些词的段落——比如错误日志、事后分析报告、拉取请求中的讨论——都会被预测为负面情绪,好像有人正写着遗书。 ## 增加上下文能解决问题吗? 你可能会想:“如果提供更长的段落,模型应该会更好地理解上下文。”试试这个: ```swift let technical = """ The daemon was killed after a fatal memory error. \ We restarted the service and the crash did not recur. \ Deployment completed successfully with zero downtime. """ print(sentiment(of: technical)) // 典型结果:-0.3 ~ -0.5 这是一个描述成功解决问题的段落。结尾显然是积极的。但其中的“killed”、 “fatal”、 “error” 和“crash”这些词的负面权重,远远超过了“successfully”和“completed”的正面积极权重。模型仅仅是将词汇的极性进行平均,而无法理解叙述的含义。 ...
乘法很难,加法很简单。 任何小学生都知道这一点。但他们不知道的是,对数的存在正是为了利用这种不对称性:通过将乘法转换为加法,在简单的世界中操作,然后逆转这种变换。结果正确,而努力则大大减少。 这种模式——把问题转化到一个易于解决的空间,解决问题,然后再转换回来——是整个工程学中最强大的工具之一。快速傅里叶变换(FFT)用它来处理信号,对数用它来处理乘积。而现在,Google发表了一篇论文,提出用它来压缩语言模型。 这个方案叫做 TurboQuant。它的理念优雅得值得我们详细探讨一番。 问题:压缩而不牺牲质量 现代的大语言模型(LLMs)有一个主要瓶颈,而它并不是模型本身,而是它的工作内存。每当一个模型生成文本时,它需要在内存中维护一种叫做键值缓存(KV缓存)的结构——基本上就是模型用来生成下一个词所需的“上下文”记忆。 对于一个具有长上下文的大型模型来说,其KV缓存可以占用几十GB的显存。这是一个严重的问题:GPU的显存非常昂贵、有限,而且需要用来处理其他很多事情。 一个显而易见的解决方案是量化:减少每个数字的精度。比如,代替用32位浮点数(float32)存储每个值,可以用4位甚至3位。这意味着你从一个连续的值域转化为离散的定标值。通常,这种方法效果不错。 直到你遇见了一个陷阱。 规范化常数的陷阱 要对一组数字进行量化,你需要知道它们的范围:它们的最小值和最大值。这些被称为规范化常数,如果需要还原原始值,就必须保存这些完整的常数(16位)。 而问题就在这里:,如果你用3位对每个数字进行量化,但每8个数字你就需要保存一个16位的常量,这些常量会为每个数字额外占用2位。你的“3位”压缩实际上需要5位才能实现。你几乎失去了一半的压缩效果。 这就像是搬家到一套更小的公寓,却发现搬家的纸箱占了新公寓的一半空间。 而你也不能随意增加数据块的大小以分摊这些常数,因为更大的数据块意味着近似度变差——范围加大,你会失去精度。你被困在了两个矛盾的力之间。 这个问题困扰了大家好几年。每个人都试图通过发明更好的压缩算法来解决它,比如自适应块、非均匀量化、混合方案之类的方案。但所有这些方法只是在增加复杂度,带来的效果却非常有限。 然后Google做了一件不同的事情。他们没有发明一个更好的压缩器,他们改变了坐标。 改变坐标系:从直角坐标变成极坐标 TurboQuant的核心思想是,在量化之前先对向量进行随机旋转,然后将结果转换为极坐标。 分步骤来解释。 第一步:随机旋转 想象一下,你在一个高维空间中有一个向量。它的各个分量分布得并不均匀——某些维度的值非常大,而另一些则接近于零。这种不平衡恰恰是为什么你需要为每个数据块保存规范化常数:每个块都有一个不同的范围。 如果你把这个向量乘以一个随机的旋转矩阵会怎样?从几何上看,你是在将向量旋转到一个任意的方向。向量本身保持不变(长度相同,与其他向量的关系也依然保持),但它的各个分量会重新分布。 这时,会发生一个看似魔法的数学现象:在高维空间中,随机旋转会使向量的分量变得几乎均匀。这被称为测地集中现象——在高维空间中,几乎所有分布的质量都会集中在其平均值附近。随机旋转会使这一切变得平滑。 用通俗的语言来说:旋转消除了峰值和谷底。在旋转之后,所有的数据块范围变得相似。如果所有块的范围都类似了……就不需要为每个块单独保存范围了。 第二步:转换为极坐标 经过旋转之后,TurboQuant将向量从直角坐标系(x, y, z…)转换为极坐标(一个半径和多个角度)。 为什么要这样做?因为随机旋转使得角度变得高度可预测——它们被集中在狭窄的范围内且是已知的。这些角度的量化阈值是固定的,不受数据的影响。 这意味着:角度不需要保存任何规范化常数。 之前每个数字要额外占用的1-2位开销现在完全消失。你只需要保存半径(一个数值对应整个向量)和用固定界限量化的角度。 flowchart LR subgraph antes[" 传统量化 "] direction LR V1[" 原始向量 "] --> B1[" 划分为数据块 "] B1 --> N1[" 计算每块的<br/>最小值/最大值 "] N1 --> Q1[" 量化 <br/>3位+2位开销 "] end subgraph despues[" TurboQuant "] direction LR V2[" 原始向量 "] --> R2[" 随机旋转 "] R2 --> P2[" 转换到极坐标 "] P2 --> Q2[" 量化 <br/>3位,开销≈0 "] end antes ~~~ despues --- 精彩之处在于,旋转是可逆的。要恢复原始向量,只需反量化,回到直角坐标系,然后应用逆旋转。结果是,KV缓存的压缩比达到了**6倍**,且没有可测量的精度损失。 ## 为什么能奏效:随机性减少不确定性 这是论文中最违背直觉的地方。**添加噪声(随机旋转)实际上减少了不确定性。** 这似乎是个悖论,但其背后有着优雅的解释。 没有旋转时,每个向量都有自己内部的分布特性。某些维度占主导地位,其他维度是噪声。你无法提前知道每个块的范围,因此必须测量并存储这些范围。 有了旋转,**你强制所有向量都服从相同的统计分布**(测地集中现象的作用)。你无需测量任何东西,因为你提前知道值的分布特性。随机性破坏了每个向量的特定信息(这些信息对你毫无用处),并用可预测的规则性取而代之(这对你非常有用)。 这就像洗牌一副牌。在洗牌前,你不知道牌的顺序——必须逐张查看。而洗好后,你能确定:它们的顺序是均匀随机的。看似无序的洗牌后,在统计层面你对这副牌了解更多了。 ## TurboQuant的成果 Google使用Gemma和Mistral模型在多个基准中测试了TurboQuant,结果如下: | 指标 | 值 | |------|----| | KV缓存压缩 | **6倍**(从32位减少到大约5位,其中量化为3位) | | 注意力计算加速(H100) | **最高达8倍**的*attention logits*计算速度提升 | | 精度 | 在LongBench、RULER、ZeroSCROLLS上无可测精度损失 | | 是否需要重新训练 | **不需要**——无需微调或校准即可运行 | | 数据依赖性 | **无**——与数据无关 | 最后一点尤为关键。大多数量化技术依赖于校准数据集来调整参数。而TurboQuant完全不需要——随机旋转直接起效,因为测地集中现象是空间的属性,而不是数据的特性。 ## 每个程序员都该知道的模式 TurboQuant是一个更大范围内模式的特例,这个模式值得一个名字:**转化即制胜**。 核心思想是:当一个问题很难解决时,先问问自己,你是否站在正确的空间中。有时候,答案并不是更聪明的算法,而是问题的另外一种表达形式。 你早已熟悉的一些例子: | 问题 | 原始空间 | 转换 | 简化空间 | |------|---------|------|--------| | 大数乘法 | 算术 | 对数 | 加法 | | 过滤信号 | 时间 | FFT | 频率 | | 解微分方程 | 时间 | 拉普拉斯变换 | 代数 | | 压缩KV缓存 | 直角坐标 | 旋转+极坐标 | 规整角度 | | 找文本模式 | 字符 | 正则表达式 → 自动机 | 状态转移 | 在每种情况下,困难并不在于问题本身,而在于**表达形式**。改变坐标,难题变得简单。 ## 你今天能应用的 即使你不在压缩LLMs,也可以采用这个模式。下次遇到一个“看似简单但难以解决”的问题时,问问自己: **1. 我在正确的空间中吗?** 如果比较两件事很难,也许应该先规范化后再比较。如果搜索效率低,也许应该建立一个索引(这是为搜索优化的替代表达方式)。 **2. 我的领域中是否有已知的变换?** FFT自1965年以来就存在了。小波变换从80年代就被引入。向量嵌入技术自2013年来到大舞台。许多“困难”的问题已经有标准的变换方案,解决它们。别急着发明新东西,先查查是否有人已经找到了正确的坐标。 **3. 我能通过添加随机性简化问题吗?** 哈希、随机投影、概率性摘要——它们之所以有效,是因为添加受控随机性可破坏不必要的复杂性。如果你的问题有着让你头疼的不规则结构,有时最好的策略是有目的地摧毁这种结构。 ## 下次碰到问题解决不了时 TurboQuant的教训并不仅仅适用于模型压缩。它告诉我们,在发明更聪明的解决方案前要三思,也许你真正需要的只是**改变视角**。 近年来,机器学习领域一直试图通过更复杂的量化算法来解决规范化常数负担的问题。更大的数据块,更多的量化级别,更复杂的启发式方法——一切都停留在直角坐标的框架内。然而改进却十分有限。 Google却另辟蹊径。他们旋转数据,改变了坐标系,问题随之消失。他们不是解决了这个问题。而是**消弭**了它。 下次,当你花费数小时试图用一堆补丁修复一个无法解决的问题时,停下来问问自己:我是和问题本身作斗争,还是和表达形式做斗争?因为如果站在错误的坐标系上,世界上最聪明的算法也救不了你。 --- **参考来源:** [TurboQuant: Redefining AI Efficiency with Extreme Compression](https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/) — Google Research Blog.
几周前,世界上最顶尖的量化交易公司之一 Jane Street 发布了一个关于 解释性机制 的谜题。他们手工设计了一个拥有大约2500层线性结构、使用整数权重的神经网络,并以一个简单的问题把它交给了公众:这个网络到底在计算什么功能? 答案是:MD5。一个诞生于1992年的密码哈希算法,完全由矩阵运算和ReLU函数以神经网络的形式实现。 问题的精髓并不在答案,而是在最终赢家破解谜题时的过程。本质上,这个过程是一份调试复杂系统的教科书,它的应用远超机器学习领域。 实验背景 这个谜题并非一个典型的“黑盒”问题。参赛者可以获得模型的完整规范:所有的权重矩阵、偏置以及网络架构都公开了。不需要猜测模型的结构,而是需要理解模型 具体在做什么。 网络接受一个文本字符串作为输入,并返回0或1。官方给的例子是:“vegetable dog” 的输出是0。 拥有大约2500层线性结构、约200万个节点、却没有任何文档,这个问题的核心是:输入和输出之间到底是什么关系? 解题路径:一个调试案例分析 最终赢家 Alex 的解题过程展现了资深工程师常用的调试逻辑。过程中的工具可能因领域不同而有所变化,但思维方式无疑是通用的。 阶段1:观察,而不是立刻动手 Alex 做的第一件事是可视化权重矩阵。他没有直接运行模型,也没有尝试训练它。他只是看了数据。 他的发现是:所有权重都是整数。没有小数,没有浮点数,而是清一色的整数。 这显然是一个重要的信号。使用梯度下降训练的神经网络通常会产生许多小数权重,而整数权重表明这是一个用特定目标手工设计的网络。这不是一个“学习”的模型,而是一个伪装成神经网络的程序。 这告诉我们调试中的第一个关键原则:在解释数据内容之前先观察数据的形状。比如,一个以每100毫秒精确间隔打出的时间戳日志肯定不是来自实际流量;一个JSON对象里的每个字段都精确包含3个元素,也不可能来自生产环境。数据的形状揭示了它的来源。 阶段2:缩小问题规模 Alex 尝试将神经网络转化为一个可满足性(SAT)问题。基本思路是:如果每个神经元都可以看作一个逻辑约束,或许一个SAT求解器能找到让网络输出为1的输入。 他的缩减过程是循序渐进的: 移除了80%的“恒等变换”神经元 合并了只有一个输入且权重为1的节点 压缩了具有相同输入向量的节点 通过这一系列优化,网络从200万个节点缩减到7.5万个节点,最终转化为20万个SAT变量。 但依然没解出来。问题的计算复杂度仍然过高,无法用暴力法解决。 这个阶段带来的重要教训是:正确地简化问题后仍无法解决,这本身也是重要的信息,而不是失败。如果在消除所有“附带复杂度”之后,问题仍然困难,那说明问题的难点是固有的。这一点揭示了它的本质,而在本例中,它的本质是这个函数可能不可逆——我们无法通过输出推断出输入。 阶段3:识别模式 Alex 注意到这个网络中存在一个显著的模式:有32个计算模块周期性地重复出现。32轮完全相同的结构,加上不可逆的性质。 于是他向 ChatGPT 提问:“有哪些密码算法使用32轮计算?” 答案是——MD5。 这是“灵光一闪”的瞬间。但注意,它并不是无来由的突发奇想,而是三个前期资料(整数权重 → 手工设计、不可逆 → 加密算法、32轮 → 特定密码协议)的合并推理,最终形成了一个可验证的假设。 这种模式——通过不断加入约束,直到可能性空间逐渐收敛,与资深工程师在生产环境中诊断问题的方法高度一致。资深工程师并不是提前知道答案,而是通过每一步观察逐步排除整个可能性类别,直到只剩下一种可能性为止。 阶段4:发现问题 接下来的步骤中,Alex 验证了自己的假设:他计算了几组输入的MD5值,并与网络输出进行对比。结果显示,网络输出与MD5哈希值完全一致,当输入长度不超过32字符时。 但是,当输入长度超过32字符时,网络的计算结果就出现了偏差。 这是因为网络中存在一个bug。设计者在处理输入长度编码时出现了错误——在前7层中发生了一个溢出错误,使得网络在处理长输入时与标准MD5的输出结果不同。 Alex 一层一层地追踪错误,直至找到确切的偏差点。 这是一个绝佳的案例:在边界条件下验证假设。如果你的心理模型认为“这个神经网络在计算MD5”,那么仅仅在理想情况下(短输入)验证它是不够的。必须在极端条件下(长输入、空输入、特殊字符)对其进行测试。而当它失败时,这些偏差往往正是定位问题的关键线索。 阶段5:成功破译 最终,确定这是一份具有已知bug的MD5函数后,Alex 提取出了倒数第二层中的目标哈希值偏置项。接着使用词典暴力破解法,找到了谜底——两个用空格分隔的英文单词。 这对调试的启示 Alex 的解题过程与机器学习本身关系不大,它本质上是一场关于调试的教科书式行动: 阶段 在谜题中的体现 在实际调试中的体现 观察形状 整数权重 → 手工设计 日志间隔均匀 → 合成数据 缩小问题规模 200万节点 → 7.5万节点 → SAT问题 完整堆栈轨迹 → 具体组件 累积约束 不可逆 + 32轮 → 加密算法 只在生产中失败 + 只发生在周一 → 定时任务 边界验证 短于32字符可行,长于32字符失败 → 溢出 ASCII正常,UTF-8失败 → 编码问题 利用错误找线索 溢出问题定位到具体层 堆栈追踪定位至具体行 结构完全一致,差异只是领域不同而已。 ...