中文 BERT 微调效果上不去?先别换模型,检查分词器和 max_len

🔑 关键词:自然语言处理,中文分词,BERT微调,tokenizer,max_len

📖 摘要:一个工单分类项目踩过的坑:BERT-base-chinese 的 vocab_size 21128、max_position_embeddings 512 怎么悄悄吃掉你的 F1,附四种输入处理方式的实测对比。

2021 年冬天我在一家做 SaaS 客服的公司打杂,接了个工单自动分类的活。8 个类别,3 万条人工标注的工单,最长的一条有两千多字,是客户把整份报错日志粘进来的。第一版基线是 jieba 分词 + TF-IDF + LinearSVC,跑出来 macro-F1 0.81。老板说能不能上深度学习,我就换成了 BERT-base-chinese 微调,learning rate 2e-5,batch size 32,max_len 128,跑完 0.86。

图片

然后就卡住了。

我加数据加到 4.5 万条,F1 到 0.865。把学习率从 2e-5 扫到 3e-5、5e-5,warmup 从 0.1 调到 0.2,最后落到 0.87 就不动了。组里第一反应都是「数据不够」或者「换个更大的模型」,有人建议上 RoBERTa-wwm-ext,有人建议做数据增强。我心里其实有点别扭——0.86 这个数字,和当年线性模型的 0.81 之间只差了 5 个点,而参数量差了差不多一千倍(LinearSVC 撑死几万个权重,BERT-base 是 1.1 亿)。

图片

后来干了一件很土的事:把喂进模型的 token 打印出来看。我贴的那条 2100 字工单,BERT 只吃了前 512 个 token,后面 1600 多字全扔了。更难受的是那 512 个 token 里,有三百多个是日志里的时间戳和十六进制哈希值,真正的业务描述在里面只占一小截。

这里得解释一下中文 BERT 的分词逻辑。BERT-base-chinese 的 vocab_size 是 21128,用的是 WordPiece,中文部分基本是按字切的——「南京市长江大桥」进去,出来是 南、京、市、长、江、大、桥 七个 token,再加 [CLS] 和 [SEP]。它的 max_position_embeddings 是 512,也就是说一条中文工单超过 510 个字就要被硬截断。作为参照,同样 512 个 token 在英文里大概能装 380 个单词。中文信息密度高,512 个 token 听着不少,但客户往往在工单最后才把真正的问题写清楚。

图片

于是我做了组对比测试。同一批 3000 条测试集,同一个 BERT-base-chinese,只改输入处理方式:

输入方式 平均 token 数 max_len macro-F1
BERT 原生按字切 187 512 0.870
jieba 粗粒度先切词、空格连接 124 512 0.834
按字切 + 正则剥掉日志块 156 256 0.903
RoBERTa-wwm-ext(vocab 同样 21128) 187 512 0.891

图片

第二行是最意外的一行。我本以为先分词能减少 token 数、让模型看到更多内容,结果掉了 3.6 个点。原因大概是 jieba 的切分错误会往下游传:它把「未收到」切成「未/收到」,语义就反了,而按字切虽然啰嗦,每个字本身不会错。这是中文分词的老毛病,字粒度没有这个问题,代价是序列变长、训练变慢。

第三行才是真正值钱的。没换任何模型参数,只用工单里的正则把用户贴的技术日志剥掉,max_len 从 512 降到 256,F1 从 0.870 跳到 0.903。训练时间还少了一半——512 长度下 2080Ti 跑一个 epoch 要 11 分钟,降到 256 之后 6 分钟出头。代码里就加了六行正则。

图片

再往后两年,行业整体换到了大模型那套。我用 Qwen 的 tokenizer 试过同一批中文工单,一个汉字大概消耗 0.6 到 1 个 token,它 vocab 有十五万上下,中文词表收得比较满。但如果拿 GPT-2 系 tokenizer 处理纯中文,情况就难看了——它是 byte-level BPE,一个汉字 UTF-8 编码占 3 个字节,经常被拆成 2 到 3 个 token,同一段话的 token 数直接是前者的两三倍。这不是模型能力问题,是词表覆盖问题,但它会直接变成你的 API 账单。我见过一个小团队用英文模型做中文问答,一个月 token 费比隔壁用国产模型的高出四倍,他们一直以为是调用量的问题。

还有个坑顺带提一句。Hugging Face 的 transformers 里 tokenizer 有 slow(Python)和 fast(Rust)两套实现,换实现的时候注意 truncation_side 这个参数,默认是 'right',改成 'left' 之后 [CLS] 的位置会变。我有一次为了调试把它们来回换,报错报了一下午,最后发现是两个实现下 align 对不上。

图片

所以我的看法有点不合主流:NLP 这几年的进步,很大程度不是模型更聪明了,而是把复杂度从算法搬到了工程和数据上。看 GLUE、CLUE 榜单,大家比的是 backbone 和参数量;但真落到业务里,决定 F1 的是那几件没人愿意干的事——把乱码特征字段剥掉、统一全角半角、删掉重复的问候语模板、把 max_len 设成一个合理的值。这些事不进论文,做完了也没法写进简历,但其中一个的收益可能顶得上你换三次模型。

我后来带过一个实习生,花三天调 learning rate 想从 0.88 冲到 0.90,没成功。我让他去看一眼数据里「客户等级」那一列有没有混进正文,他改完,0.91 了。他当时那个表情我现在还记得。

🏷️ 标签: