简介:面向中文自然语言处理学习与研发的预训练模型资源,基于哈工大讯飞联合实验室发布的Chinese-BERT-wwm-ext版本,专为PyTorch框架封装。该模型采用全词掩码策略,对中文词汇表做了针对性优化,相比原版BERT更注重语义完整性,在文本分类、命名实体识别、情感分析、阅读理解等多种下游任务上表现更优。压缩包内共包含三个文件,模型权重文件为约三百六十七兆字节的二进制权重,中文词表与配置文件则记录分词信息和网络结构参数,解压后配合transformers库即可完成加载、微调与特征提取,省去自行转换模型的步骤。已有两千五百五十四人浏览学习,适合NLP算法工程师、科研人员和学生快速搭建中文预训练基线;同时也可作为学习全词掩码改进思路的参考样本,兼具实用价值与教学意义。 如果你最近在做中文 NLP,那么大概率会在某个开源项目或者同事的网盘里遇到一个叫chinese-bert-wwm-ext.rar的压缩包。我第一次拿到这份文件时也愣了一下,文件名看起来像一串参数,解压之后才发现,这是哈工大讯飞联合实验室发布的中文 BERT 全词掩码扩展模型,也是很多中文文本分类、实体识别、语义匹配项目的默认底座。chinese-bert-wwm-ext解决的核心痛点很直接:原版中文 BERT 按单字 mask,训练时学不到完整的词级语义;这个模型在预训练阶段改成整词掩码,又用更大规模的中文语料做了扩展训练,所以下游效果普遍更好。这篇文章不聊花哨的概念,就从这个 rar 包本身出发,带你走一遍解压、加载、微调和排错的完整流程,适合刚开始用中文预训练模型的开发者。
1. 压缩包里装了什么,为什么它比原版 BERT 更懂中文
1.1 全词掩码的改动到底改在哪里
BERT 预训练阶段的常见任务,是随机把输入里的一部分 token 遮住,再让模型根据上下文去预测被遮住的内容。对英文来说,token 一般是 WordPiece 子词,被遮住的是子词片段;对中文来说,主流的bert-base-chinese直接按字切分,一个汉字就是一个 token。这就带来一个隐蔽的问题:模型在预训练时看到的“掩码单位”是字,而不是词。
举个例子,句子“我喜欢哈尔滨工业大学”,如果随机选中“哈”字并把它遮住,模型只需要根据“尔滨工业大学”猜出这个位置大概率是一个“哈”字,整个过程并不需要理解“哈尔滨工业大学”是一个完整的地名或机构名。全词掩码(Whole Word Masking)的做法是,一旦某个字被选中,就把同一个词里的所有汉字一起遮住。比如选中“尔”,那“哈尔滨”三个字会同时被掩码,模型需要结合上下文还原整个词,这迫使它去学习中文里的词边界、词内字组合和跨上下文语义。
chinese-bert-wwm-ext里的wwm就是这个机制,ext是 extended 的意思,代表比最早版本用更大规模的语料、更长的训练步数做扩展训练。这个思路最早是 Google 在英文 BERT 上验证的,哈工大讯飞联合实验室把它搬到了中文,做成了多个版本,chinese-bert-wwm-ext是目前最常用的一个。从实际效果看,它不一定在所有任务上都吊打原版,但在绝大多数中文任务上比bert-base-chinese稳定高零点几个点到两三个点,尤其是词义消歧、命名实体识别这些对词语边界敏感的任务,提升会明显一些。
1.2 rar 包里的文件长什么样
模型压缩包解压之后,一般会是一组标准文件。不同渠道打包可能略有差异,但核心文件逃不出下面这几个:
| 文件名 | 作用 | 典型大小 |
|---|---|---|
bert_config.json | 模型结构参数,包括层数、隐藏维数、注意力头数、词表大小 | 几百字节 |
vocab.txt | 中文词表,包含[PAD]、[CLS]、[SEP]、[MASK]等特殊 token 和汉字 | 约 1MB |
pytorch_model.bin | PyTorch 格式的权重文件 | 约 400MB |
tf_model.h5 | TensorFlow 格式的权重文件 | 约 400MB |
model.ckpt-* | TensorFlow 1.x 的 checkpoint 文件,部分打包会带上 | 约 1.3GB |
这里最容易踩坑的是bert_config.json和config.json的命名问题。Hugging Face Transformers 在加载模型时默认去目录里找config.json,而很多从原仓库直接打包的文件叫bert_config.json。直接用from_pretrained会报ConfigNotFoundException。我的习惯是拿到文件后先看一眼,如果是bert_config.json,就复制一份为config.json,反正内容完全一致,这一步能省掉后面的很多麻烦。
vocab.txt是中文词表,里面是[UNK]、[SEP]、[PAD]、[CLS]、[MASK]加上汉字。注意这个版本的词表是字粒度的,不是词粒度,所以输入文本时不需要先做分词,直接让 tokenizer 按字切就可以。这和后面要说的加载方式直接相关。
2. 解压和部署:先把准备工作做扎实
2.1 建议的 Python 环境和依赖
chinese-bert-wwm-ext本身不会要求特别新的库,只要 Transformers 和深度学习框架能跑起来,基本都能加载。我自己的常用环境是 Python 3.8,PyTorch 1.12 或 2.0,Transformers 4.21 以上。如果你用 TensorFlow,也不要低于 2.4,否则部分 API 和权重加载逻辑对不上。
建议直接新建一个独立环境,避免和别的项目相互污染依赖版本:
conda create -n wwm python=3.8 -y conda activate wwm pip install torch transformers datasets scikit-learn如果只做 CPU 推理,不装 CUDA 版 PyTorch 也能跑,但微调训练会非常慢。我有一次在笔记本 CPU 上跑微调,一个 3000 条的小数据集,一个 epoch 跑了快四十分钟,后来换 GPU 几分钟就完事。所以在准备环境之前先想清楚:你是只抽取特征,还是要做 fine-tune。前者 CPU 能接受,后者建议直接上 GPU。
2.2 解压重命名与目录结构
解压.rar文件时,中文文件名乱码是一个很常见的问题,尤其模型文件本身是纯英文,不会出问题,但外层目录或者说明文档如果是中文,Windows 下直接右键解压可能乱码。Linux 下建议用unar,对编码的识别比unrar更友好;Windows 下我用 Bandizip 选“自动检测编码”也基本能解决。
解压后建议把模型放到一个统一管理的目录里,比如:
models/ └── chinese-bert-wwm-ext/ ├── bert_config.json ├── vocab.txt ├── pytorch_model.bin └── tf_model.h5目录路径最好不要有中文和空格。虽然 Transformers 的from_pretrained技术上能处理路径,但一些底层文件操作偶尔会因为路径转义出问题,没必要给自己埋雷。目录内部的文件命名尽量保持和官方仓库一致,尤其是权重文件,不要随手改成model.bin或者bert.pt,因为 Transformers 默认会找pytorch_model.bin或tf_model.h5。
如果你拿到的压缩包里只有bert_config.json,这时候补一个config.json:
cd models/chinese-bert-wwm-ext cp bert_config.json config.json这一步做好之后,模型目录就基本满足from_pretrained的加载要求了。
3. 加载模型的三条实际路线
3.1 用 Hugging Face Transformers 加载,最快
大多数场景下,直接用 Transformers 加载是最省事的。代码只需要两行:
from transformers import BertTokenizer, BertModel model_dir = "models/chinese-bert-wwm-ext" tokenizer = BertTokenizer.from_pretrained(model_dir) model = BertModel.from_pretrained(model_dir) model.eval()chinese-bert-wwm-ext使用的是标准 BERT 结构,所以用BertTokenizer和BertModel就能对上,不需要额外指定AutoModel和AutoTokenizer。调用模型时,输入要先经过 tokenizer 处理:
import torch text = "哈尔滨工业大学在自然语言处理领域很有名。" inputs = tokenizer(text, return_tensors="pt", max_length=64, truncation=True) with torch.no_grad(): outputs = model(**inputs) last_hidden = outputs.last_hidden_state # shape: [1, seq_len, 768] cls_vec = outputs.pooler_output # shape: [1, 768]这里有个容易混淆的点:chinese-bert-wwm-ext虽然叫“全词掩码”,但推理阶段并不需要你告诉模型哪些字属于一个词。全词掩码只是预训练时的掩码策略,模型训练完已经学会了词级语义,推理时输入仍然是普通字符序列。所以你完全不需要提前用 jieba 分词,直接丢给 tokenizer 就好。
3.2 PyTorch 原生加载权重
如果你不想依赖 Transformers 的自动加载逻辑,也可以用 PyTorch 原生方式加载权重。这种做法的好处是可控性强,比如你想在加载前对 state dict 做前缀清洗,或者只是不想引入过多的抽象层。
import torch from transformers import BertConfig, BertModel config = BertConfig.from_pretrained("models/chinese-bert-wwm-ext") model = BertModel(config) state = torch.load("models/chinese-bert-wwm-ext/pytorch_model.bin", map_location="cpu") # 如果发现 key 里有 module. 前缀,需要去掉 state = {k.replace("module.", ""): v for k, v in state.items() if not k.startswith("_")} model.load_state_dict(state)新手最常遇到的问题是在多卡训练或某些保存逻辑影响下,权重文件的 key 带了module.前缀,直接load_state_dict会报unexpected key或missing key。上面的代码已经做了兼容处理。加载完模型后,同样先model.eval()再推理。
还有一个细节:torch.load加载出来的 state dict 有时包含_metadata这类非权重键,用if not k.startswith("_")过滤掉,避免意外报错。
3.3 用 TensorFlow 加载 h5 权重
项目里如果用 TensorFlow,加载方式也类似,只是类名换成TFBertModel:
from transformers import TFBertModel, BertTokenizer model_dir = "models/chinese-bert-wwm-ext" model = TFBertModel.from_pretrained(model_dir, from_tf=True) tokenizer = BertTokenizer.from_pretrained(model_dir)如果你手上只有pytorch_model.bin,想拿到 TensorFlow 模型,可以加一个from_pt=True参数,Transformers 会帮你把 PyTorch 权重转成 TensorFlow 权重。反过来,如果你只有tf_model.h5,想加载 PyTorch 模型,用BertModel.from_pretrained(..., from_tf=True)也可以自动转换。
这条转换逻辑看起来方便,但有个注意点:转换是在内存里完成的,加载时模型文件会被读两遍,如果服务器内存不足,转换过程可能被系统杀掉。我碰到过一次训练服务器上有 GPU 但内存只有 8GB,转换时直接 OOM,后来换到内存更大的机器才解决。如果你的内存比较紧张,建议还是直接采用对应框架的官方权重,别在内存里折腾转换。
4. 下游任务微调:文本分类和序列标注
4.1 文本分类的实操套路
用chinese-bert-wwm-ext做文本分类,最简单的方式是直接使用BertForSequenceClassification。这个类会在 BERT 顶层加一个分类头,微调时只需要更新整个模型和分类头的参数。
from transformers import BertForSequenceClassification, BertTokenizer, AdamW from torch.utils.data import DataLoader, Dataset import torch tokenizer = BertTokenizer.from_pretrained("models/chinese-bert-wwm-ext") model = BertForSequenceClassification.from_pretrained( "models/chinese-bert-wwm-ext", num_labels=2 ) class CLSDataset(Dataset): def __init__(self, texts, labels): self.data = tokenizer(texts, padding=True, truncation=True, max_length=128, return_tensors="pt") self.labels = torch.tensor(labels) def __len__(self): return len(self.labels) def __getitem__(self, i): return {k: v[i] for k, v in self.data.items()}, self.labels[i] train_ds = CLSDataset(["这个产品很好用", "体验很差"], [1, 0]) loader = DataLoader(train_ds, batch_size=8) opt = AdamW(model.parameters(), lr=2e-5) model.train() for inputs, labels in loader: opt.zero_grad() outputs = model(**inputs, labels=labels) loss = outputs.loss loss.backward() opt.step()微调 BERT 时,学习率是最敏感的超参数之一。2e-5是比较稳妥的起点,比它大容易训练不稳定,比它小收敛很慢。max_length也不是越大越好,短文本任务用 64 或 128 就够,长文本最多用到 512,再长就需要切段或者改用 Longformer 这类模型。batch_size同理,显存不够时调到 4 或 8,配合梯度累积来做。
4.2 序列标注的模型替换
做命名实体识别或词性标注,只需要把模型类换成BertForTokenClassification。因为中文 BERT 按字切分,输入序列里每个 token 对应一个位置,所以标签也要按字对齐。比如“哈尔滨”三个字分别标为B-LOC、I-LOC、I-LOC,一个都不能少。
from transformers import BertForTokenClassification model = BertForTokenClassification.from_pretrained( "models/chinese-bert-wwm-ext", num_labels=len(tag2id) )训练循环和分类基本一致,唯一需要注意的是[CLS]和[SEP]这两个特殊位置在计算 loss 时要忽略。常见做法是把这两个位置的标签设为-100,PyTorch 的交叉熵会自动忽略-100。我在项目里经常用tokenizer.encode_plus(..., return_offsets_mapping=True)来辅助对齐,先把 token 和原始文本的字符位置映射关系拿到,再生成标签序列,这样不容易错位。
全词掩码模型在序列标注里优势比较明显,特别是实体边界识别。因为预训练时模型见过整词掩码,对词边界的敏感度比原版更强,在验证集上 NER 的 F1 通常能比bert-base-chinese高 0.5 到 1.5 个百分点。
4.3 轻量特征抽取方案
如果不想做微调,只把模型当成一个文本编码器,可以直接抽取特征。这种方式适合数据量小、或者只是想快速跑一个基线对比的场景。
with torch.no_grad(): outputs = model(**inputs) hidden = outputs.last_hidden_state # 取 [CLS] 向量作为整句表示 sentence_emb = hidden[:, 0, :].detach().cpu().numpy()[CLS]向量是 BERT 在预训练时用来聚合整句语义的,可以直接用于下游分类或聚类。如果要做语义匹配,也可以用最后一层所有 token 的均值池化,我实际对比过,平均池化在短文本相似度任务上往往比[CLS]更稳定一点,因为[CLS]本身没有经过专门的下游训练时,信息可能只集中在前面几个 token。
要注意,抽取特征时一定加上torch.no_grad(),否则模型会默认保存梯度计算图,显存很快就被撑爆,而且推理速度严重下降。如果 batch 比较大,还可以在tokenizer的参数里加上padding=True,让每个 batch 内的文本统一长度,但不要在整个数据集上都 padding 到 512,那样会浪费大量显存。
5. 踩坑记录:加载和微调常见问题
5.1 权重加载报 mismatch
我在实际使用中,遇到最多的报错是size mismatch for bert.embeddings.word_embeddings或者unexpected key。前者通常是 config 和权重文件不匹配,比如vocab_size对不上;后者通常是 state dict 带了module.前缀。如果你用多卡保存过模型,再单卡加载时就要先去掉前缀:
state = {k.replace("module.", ""): v for k, v in torch.load("pytorch_model.bin", map_location="cpu").items() if not k.startswith("_")} model.load_state_dict(state)如果是vocab_size不一致,检查一下是不是加载了其他模型的 config。chinese-bert-wwm-ext的词表大小是 21128,如果你从网上下载的版本基于额外词表微调过,vocab.txt可能不一样,这时候不要硬用官方 config,直接用压缩包里的bert_config.json去加载。
5.2 模型目录没有 config.json
报错信息类似ConfigNotFoundException: models/chinese-bert-wwm-ext does not appear to have a file named config.json。原因很简单:压缩包内只有bert_config.json。解决办法就是复制一份:
cp models/chinese-bert-wwm-ext/bert_config.json models/chinese-bert-wwm-ext/config.json这个坑我帮朋友排查过很多次,每次都是同一个原因。拿到模型包之后先检查文件结构,缺什么补什么,比等到加载报错再回来改要快得多。
5.3 显存不足和训练速度优化
微调 BERT 最烦的就是 OOM。如果你的显卡只有 6GB 或 8GB 显存,batch_size=16加max_length=128很可能直接爆掉。可以考虑下面几个方向:
- 把
batch_size改到 4 或 8,再用梯度累积模拟大 batch; - 给训练循环加上自动混合精度(
torch.cuda.amp),显存占用能降低不少; - 使用动态 padding,也就是在
collate_fn里对当前 batch 的文本统一长度,而不是所有样本都 padding 到max_length; - 推理阶段一定加
torch.no_grad(),并且优先用half()把模型转成半精度。
梯度累积的代码很简单,就是每 N 个 step 才更新一次参数,但不建议把累积步数设得太大,容易让优化器对学习率的估计失真。
5.4 中文预处理的隐藏坑
chinese-bert-wwm-ext的 tokenizer 虽然有中文词表,但它仍然是一个字粒度 tokenizer,所以不要把 jieba 分词的结果再丢给它。如果你提前分好词,再用空格连接,tokenizer 会把空格也当成一个 token,导致序列变长,而且 label 对齐会全乱。正确做法是直接把原始文本传给tokenizer。
另外,文本里的全角数字、全角标点最好先统一转成半角,换行符替换成空格。vocab.txt里本身有全角字符的位置,但统一转半角能减少[UNK]的出现,也能避免一些同义字符被当成两个完全不同的 token。如果你处理的是用户评论这类噪声很大的文本,还要考虑把连续空格合并,避免 token 序列里出现大量无意义空格 token。
5.5 效果对比的参考
因为模型效果和任务、数据、超参数都有关,我不给绝对数字,只能说一个我在多个项目里观察到的相对范围:
| 任务 | 相对bert-base-chinese提升 |
|---|---|
| 文本分类 | 0.5 ~ 1.5 个点 |
| 命名实体识别 | 0.5 ~ 2.0 个点 |
| 语义匹配/文本相似度 | 0.3 ~ 1.0 个点 |
这个范围不是官方 benchmark,但是如果你做 baseline 对比时发现没有任何提升,先别急着怀疑模型,优先检查数据处理和训练超参。
我在实际项目里一直把chinese-bert-wwm-ext当作中文 BERT 系模型的首选基线,原因很简单:它兼容 Hugging Face 生态,加载方式和原版完全一致,又能明显提升下游任务,替换成本几乎为零。最后分享一个保存经验:微调完模型后不要只存一个pytorch_model.bin,把config.json、vocab.txt和tokenizer_config.json放在同一个目录,下次加载时再也不用到处补文件。遇到加载报错,先检查文件命名,十次里有八次都是config.json缺失或者名字不对。希望这篇内容能让你少踩几个坑。
本文还有配套的精品资源,点击获取