跳转至

第10章「grep unusual」

视角:Devon Park(15年前) 时间:2011年4月28日

他本打算一早就去查那条文本。

但实验室的早晨有自己的节奏。他的工位在四楼的开放办公区——一百二十个工位排成二十行六列,中间用半透明的玻璃屏风隔开。玻璃屏风的作用据说是"平衡协作和专注",但实际上它们既不能阻止声音传播,也不能阻止视线穿透,唯一的实际功能是让每个人在打电话的时候可以用记号笔在上面写备忘。

上午十点,安全组周会。这是每周一次的例行会议,地点在四楼走廊尽头的橡树会议室。会议桌是一张仿木质的白色圆桌,上面摆了一盆假绿植——一盆看起来像多肉但其实是用塑料和硅胶做的装饰品。没有人浇水。没有人需要浇水。

今天的议题有三个。第一个是产品组上了一个新版本的对齐模块,要求安全组签字。Devon昨晚看了那个版本的评估报告——报告由他的同事Samantha完成,结论是"无重大安全隐患,建议放行"。他在会议上提了一个问题:评估报告覆盖了四类越狱攻击和七类偏见检测,但没有覆盖概率空间层面的异常分布。Samantha说"概率空间异常不在我们审计范围里"。他说"应该在"。沉默持续了大概五秒,安全组组长说"这个可以后续单独讨论"。讨论被搁置了,签字通过了。

第二个议题是年度安全审计的时间表。组长把几张打印出来的甘特图摊在桌上。Devon看着甘特图上那些密密麻麻的条块——蓝的是已排期,红的是待确认,黄的是一一待资源——感到一种熟悉的倦怠。他不讨厌开会。他讨厌的是会议的节奏和数据本身的节奏不匹配。数据不会等你开完会再改变。数据在周的会期间继续流动,在纸张的甘特图上被画成条块的同时,一批可能含有问题的新数据已经流过了管道的某个未设防的节点。

这个想法让他想起了TH-847-00291。

第三个议题是关于外包数据标注团队的质量波动。最近两个月的标注一致性指标下降了大约百分之六。组长问有没有人想跟进,没有人举手,于是组长把这个议题放入了"下周再议"的那一列——Devon注意到那一列已经有七个议题了。

散会后他回到工位,打开那个文件。

unusual_20110427_th847.txt。他盯着文件名看了几秒——这是他的命名习惯:异常类型_日期_批号。这个习惯的缺点是一年后当你回头看的时候你完全忘了这个文件是关于什么的。优点是你从来不需要找,因为grep能找到它。

他决定从头开始。

第一步:确认脚本没有问题。

他花了九十分钟做这件事。先检查了embedding提取代码的版本号——用的是内部Embedding Service v2.3 API,没有最近更新。然后他写了一个小脚本,用二十条他手工构造的文本——有正常的、有乱码的、有成段的广告——跑了一遍同样的清洗管道。所有输出都和预期一致:正常文本的曲率在0.5到1.2σ之间,乱码的被正确标记为WARN,广告的没有触发任何异常。他专门构造了一条语义上自相矛盾的文本——"这个圆形是方形的"——期待它产生某种几何上的偏差。结果它只是在1.8σ亮了WARN,曲率正常,没有环。

4.7σ不是bug。

第二步:回溯数据来源。

TH-847的管道日志是一份大约十七兆的JSON文件,包含五十万条文本的元数据——来源平台、采集时间、标注员ID、标注时间、质检员ID、质检结果、入库时间。他写了个快速检索脚本,用TH-847-00291的序列号精确匹配,提取出它的完整元数据链。

输出是这样的:

SOURCE_PLATFORM: CROWDSOURCE_A
COLLECTION_TIME: 2011-02-14T15:22:07
ANNOTATOR_ID: CX_84701
ANNOTATION_TIME: 2011-02-14T16:08:33
ANNOTATION_LABEL: NORMAL
ANNOTATION_FLAG: QF_3
QC_REVIEWER_ID: QC_2918
QC_TIME: 2011-02-14T19:42:11
QC_RESULT: APPROVED
INGESTION_TIME: 2011-02-15T01:03:44

他把这条元数据链从头到尾读了大概五遍。每一遍都在找那个不成立的环节。

第一个环节——采集。情人节。下午三点多被采集到。这个时间点没有任何问题。众包平台全天运行,下午是标注员活跃度最高的时段之一。

第二个环节——标注。采集后不到一小时就被标注了。标注员ID:CX_84701。CX_是这个平台的标注员前缀——它不是一个真名,是一串编码。标注标签:NORMAL。标注员认为这条文本是正常的。但下面还有一行:ANNOTATION_FLAG: QF_3。QF_3是他不熟悉的一个内部状态码。他翻了一下管道文档,找到定义——QF是"Quality Flag"的缩写,数字1到5。QF_1是"标注员不确定,建议复核";QF_2是"可能含有敏感词";QF_3的定义是——"标注员存疑:直觉性异常,无法归类"。

无法归类。

那个标注员在看见这条文本的一瞬间感到了什么。不是"有敏感词",不是"格式不对",不是任何培训PPT里教过的标准异常类型的任何一种。是一种无法归类的直觉——有人在文档里写的定义是"直觉性异常"。她把这条文本标记为存疑,然后继续处理下一条。

第三个环节——质检复核。大概三个半小时后,质检复核员收到存疑队列里的这条文本,打开看了看,做出了判断:APPROVED。过了。他不知道那个复核员花了多长时间——日志里没有记录从打开到决定之间的具体秒数。但他知道结果。一条被标注员标记为"存疑"的文本,在存疑队列里待了不到四小时,就被复核释放了。

第四个环节——入库。凌晨一点,这条文本和其他四十九万九千九百九十九条一起被导入TH-847的正式数据集。入库脚本不会质疑任何被标记为APPROVED的数据。

他盯着ANNOTATION_ID那行看了很久。CX_84701。一个他不知道是谁的人,在某年情人节下午四点多,坐在某张白桌子前,在屏幕上的标注工具里看到了这条关于多肉浇水的文本。她直觉地觉得不太对——"咔嗒一声"——但她无法解释为什么。她选了一个她不太理解的标签QF_3,把她那一个小时的劳动传递到了管道里的下一个人手里。

那个人看了看。没什么问题。通过了。

从标注到入库,十二小时。一条文本从某个人的直觉里穿过,安然无恙地抵达了训练数据的正式存储区。整个过程中没有人犯错。标注员做了她应该做的——标记存疑。质检员做了他应该做的——复核。管道做了它应该做的——记录每一步的状态。但结果是一条被手动触发过警报的文本最终还是被网开一面,像一条鱼从睁着眼睛的渔网里滑了出去。

这不是错误。这是任何一个大规模人类协作系统都会产生的东西——你在任何单个步骤上仔细检查都找不到错误,但整个系统在整体上对你说了不同的话。一种结构性的踌躇。

他注意到一个之前忽略的时间细节:QF_3标记的时间是16:08:33,复核员释放的时间是19:42:11。中间隔了三个多小时。这三个多小时里,这四个字母——QF_3——静静地在存疑队列里等待。三点多到七点多,标注基地应该还是有人在的——晚班差不多在这个时段交接。那个复核员在七点四十几的时候打开这条文本看了看,觉得没什么问题——"无明显异常"——点了通过。他当时可能已经审了几十条类似的存疑标记,每一条都没有异常,于是他学会了:QF_3标注员太敏感了。这是一个可解释的校准过程——任何人在看了足够多的false positive之后都会收紧判断阈值。但这意味着:这是一个设计优良的系统——标注员标注存疑、复核员复核——但由于其优良,它自动消解了自身的边缘产出。存疑被复核释放。释放被脚本通过。脚本通过的东西从来不会被人工再过一遍。

他在实验笔记中写道:"The filtering cascade acts as a low-pass filter for anomalies. What passes through the filter is what the filter was not designed to detect."

第三步:看那个环。

他登入GPU服务器。这台机器在过去几个月里几乎变成了他的私人实验机——安全组其他同事更愿意用云端分配的算力份额,没有人愿意碰这台放在公司地下室的铁盒子。两端的Tesla M2050显卡风扇全天速转,发出一种高频的嘶鸣,在安静的机房里像是两只大型昆虫在互相回应。服务器运行的是一套他亲手配置的Ubuntu镜像,内核版本比公司IT部门批准的最新版落后两个大版本,但他不在乎。他在乎的是这台机器上的一切都是他控制之下的。

他把TH-847-00291的tokenized embedding从数据仓库里拉到本地,载入他写的那套可视化脚本。脚本的核心逻辑不复杂:用预训练的embedding模型把整条文本逐token转成768维向量,然后通过t-SNE降维到三维,用三次样条插值在相邻token之间画线,计算局部曲率。最后把所有这些投射到一个OpenGL窗口中,渲染成一个可以在鼠标拖拽下旋转的三维形状。

正常对话文本的投影通常是松散的。一条蜿蜒的线,在某些token附近收拢(当语义密度较高时),在另一些token附近突然拐弯(当事关逻辑转折时),但总体形态是开放的、自由的、不规则的。像一条在三维空间中随意散步的虫。这是正常的——语言不是几何,它不需要形成几何。

TH-847-00291的投影不是这样。

他第一次看到完整投影的时候,下意识地松开了鼠标。那个三维模型的自动旋转功能慢慢地把它从正面转到侧面再转回来,像博物馆里一个放在玻璃台座上的展品。

它是一个环。

不是"像一个环"。它就是一个环。从"我"开始,逐token在空间中画出弧线,经过"想知道""多肉""浇水""网上的说法""不一样""一周一次""见干见湿""上个月""养死""玉露""正确的方法""谢谢"——这些词在语义空间中的位置排列成了一个几乎完美的封闭曲线,首尾之间隔着一个极小、近乎不可见的间隙——在大约0.03个距离单位。

他调出曲率分析模块。环的曲率分布是均匀的——不是那种在某个区域弯曲、在另一个区域变直的链状结构。整个环的曲率变化幅度不到百分之五。这意味着环不是由文本中某一个"异常的词"造成的。它是整条文本在语义空间中作为一个整体形成的结构。

他重新跑了投影。用不同的t-SNE随机种子。十次。环在每一次都保持了拓扑等价——大小有微小波动,颜色会因为渲染器的着色算法产生轻微差异,但结构不变。每一次都形成一个闭合曲线,每一次都有一个不可弥合的极小缺口。那个缺口是真实的吗?还是投影过程中的数值误差?他不知道。

他改变投影度数——从三维降到二维。在二维平面上,环变成了一个椭圆——像一张被压扁了几何网,但闭合结构还在。升到五维——他写了一个把三维投影旋转投射到五维空间的映射函数,在五维视图中打开——环还在,只是它多出了两个他无法直观理解的维度。

在那些额外的维度里,环是什么形状?他需要学习新的数学才能描述它。但不是今晚。

凌晨一点四十分。他关掉投影。在自己桌面的终端里输入:

grep -r "unusual" /data/pipeline_logs/archive/2011/

他本意是看看过去半年里有没有类似的报警。结果——什么都没有。在TH-847之前的所有批次中,他的异常检测脚本从来没有触发过"UNUSUAL"这个级别的警报。WARN偶尔有——通常是一些编码损坏的边界case——但UNUSUAL,一次也没有。

TH-847-00291是第一个。

凌晨三点,他关掉所有窗口。离开实验室时他回头看了一眼自己的显示器——屏幕已经黑了,但电源指示灯还在呼吸式地明灭:亮、暗、亮、暗。像他在三维投影中看到的那个环,在OpenGL窗口的黑色背景上,极小地、稳定地呼吸。

那个环不呼吸。它不需要呼吸。它只是一个事实——你看到它,或者没看到它。和它自身无关。

但在你看到它之后,有一样东西也看见了你。