第 2 章 BERT 模型加载与推理
2.1 用 Hugging Face Transformers 加载预训练 BERT
我个人习惯在项目一开始就把 Transformers 和 Tokenizers 装好。你想想看,BERT 虽然名字里有“双向”,但本质上它还是一个可拆解的编码器。Hugging Face 帮我们把模型、配置、分词器都封装成了统一的接口,省了不少事。
加载一个预训练模型,其实就两行代码:
from transformers import BertTokenizer, BertModel
model_name = "bert-base-uncased"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)
我在项目中遇到过用户抱怨“下载太慢”的问题。其实你可以先手动下载到本地目录,然后指定 local_files_only=True。另外记得检查 cache 目录,默认在 ~/.cache/huggingface,容易把磁盘撑爆。
OSError。比如 "bert-base-uncased" 和 "bert-base-cased" 是不一样的。建议先访问 Hugging Face Model Hub 确认模型名称。
2.2 单条文本推理
单条测试是调试的第一步。我把过程拆成三步:分词 → 调模型 → 读输出。说白了就是“喂句子,取张量”。
def predict_single(text):
inputs = tokenizer(text, return_tensors="pt")
outputs = model(**inputs)
return outputs
text = "I love natural language processing."
result = predict_single(text)
print(result.last_hidden_state.shape) # (1, seq_len, 768)
这个 last_hidden_state 是所有 的最终表示。喂一个句子出来的是一个三维张量(batch=1, 长度, 768 维)。我刚开始时常把 pooler_output 和 last_hidden_state 搞混。前者是 [CLS] 经过一个全连接+tanh 的输出,用于分类任务;后者才是真正的上下文向量。
padding 和 truncation,单条推理没问题。但到批量时就会因为长度不齐而报错。后面会细讲。
2.3 批量推理
批量推理的核心是“同一个 batch 内所有句子必须等长”。怎么做?看代码:
texts = ["I love NLP.", "BERT is great for sentiment analysis.", "Hello world!"]
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
outputs = model(**inputs)
print(outputs.last_hidden_state.shape) # (3, max_len, 768)
padding=True 会自动把短句补到 batch 内最长的长度,truncation=True 会截断超过最大长度的部分。这里有个性能细节:如果你知道自己的数据最长不会超过 128,可以指定 max_length=128 避免浪费计算。
我曾经在一个线上服务里只用了 padding=True 没设 max_length,结果用户输入了一个 2000 字的评论,batch 内其他句子全被补到 2000,GPU 直接 OOM。嗯,这里要注意。
| 参数 | 作用 | 推荐设置 |
|---|---|---|
padding |
短句补 PAD | True 或 'max_length' |
truncation |
超长截断 | True |
return_tensors |
返回张量格式 | 'pt'(PyTorch) |
2.4 输出解析
我把模型输出的三大结构做个归纳:logits、hidden_states、attentions。前两个我们常用,最后一个虽然占用大但能帮我们理解 BERT 的注意力机制。下图是这三种输出在模型推理中的位置示意:
从图中可以看到:hidden_states 包含了每一层的输出(包括 embedding 层),一共 13 个(12 层 + embedding)。而 attentions 是每层的注意力权重矩阵,调试误码注入时特别有用——你可以直视 BERT 是否把注意力放在被破坏的 上。
2.4.1 logits
logits 只在带了分类头的模型里出现,比如 BertForSequenceClassification。我在做情感分析时,直接从 logits 取 argmax 就得到类别。不要用 softmax,直接用 torch.argmax(logits, dim=-1)。很多人把 logits 和概率混为一谈,其实 logits 就是 softmax 之前的原始分数。
from transformers import BertForSequenceClassification
model_cls = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
logits = model_cls(**inputs).logits
preds = torch.argmax(logits, dim=-1)
2.4.2 hidden_states
我需要提取中间层特征时(比如做对比学习或特征拼接),一定会在模型加载时设置 output_hidden_states=True。返回的是一个 tuple,长度 13(base 模型)。第 0 个是 embedding 层,第 1–12 个是各层输出。记住:第 0 个没有经过任何 Transformer。
2.4.3 attentions
attentions 的输出格式是 tuple of torch.FloatTensor,每个形状为 (batch_size, num_heads, seq_len, seq_len)。我在分析误码对注意力分布的影响时,喜欢画热力图。比如,误码位的注意力分数会急剧下降,说明 BERT 在“忽略”坏 。这个特性对后续注入策略设计很重要。
model = BertModel.from_pretrained("bert-base-uncased", output_attentions=True)
outputs = model(**inputs)
# outputs.attentions 是一个元组,每个元素对应一层
first_layer_attn = outputs.attentions[0] # (1, 12, 5, 5)
关键点总结
- 单条推理:分词后直接调模型,注意 tensor 维度。
- 批量推理:必须
padding=True,建议配合max_length避免 OOM。 - 输出解析:
last_hidden_state最常用,hidden_states用于多层特征,attentions用于可视化注意力。
嗯,这一节的内容就到这里。代码你都试跑过了吗?如果还没有,建议开个 Jupyter 从头敲一遍——尤其是打印张量形状那一步,能帮你建立直觉。后面的章节会基于今天学的推理流程,一步步植入误码。别着急,先把这个基础打牢。