SyncTrans 翻译的是现场口播的语音,而谈到钱的口播往往一就是一、二就是二:一 个价格、一个数量、一个折扣。这篇文章讲的是一类 bug:译文流畅、自信,却在最要 紧的那个数字上错了 —— 以及我们在 1.0.0 里为此发布的那个一点也不光鲜的修复。
自己把自己译反的折扣
中文和日语都有自己惯用的折扣说法,而两者的计数方向正好相反。中文数的是你 付多少:七折就是按标价的 70% 付款,也就是优惠 30%。日语数的是 减掉多少:同样这个优惠写作 3割引 —— 减去三成。每个中文母语者和每 个日语母语者都早早学会了自己这套约定,此后再也不会多想。
翻译模型看到的却是另一回事:七折和 割引 在折扣语境里成对出现的频率极高,而 数字 七 对应的是 7。在我们早期的测试里,模型会兴高采烈地把 七折 译成 7割引 —— 听在日语使用者耳朵里,那就是优惠 70%。供应商说「我给你打七折」,译出来 却成了「我给你优惠 70%」。在一场谈价格的对话里,这就是「成交」和「代价高昂 的误会」之间的差别,而且双方都坚信自己听到的数字和对方说的一样。
同一个陷阱,放大一个数量级
折扣是最尖锐的案例,但同样的结构在普通数字里也会出现。英语按千进位数大数; 中文按 万(一万)和 億(一亿)来数。12万 是 120,000,1.2億 是 120,000,000。 一个只把数字照搬过来、却不重新锚定单位的模型,会把 12万 译成 「12,000」—— 流畅、合语法,但差了整整十倍。口播的数字让情况更糟,因为语音 识别交给翻译器的是一串词流,而那个决定数量级的单位,可能在数字之后隔了整整 一口气才出现。
模型为什么会中招
我们花了一段时间才真正想明白,为什么不同的模型都会在同一个地方栽跟头。折扣 是一个带方向的数字,而这个方向藏在数字的计数约定里,不在任何可以被模型照搬 过去的词里。词典里不存在一条能把「折」正确映射到「off」的词条,因为「折」 从头到尾讲的就不是减掉了多少 —— 它讲的始终是剩下了多少。模型只是把见过一 万遍的表面模式复现出来,它内部没有一本账,能发现数值被翻转了。
关键在于,这是一个约定问题,而我们验证了它不是能力问题。我们构造了一个小型 回归语料,包含双向的折扣短语和大数短语,然后拿它去跑 SyncTrans 发布的每一个 模型档位 —— 两种端侧技术栈和云端引擎都跑了。这个翻转在每一个规模、每一套 技术栈上都出现了。更大的模型只是把错误的数字说得更流利而已。当测量结果长成 这样,结论也就自己写好了:等一个更好的模型解决不了这个问题,修复必须落在模 型之外。
模型身后的一层规则
1.0.0 里的修复,是一层位于翻译器身后的确定性后处理层,专门盯着那些带着计数 约定的数值表达。当一个折扣短语可以无歧义地解析时,这层规则就不再把它当语言 处理,而是当算术处理:先把 七折 换算成它的显式数值 —— 付 70%,即优惠 30% —— 再用目标语言自己的约定重新表达出来。「折」是算术,我们就用算术来 做。按万进位的数字享受同样的待遇:先根据单位算出数量级,再按目标语言的分节 方式渲染出来。
这层规则的妙处在于它很无聊。每条规则都是一个小的、可以写单元测试的函数, 答案固定,被当初证明 bug 存在的那份语料钉死。规则触发时,我们能说清楚确切 的原因;模型即兴发挥时,我们做不到。对于「一个流畅的错误会让人真金白银受 损」的这类短语,我们要的就是一个每次给出同样答案、可以逐行审计的组件。
这套混合方案的代价
这些取舍我们是睁着眼睛做的,而且它们确实存在。规则层的好坏取决于覆盖面, 所以第一条纪律是克制:规则只在短语能干净利落地解析时才触发,任何有歧义的 情况都交还给模型。一条靠猜的规则,会把模型本来译对的内容改坏 —— 那是比 原 bug 更糟糕的唯一一种结果。第二个代价是维护 —— 计数约定是按语言对划分 的,每认真对待一对语言,语料就得跟着长一截,每条规则两侧都要有测量数据。 而其余的一切仍然由模型负责:行文、语气、上下文。规则只是针对一类狭窄、可 测量的失败的兜底,我们刻意把它压得足够小,免得它长成第二个翻译器。
目前进展
数字规则已随 SyncTrans 1.0.0 发布,语料也跑在我们的回归测试里,所以未来任 何模型升级都必须持续通过它。这项工作还没有结束:围绕大数边界的加固 —— 混 合单位、以口语里随意的形式说出的数量级 —— 仍在进行中,而且我们预计,随着 真实对话不断教会我们新的出错方式,语料还会持续增长。如果有数字从 SyncTrans 里出来时看着被翻转了或者缩水了, 请告诉我们 —— 这样的报告会直接进语料库。
想了解 1.0.0 版本里其他内容的完整故事,见 SyncTrans 1.0 之路。