我们的NLP预训练模型上线后,线上出现了一个诡异的Bug:部分用户的文本分类结果完全错误,但本地复现不了。我排查了整整一夜,从数据预处理到模型推理,从分词器到编码方式,最终找到了根因。本文记录这次排查过程,以及NLP模型上线时需要注意的坑。

一、背景:预训练模型上线

我们做了一个文本分类服务,用BERT预训练模型在业务数据上微调,用来对用户输入的文本做分类,支持十几个类别。模型在测试集上准确率95%,效果很好,我们就上线了。

上线初期一切正常,准确率和测试集差不多。但上线一周后,开始有用户反馈分类结果不对:明明是A类的文本,被分成了B类,而且错得很离谱。我们赶紧去查,但用同样的文本在本地测试,结果又是对的。

这种"线上错、本地对"的Bug最头疼了,因为复现不了就没法定位。用户投诉越来越多,领导要求当晚必须解决,我开始了漫长的排查之夜。

二、排查第一步:收集信息

复现不了,就先收集信息。我们做了以下几件事:

  1. 加日志:在推理服务的关键节点加日志,记录原始文本、分词结果、输入ID、推理结果,全部上报
  2. 收集bad case:把用户反馈的错误文本都收集起来,整理成测试集
  3. 分析错误规律:看看错误的文本有没有什么共同特征

收集了几十条bad case之后,我发现了一个规律:出错的文本都比较长,而且都包含一些特殊字符,比如emoji、特殊符号、生僻字。而短文本、普通文本基本不出错。

这个发现很关键,说明问题可能出在文本预处理或者分词环节,长文本和特殊字符触发了某个边界条件。

三、排查第二步:怀疑分词器

NLP模型的第一步是分词(tokenization),BERT用的是WordPiece分词器。如果分词和训练时不一致,模型输入就不对,结果自然会错。

我仔细对比了线上和本地的分词器配置,发现了一个可疑的地方:线上服务用的分词器是从一个旧的模型文件里加载的,而本地测试用的是最新的分词器文件。

会不会是分词器版本不一致?我把线上的分词器文件下载下来,在本地用它做推理,结果还是对的。说明分词器本身没问题,版本虽然旧一点,但和模型是匹配的。

那问题出在哪呢?我又仔细看了bad case的分词日志,发现了一个异常:有些长文本的分词结果被截断了,只保留了前128个token,后面的被丢掉了。

BERT的最大序列长度是512,我们训练的时候用的是128。但线上推理的时候,有些文本超过了128个token,被截断了。但截断应该只会影响准确率,不会导致完全错误啊?而且训练的时候也有截断,为什么训练时没问题,线上就有问题?

我又看了一下,发现截断的位置不对。训练的时候,我们是先分词再截断,保留前128个token。但线上的预处理代码,是先截断文本到某个长度,再分词。这两种方式的结果是不一样的!

比如,一段文本先截断到200个字符再分词,和先分词再截断到128个token,结果可能完全不同。尤其是包含emoji和特殊字符的时候,字符数和token数的差异很大。

我以为找到了问题,赶紧改了线上的预处理逻辑,改成先分词再截断。重新上线后,错误率确实降了一些,但还是有部分文本分类错误。说明这只是问题之一,不是根本原因。

四、排查第三步:发现编码问题

错误率降了,但还有问题。我继续看日志,发现剩下的bad case有一个共同点:都包含emoji表情。

emoji在BERT分词中是怎么处理的呢?我查了一下,BERT的WordPiece分词器对emoji的处理比较特殊,有些emoji会被分成多个token,有些会直接变成[UNK](未知字符)。

但训练数据里也有emoji啊,为什么训练时没问题?我去查了训练数据的预处理代码,发现了一个惊天大坑:训练数据预处理的时候,用了一个函数把emoji和特殊字符都过滤掉了!

也就是说,训练的时候,模型从来没有见过emoji,所有emoji都被删掉了。但线上推理的时候,我们没有过滤emoji,直接把带emoji的文本送进了模型。模型在训练时没见过emoji,遇到这些字符就懵了,输出完全错误的结果。

这就解释了为什么包含emoji的文本会出错,而本地测试的时候,我用的测试数据是干净的,没有emoji,所以复现不了。

找到原因之后,修复就简单了:线上推理的时候,也用和训练时一样的预处理逻辑,过滤掉emoji和特殊字符。改完之后重新上线,错误率降到了正常水平,和测试集一致。

但故事到这里还没结束。

五、排查第四步:还有一个隐藏问题

emoji的问题解决之后,我以为完事了。但又发现了一个新问题:有些没有emoji的长文本,分类结果还是不对。

我继续看日志,发现这些文本的共同特点是:包含全角字符、繁体中文、或者一些不常见的标点符号。

我又去查训练数据的预处理,发现训练时用了NFKC归一化,把全角字符转成半角,把繁体转成简体,统一了标点符号。但线上推理的时候,没有做这个归一化!

BERT的分词器对全角和半角、繁体和简体的处理是不一样的。训练时做了归一化,模型学到的是归一化后的文本;线上没做归一化,输入分布和训练时不一致,模型自然会出错。

这个问题和emoji的问题本质上是一样的:训练和推理的预处理不一致。这是NLP模型上线最常见的坑之一。

我把线上的预处理逻辑改成和训练时完全一致:NFKC归一化、繁简转换、过滤特殊字符、先分词再截断。改完之后,所有bad case都复现正确了,线上准确率恢复到了95%。

等我全部搞定,天已经亮了。整整一夜,终于把这个Bug解决了。

六、根本原因总结

这次Bug的根本原因,总结起来就是一句话:训练和推理的预处理不一致

具体来说,有三个不一致的地方:

  1. 截断方式不一致:训练先分词再截断,推理先截断文本再分词
  2. 特殊字符处理不一致:训练过滤了emoji和特殊字符,推理没有过滤
  3. 文本归一化不一致:训练做了NFKC归一化和繁简转换,推理没有做

这三个问题单独看都不大,但叠加在一起,就导致了部分文本分类完全错误。而且因为本地测试数据比较"干净",这些问题在测试时没有暴露出来,到了线上真实用户的"脏数据"环境下就爆发了。

七、NLP模型上线的坑

这次排查让我总结了NLP模型上线时需要注意的几个坑,分享给大家。

1. 训练和推理的预处理必须完全一致

这是最重要的一条。训练时怎么做预处理,推理时就必须怎么做,包括:

  • 文本清洗(过滤特殊字符、HTML标签等)
  • 归一化(NFKC、大小写转换、繁简转换等)
  • 分词器和分词参数
  • 截断和填充的方式
  • 最大序列长度

最好的做法是把预处理逻辑封装成一个统一的函数,训练和推理都调用同一个函数,从根本上保证一致性。不要训练写一套、推理写一套,迟早会出问题。

2. 测试数据要覆盖真实场景

我们的测试数据太干净了,都是标准的文本,没有emoji、特殊字符、生僻字、长文本。但真实用户的输入什么都有,测试数据必须覆盖这些边界情况。

建议在测试集中加入:

  • 包含emoji和特殊符号的文本
  • 全角字符、繁体中文
  • 非常短的文本(几个字)和非常长的文本(几千字)
  • 空文本、纯数字、纯符号
  • 各种语言混合的文本

只有在这些"脏数据"上测试通过了,模型上线才不会出问题。

3. 分词器要和模型匹配

分词器是NLP模型的重要组成部分,必须和模型一起保存、一起部署。不要用错版本,不要自己修改分词器的配置。

上线前要验证:用线上的分词器处理训练数据,看结果和训练时是否一致。如果不一致,说明分词器有问题。

4. 注意OOV(未登录词)问题

BERT的词表是固定的,遇到词表里没有的词会变成[UNK]。如果线上文本中有大量[UNK],模型效果会下降。

要监控线上数据的[UNK]比例,如果太高,说明词表覆盖不够,可能需要重新训练分词器或者用更大的词表。

5. 长文本处理要小心

BERT有最大序列长度限制,长文本必须截断。但截断方式很重要,要根据任务选择合适的截断策略:

  • 分类任务:一般保留前128或前512个token
  • 重要信息在后面的任务:可以用首尾截断(保留前N个和后N个token)
  • 长文档分类:可以用滑动窗口,分段预测再聚合

不要简单地按字符截断,一定要先分词再按token截断。

6. 线上要有完善的监控和日志

这次能排查出来,靠的是详细的日志。NLP模型上线后,一定要有完善的监控:

  • 记录原始文本和预处理后的文本
  • 记录分词结果和输入ID
  • 记录推理结果和置信度
  • 监控输入长度分布、[UNK]比例、置信度分布
  • 对低置信度的预测做抽样人工审核

有了这些监控,出了问题才能快速定位,而不是瞎猜。

八、经验和教训

这次通宵排查Bug,让我总结了几条经验。

1. 线上问题先找规律,不要瞎试

遇到复现不了的Bug,不要盲目改代码。先收集bad case,找规律,看错误的样本有什么共同特征。找到规律之后,再针对性地排查,效率会高很多。

2. 训练和推理的一致性是NLP的生命线

NLP模型对输入分布非常敏感,预处理的一点小差异,都可能导致结果完全不同。一定要保证训练和推理的预处理完全一致,最好用同一份代码。

3. 测试要贴近真实场景

实验室里的测试通过不算数,只有在真实用户的"脏数据"上测试通过了,才算真的通过。上线前一定要用真实数据做充分的测试。

4. 日志和监控是排查问题的眼睛

没有日志,线上问题就是黑盒,根本没法排查。上线前一定要把日志和监控做好,关键节点都要记录,出了问题才能快速定位。

5. 细节决定成败

这次Bug的根源都是一些细节问题:截断方式、emoji过滤、文本归一化。这些细节在开发时很容易忽略,但到了线上就会变成大问题。做NLP的,一定要对细节敏感。

九、写在最后

NLP模型的上线,不只是把模型文件部署到服务器上那么简单。预处理、分词、推理、监控,每一个环节都可能出问题。这次的Bug虽然让我熬了一夜,但也让我对NLP模型的工程化有了更深刻的理解。

算法工程师不能只关注模型指标,还要关注工程落地。模型在实验室里效果再好,上线之后出问题,等于白做。只有把训练、推理、监控、迭代整个流程都做好,模型才能真正发挥价值。

希望这次的排查经历能给做NLP的朋友一些参考,让你们少踩一些坑,少熬一些夜。如果你们也遇到过类似的线上Bug,欢迎在评论区分享交流。