大数据文摘出品
编译:张秋玥、蒋宝尚
文字语音转换圈内流传这么一则真假未知的故事:一个研究者花了数月(甚至数年)调整他/她的语音生成模型,使其语音样本听起来效果非常好。最后他们发现,他们从头到尾都误用同一语音文件进行训练,最终模型只是完全符合该语音文件特征所以才拥有如此流畅的语音样本输出。这个故事到现在都让人不寒而栗。
想象一下另一则恐怖故事:你是个小实习生,老板让你搭建一个判断识别“Yes”与“No”的语音识别分类器。你有这些音频文件:yes1.wav,no1.wav,yes2.wav,no2.wav,yes3.wav等等。你建好了分类器,效果也很好。就在你要展示工作成果之前,你发现这个模型唯一在做的事情就是通过读取文件名里的yes或者no来预测结果,压根不会听文件里面的音频。你吓傻了,大哭一场,准备跑路。
这就是本文作者Vincent Vanhoucke所经历的恐怖故事,完全真实,这些小事故也决定了这位Google首席科学家的职业生涯。
以下是他以第一人称讲述的更多小故事,让我们看看能够从中得到哪些经验:
那是我作为研究者的第一份工作。任务很明确,提供了大量数据以及优秀的预测准确度标准来评估模型效果。模型的基准结果很强,我最后甚至和一位客户一起在生产实践中部署了这个模型。
我有试图根据我觉得很聪明很厉害的方法来改进模型表现指标——它没有很完美但每一天都在进步。我都能看到我脑子里慢慢形成的一篇优秀学术论文啦。生活真美好。
这算是一项产业研究,所以在开始撰写论文之前我还需要通过最后一项测试:使用真实顾客数据来评估模型,以便于快速在生产实践中部署改进方案。在真实数据集上我的模型达成了零精确度成就。我可是一直在提高我觉得超级厉害的表现指标来着。
八成是出了bug,要不就是真实顾客数据质量很糟糕——我脑子这么想着,觉得没多大关系就急着开始上手写论文了。但实际上我又并没有办法完全放下这个糟糕结果,所以我就开始研究到底是怎么回事。我最后发现的是全世界数据科学家共同的噩梦:准确度就是零,这一点毫无疑问。我其他所有的准确度数据都是所谓的“幽灵”数字。我简直不敢信:这些数字看起来超可信啊,它们比基准高但并没有高到不可能的地步。
人们常说,灾难一般不会“成单”出现,而是在有两件事一起出错时,因为我们总体来说很擅长预判并改正单个失误。为了完全了解到底是什么样几乎不可能发生的系列事件导致了这些看似可信的精确度数字的出现,我必须得从细节开始分析。
模型目标是改善用来识别人名的语法数据结构。比如说,假如你叫“Robert Moore”,语音识别系统可能将会把你的名字编译成为一个语音图,大致看起来像是某种正则表达式:“/(ˈɹɑb.əɹt|ˈbob|ˈɹɑb) mʊɹ/”——它还兼容类似于“Rob”或“Bob”的昵称呢。我的任务是生成更好的语音图。我的数据被存储为键值对数据库的形式:
record {
key (string): “robert_moore”
value (Grammar): /(ˈɹɑb.əɹt|ˈbob|ˈɹɑb)mʊɹ/
}
这里有一个bug:有些我的语法数据结构里用到的语音符号并不会被发音引擎识别。系统尝试把语法数据结构编译为一个应当代表正则表达式的图像对象,但它失败了。在层层代码的深处,有人曾尝试将系统变得对于这些失败更加稳健:毕竟,只要可能,你永远不希望系统在生产实践中突然垮掉嘛。那段代码看起来类似于这样:
Graph* graph = compile(record->value);
if(!graph) { // Failed to compile.
graph = compile(record->key); // (什么鬼???)
}
这可真的让我大吃一惊措手不及:怎么会有人觉得只要一条数据库记录损坏了就代表这条记录的键包含真正的负载?而且这怎么可能可行嘛?“值”就是一条序列化的型语法,“键”就只是一串字符而已。再深挖一点——看,更“稳健”的在这里:
Grammar* grammar = parse(record);
if(!grammar) { // Failed to parse.
grammar = parse(pronounce(record)); // (啥???)
}
如果数据不是我们预想的类型,我们就会尽量提取那条记录的内容为单词进行发音。为什么不呢,反正已经毫无希望了嘛。而且,发音生成是一项非常耗时耗计算力的操作。想象一下,不管出于什么原因,一大串没有任何意义的垃圾字符(包括对于拒绝服务的报复性操作)突然被输入到系统里,这对于系统意味着什么。系统将会立刻过载,而非“逐渐失败”。
你可能已经意识到接下来要发生什么了。我的数据的键都是用户的真名,比如“robert_moore”。发音引擎很容易就将其近似于“/ˈɹɑb.əɹt mʊɹ/.”。所以,我的数据的问题直接来自于决定模型评估标准的事实。
理论上来说这就与我在前文提到的根据文件名预测音频是yes还是no一个道理。我没预料到的是,发音模型的随机试验看起来确实改善了结果。然而,那其实只是取决于每次实验中未编译成功的数据比例而已。我的模型失败次数越多,生成的错误就更多,真实键值使用的更多,我的模型精确度就越好。至于解锁零精确度成就的真实数据?那个数据库里的键都是乱七八糟的字符串,看起来类似于“h4a7n6ks2l”这种发音模型?
我还算是幸运的。我对符号检索问题进行的修复确实提高了效果,新系统确实得到了改进。数周的实验最后都是竹篮打水一场空,我还得跟同事解释我这个模型差点就上线运行害了所有客户,以及为啥我越伤害这个模型线下精确度就越高。必须得说一句,他们最后只是大笑了一场这事儿就过去了,还是很客气的。
下面是我学到的经验教训:
第一,不要相信任何人、任何事情。
谁都想抓住你的把柄,尤其是数据科学界。大多数问题会将预测结果变得看起来糟糕很多,但有时结果看起来还是足够好且真实让人无法起疑心。实际上在语言建模领域,这是一个超级常见的问题。计算以及比较困惑度阶段超多陷阱,极小的错误都时常能够提高实验数字(而非降低!)。因此,这个领域的人对于证明的要求都很高;基于这个原因,我建议在将模型推广之前你最好多在开源评估工具上试验试验。
第二,更不要相信你自己。
在我整个学术生涯中,我很快就学到了我需要过分质疑任何我得到的结果,尽管我本性并非如此。我现在会为了模型结果持续寻求外部意见,最好是使用一个完全不同的代码库。
第三,写简单的防卫代码
不要自作聪明。你的代码应该跟你本人一样偏执,就算是合同里最小的细节有一点不符,你的模型都该立刻引人注目地垮掉。每个程序员都肯定经历过这样的事:阅读堆栈跟踪到一段标注为“这就永远不该发生”的代码。大量数据被写入磁盘,甚至位翻转这种事情都时有发生。我以前有过一个生产系统,因为XML分析错误就直接崩溃了。磁盘上配置文件(程序自动生成的)看起来就像这样:
…类似的一百万行…
…再省略一百万行…
看到哪里不同了嘛?我都等不及下一次日冕物质抛射活动来让我们都变成更厉害的程序员啦(译者注:这个作者只是在这里发泄怨气…)。
第四,不要相信你的代码,更不要相信你自己的数据处理能力。
想让你的数据出错,方法超多的。即使你只有1%的数据出错,你的A/B测试结果可能也完全不对。比如,有些著名网络数据集里的某些图片就是无法被某些图像解析器读取。如果你使用另一个解析器,或者你将这些图片计入分母,最后的结果都会与别人不同。很长一段时间内,我都在评估结果内重复计入了某些测试图像,因此得到了压根不正确却看似很可信的数字结果。
第五,尽量故意扰乱你的实验。
把标签打乱,计算概率层面精确度。在1%的数据上进行训练,确保你过度拟合。更好的方法是:把你的模型交给别人,让他们自己上手使用。每个实验室都有这么一个永远能第一时间挂掉你完美代码的人。盯住他们就对啦!
这种对结果的有益的怀疑论可能是我在博士与非博士之间发现的品质上的最大区别。我们博士都经受过这样的打击。回想起来,我很幸运能够以这么一种尴尬又没有很不合适的方式在职业生涯早期收到惊讶,以确保我永远都小心翼翼地进行研究。必须说不幸的是,数据科学界从来没有“幸福小事故”这种事情(西方公众名人Bob Ross名言:世上没有“错误”一说,它们只是“幸福小事故”)。
相关报道:
https://medium.com/s/story/no-happy-little-accidents-8663540763f8
圣诞惊喜
数据科学实训营第7期
手把手,带你走完数据采集、清洗、存储、分析、可视化、机器学习建模、大数据全流程!快加入2019年Offer收割行动吧!
【今日机器学习概念】
Have a Great Definition
志愿者介绍
领取专属 10元无门槛券
私享最新 技术干货