BERT实战|推理与微调
如果需要评估指标,可以自定义评估函数,传入trainer作为参数compute_metrics# 如果你没有定义compute_metrics,那么:# trainer.evaluate()默认只会返回loss(以及epoch等训练状态信息),比如:{'eval_loss': 0.45, 'epoch': 1.0}
BERT
BERT不能像Word2Vec那样直接“查表”得到词向量,而是需要将整个句子输入模型,通过模型的前向计算得到句子中每个词(或整个句子)的上下文相关向量表示。
- 计算资源:BERT模型较大,在CPU上运行可能较慢。如果处理大量数据,考虑使用GPU或更小的模型(如
distilbert-base-uncased
)。 - 微调 (Fine-tuning) vs. 特征提取 (Feature Extraction):
- 特征提取:如上所示,冻结BERT的权重,只将其作为一个固定的“特征提取器”。提取出的向量可以输入到另一个分类器(如SVM、随机森林)中进行训练。适合数据量较小或计算资源有限的场景。
- 微调:解冻BERT的权重,并在你的评论数据集上对其进行端到端的训练。这通常能获得最佳性能,但需要更多的数据和计算资源。
- 模型选择:根据你的评论语言选择模型。
- 中文评论:
bert-base-chinese
- 英文评论:
bert-base-uncased
(不区分大小写) 或bert-base-cased
(区分大小写) - 其他语言:Hugging Face Hub上提供了多种语言的预训练模型。
- 中文评论:
- 序列长度:BERT最大序列长度一般为512。长文本需要截断或采用其他策略(如分段处理再聚合)。
通过上述方法,你就能有效地将任意长度的评论文本转化为一个固定长度的、富含语义信息的特征向量,进而用于各种下游机器学习任务。
一、特征提取(直接推理)步骤详解
1. 预处理文本 (Preprocessing)
将原始评论文本处理成BERT模型需要的格式:添加特殊标记([CLS], [SEP])、分词(使用BERT的Tokenizer)、生成输入ID和注意力掩码(Attention Mask)。
2. 模型前向传播 (Forward Pass)
将处理好的输入数据送入预训练的BERT模型。模型会输出一个隐藏状态(Hidden States)元组,其中包含了所有层的所有词元的向量表示。
3. 选择特征向量策略 (Strategy for Feature Extraction)
这是最关键的一步。你需要决定从BERT的哪个位置获取什么样的向量来作为整个评论的“特征向量”。主要有以下几种策略:
策略 | 做法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
CLS标记向量 | 取最后一层输出中第一个位置(即[CLS] 标记)对应的向量。 |
简单、通用。BERT设计之初就用[CLS] 汇聚整个序列的信息用于分类。 |
对于某些任务,直接使用[CLS] 向量效果可能不是最优。 |
文本分类、情感分析等句子级任务。 |
平均池化 (Mean Pooling) | 取最后一层所有词元(不包括特殊标记)向量的平均值。 | 考虑了所有词的信息,更稳定。 | 可能会引入一些无关词的噪声。 | 通用性强的句子表示,常用于语义相似度、信息检索。 |
最大池化 (Max Pooling) | 取最后一层所有词元向量在每个维度上的最大值。 | 能捕捉最显著的特征。 | 可能会丢失大量信息。 | 较少使用,有时在特定任务上有效。 |
使用中间层 | 不取最后一层(第12层或24层),而是取倒数第二层等中间层的输出。 | empirically,某些任务上中间层的表示可能更具通用性。 | 需要实验确定哪一层最好。 | 当发现最后一层过拟合到预训练任务时尝试。 |
对于评论文本的情感分析或分类任务,最常用且通常最有效的策略是直接使用 [CLS]
标记的向量或进行平均池化。
二、代码实现
以下是一个使用PyTorch和Hugging Face transformers
库的示例代码。
0. 导入库
#### 安装必要的库
#### pip install transformers torch
import torch
from transformers import AutoTokenizer, AutoModel
import numpy as np
1.导入预训练模型的方式
(1) 从 Hugging Face Hub 直接加载
最常见的方式,输入 模型名称(ID):
from transformers import AutoTokenizer, AutoModel
model_name = "bert-base-uncased" # Hub 上的模型名
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
- 第一次运行会联网下载到本地缓存(默认在
~/.cache/huggingface/transformers/
)。 - 以后再调用同样的
model_name
时会直接用缓存,不会重复下载。
(2) 从本地路径加载
如果你已经把模型下载/保存到本地,直接用 目录路径:
local_model_path = "./bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(local_model_path)
model = AutoModel.from_pretrained(local_model_path)
- 目录里需要包含模型文件,如
config.json
,pytorch_model.bin
,tokenizer.json
等。 - 适合离线环境或不想重复联网下载的情况。
(3) 先下载再保存,再从本地加载
如果你要在离线环境用,可以先在有网环境下保存:
# 第一步:下载并保存
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
tokenizer.save_pretrained("./bert-base-uncased")
model.save_pretrained("./bert-base-uncased")
# 第二步:离线加载
tokenizer = AutoTokenizer.from_pretrained("./bert-base-uncased", local_files_only=True)
model = AutoModel.from_pretrained("./bert-base-uncased", local_files_only=True)
# 选择模型并加载预训练的Tokenizer和Model
model_name = 'bert-base-chinese' # 例如:中文base模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# 将模型设置为评估模式(关闭dropout等)
# model.eval() 并不会停止梯度计算,它只改变模型内部某些层的行为。
model.eval()
2. 准备输入文本
任务类型 / 文件类型 | 输入数据形式 | Tokenizer 使用方式 | 注意事项 | 推理方法 |
---|---|---|---|---|
单句子任务(情感分析 / 分类) | 文件:每行一句评论或句子 | tokenizer(list_of_sentences, padding=True, truncation=True, return_tensors="pt") |
- 可设置 max_length 防止超长 - attention_mask 自动处理 [PAD] - [CLS] 用作分类向量 |
model.eval() + torch.no_grad() |
句子对任务(NLI / 语义相似度 / QA) | 文件1、文件2:每行对应一对句子 | tokenizer(list1, list2, padding=True, truncation=True, return_tensors="pt") |
- 两个列表长度必须一致 - tokenizer 自动添加 [CLS] 和 [SEP] - token_type_ids 用于区分句子 |
model.eval() + torch.no_grad() |
多条独立评论 / 文本批量处理 | 文件:每行一句,数量很大 | - 可直接传列表一次性编码 - 或用 Dataset + DataLoader(batch_size=N) 分批 |
- batch_size 根据显存调整 - padding=True 补齐到 batch 内最长 - 对长文本可设置 truncation=True |
每批次用 with torch.no_grad() 前向推理 |
长文本 / 文档向量化 | 文件:每行一条长文本 | - 可以手动拼接 [SEP] 连接多个句子 - 或直接传长文本列表 |
- BERT 最大长度 512 token - 超长可用滑动窗口 stride |
均值池化或 CLS 池化提取向量,注意排除 [PAD] / [CLS] / [SEP] |
句子对批量长文本 | 两个文件,每行对应一对长文本 | tokenizer(list1, list2, padding=True, truncation=True, max_length=512, return_overflowing_tokens=True) |
- 需处理 overflowing_tokens - 分批次处理 |
CLS/均值池化或分类输出 |
这里我们以少量句子texts为例:
texts = [
"今天你干嘛了?",
"今天心情很差,因为工作太多了,但晚上朋友陪我聊了聊,感觉好多了。",
"明天一起吃饭吗?"
]
3. 预处理:分词并编码
以下是长文本的处理方式:
情况1:截断
encoded_input = tokenizer(texts, max_length=128, truncation=True, padding=True)
-
BERT 的最大输入长度是 512 token
-
如果
max_length < 512
→ 文本会被截断到该长度 -
如果
max_length > 512
→ token 超过 BERT 支持长度会报错
-
-
如果你的文本很长(比如一篇文章有 1000 个 token):
[CLS] token1 token2 ... token512 [SEP] ← BERT 只能处理前 512
-
直接截断 → 后面的 token 会被丢弃 → 文本信息丢失
情况2:使用滑动窗口
encoded = tokenizer(
long_text,
truncation=True,
max_length=512,
stride=128,
return_overflowing_tokens=True # 返回所有片段
)
print(len(encoded["input_ids"])) # 片段数量
- 思路:把长文本切成多个 重叠的小片段,每个片段长度 ≤ 512
- Overlap(stride):每个片段和上一个片段重叠一部分 token,保证连续性
- 这样 BERT 可以处理全部文本,不会丢失重要信息
- 处理完后需要对片段的输出做聚合(如取平均、CLS pooling、加权等)才能得到整篇文本表示
示意:
文本长度: 1000 token
max_length=512
stride=128
片段1: token 0-511
片段2: token 384-895 (128 重叠)
片段3: token 768-999
-
encoded
会返回一个字典:encoded.keys() # ['input_ids', 'attention_mask', 'overflow_to_sample_mapping']
-
其中
overflow_to_sample_mapping
很关键:- 它是一个 list,长度 = 片段总数
- 每个元素是原始样本在输入列表中的索引
- 通过它,你可以知道哪些片段属于同一条原始文本
示例:
encoded = tokenizer(
["长文本1", "长文本2"],
truncation=True,
max_length=512,
stride=128,
return_overflowing_tokens=True
)
print(encoded.overflow_to_sample_mapping)
# 输出可能是 [0,0,1,1] → 表示前两个片段属于文本0,后两个片段属于文本1
- 得到模型输出后,通过
overflow_to_sample_mapping
聚合同一条文本的片段:
all_vectors = []
for i, sample_idx in enumerate(encoded.overflow_to_sample_mapping):
cls_vector = outputs[i][:,0,:] # CLS token
if sample_idx >= len(all_vectors):
all_vectors.append([cls_vector])
else:
all_vectors[sample_idx].append(cls_vector)
# 每条原文本取平均得到最终向量
final_vectors = [torch.mean(torch.stack(vectors), dim=0) for vectors in all_vectors]
# 使用截断的方式编码文本
encoded_input = tokenizer(
texts,
padding=True, # 补齐到 batch 内最长
truncation=True, # 截断到模型最大长度
max_length=512, # 设置最大长度
return_tensors='pt' # 返回PyTorch张量
)
# encoded_input 包含:
# input_ids: 词元对应的ID
# attention_mask: 注意力掩码(1表示真实词元,0表示填充词元),标记 padding([PAD]
# 101 = [CLS] → 整个序列的开始符,用于分类等任务的聚合表示。
# 102 = [SEP] → 序列分隔符或结束符。
print(encoded_input)
print(encoded_input["input_ids"].shape)
# >>> torch.Size([3, N]) # batch_size=3,每句长度补齐到 N
4. 前向传播
场景:有一个文件,里面包含很多句子,需要批量处理(例如评论、问答对、句子对等)。
(1) 读取文本文件
假设文件每行一条句子:
file_path = "comments.txt"
with open(file_path, "r", encoding="utf-8") as f:
sentences = [line.strip() for line in f if line.strip()]
print(f"共读取 {len(sentences)} 条句子")
- 每行去掉空行
- 得到
sentences
→ 一个字符串列表
(2) 全量编码(少量句子任务)
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
encoded = tokenizer(
sentences, # 列表
padding=True, # 补齐到 batch 内最长
truncation=True, # 超长截断
max_length=128, # 可根据显存调整
return_tensors="pt"
)
print(encoded["input_ids"].shape) # [batch_size, seq_len]
- 如果句子很多,建议 分批处理,避免显存爆掉
(3) 批量处理(DataLoader 示例)
import torch
from torch.utils.data import DataLoader, TensorDataset
# 假设 encoded["input_ids"], encoded["attention_mask"] 已经生成
dataset = TensorDataset(encoded["input_ids"], encoded["attention_mask"])
dataloader = DataLoader(dataset, batch_size=32, shuffle=False)
for batch in dataloader:
input_ids, attention_mask = [b.to("cuda") for b in batch]
with torch.no_grad():
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits
batch_size
根据显存调整- 每次处理一批句子,模型输出对应批次的 logits
(4)TensorFlow 版本
import tensorflow as tf
from transformers import TFAutoModelForSequenceClassification, AutoTokenizer
# 1. 选择模型和tokenizer
model_name = "bert-base-chinese" # 示例
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = TFAutoModelForSequenceClassification.from_pretrained(model_name)
# 2. 准备数据
texts = [
"今天你干嘛了?",
"我去打篮球了。",
"明天一起吃饭吗?"
]
# 编码
encoded = tokenizer(
texts,
padding=True,
truncation=True,
max_length=128,
return_tensors="tf" # 注意这里用 "tf"
)
# 3. 转换为 TensorFlow Dataset
dataset = tf.data.Dataset.from_tensor_slices((
{
"input_ids": encoded["input_ids"],
"attention_mask": encoded["attention_mask"]
}
))
# 批处理
batch_size = 32
dataset = dataset.batch(batch_size)
# 4. 推理
for batch in dataset:
# outputs = model(inputs, training=False) 模型默认在推理
outputs = model(batch) # 直接传入字典
logits = outputs.logits
print("logits shape:", logits.shape)
关键区别 vs PyTorch
步骤 | PyTorch | TensorFlow |
---|---|---|
张量格式 | return_tensors="pt" |
return_tensors="tf" |
Dataset | TensorDataset |
tf.data.Dataset.from_tensor_slices |
DataLoader | DataLoader |
.batch() |
推理调用 | model(input_ids=..., attention_mask=...) |
model(batch) (可以直接传 dict) |
batch_size
依旧要根据显存调整- 在 TF 中不需要手动
with torch.no_grad()
,因为 Keras 默认在推理时不计算梯度
#### 少量数据可以一次通过时
# 在 Python 中,** 用于把字典展开为关键字参数(keyword arguments)。
# d = {"a": 1, "b": 2}
# f(**d) # 等价于 f(a=1, b=2)
with torch.no_grad():
outputs = model(**encoded_input)
5. 获取模型输出
# outputs.last_hidden_state: 最后一层隐藏状态,形状为 (batch_size, sequence_length, hidden_size)
# outputs.pooler_output:通常是由最后一层[CLS]标记经过一个线性层和tanh激活函数后的结果,形状为 (batch_size, hidden_size)
# 这是 BERT 预训练时 Next Sentence Prediction (NSP) 用到的那个向量。
last_hidden_states = outputs.last_hidden_state
pooler_output = outputs.pooler_output
print(last_hidden_states.shape)
print(pooler_output.shape)
6. 选择策略获取句子特征向量
# 策略一:直接使用 [CLS] 标记的向量 (最简单常用)
cls_embedding = last_hidden_states[:, 0, :] # 取batch中所有序列的第一个词元的向量
cls_embedding = cls_embedding.squeeze().numpy() # 转换为numpy数组
print(f"原始文本: {texts}")
print(f"[CLS] 向量维度: {cls_embedding.shape}")
# 策略二:平均池化 (更稳健)
# 首先排除特殊标记([PAD])的位置,通常通过attention_mask来识别
attention_mask = encoded_input['attention_mask']
# 将attention_mask扩展为与last_hidden_states相同的维度以便于计算
# unsqueeze(dim) 会在指定维度插入一个大小为 1 的维度
# expand 会把大小为 1 的维度 广播到目标维度大小
input_mask_expanded = attention_mask.unsqueeze(-1).expand(last_hidden_states.size()).float()
# 将隐藏状态与掩码相乘,将填充位的向量置为零
sum_embeddings = torch.sum(last_hidden_states * input_mask_expanded, 1)
# 计算每个序列的真实长度(防止除以零)
# torch.clamp 用于 将张量的数值限制在指定范围内。
sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
mean_embeddings = sum_embeddings / sum_mask
mean_embedding = mean_embeddings.squeeze().numpy()
print(f"平均池化向量维度: {mean_embedding.shape}")
# 输出:通常是 (768,)
三、大量文本时的代码流程
import torch
import numpy as np
import pandas as pd
from torch.utils.data import DataLoader, TensorDataset
from transformers import AutoTokenizer, AutoModel
# 选择模型并加载预训练的Tokenizer和Model
model_name = 'bert-base-chinese' # 例如:中文base模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
可以自己创造一个数据集/使用自己有的数据集,这里我使用的是一个只有一列6000多行的中文评论数据集:
# 读取 Excel 文件
df = pd.read_excel("/content/sample_data/去哪儿采集的评论内容.xlsx")
# 提取评论列,确保是字符串
texts = df["评论内容"].astype(str).tolist()
# 编码
encoded_input = tokenizer(
texts,
padding=True,
truncation=True,
max_length=512,
return_tensors='pt'
)
# DataLoader
dataset = TensorDataset(encoded_input["input_ids"], encoded_input["attention_mask"])
dataloader = DataLoader(dataset, batch_size=32, shuffle=False)
# PyTorch 默认是在 CPU,你需要明确指定设备:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
cls_embeddings = []
for batch in dataloader:
print("1")
input_ids, attention_mask = [t.to(device) for t in batch]
with torch.no_grad():
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
# 取 [CLS] 向量
cls_batch = outputs.last_hidden_state[:, 0, :] # shape: (batch_size, hidden_size)
cls_batch = cls_batch.detach().cpu().numpy() # 转 numpy
cls_embeddings.append(cls_batch)
# 拼接所有 batch
cls_embeddings = np.vstack(cls_embeddings) # shape: (num_texts, hidden_size)
# 保存
np.save("cls_embeddings.npy", cls_embeddings) # numpy 文件
np.savetxt("cls_embeddings.csv", cls_embeddings, delimiter=",") # CSV 文件
print("完成!CLS 向量维度:", cls_embeddings.shape)
四、模型微调
1. 推理与微调的模型载入差异
推理载入:AutoModel / BertModel
-
推理 = 给模型输入文本,得到向量表示或者预测结果,但不训练(不更新参数)
- 不关心任务头(分类、NER 等)
- 不更新权重
- 可以直接用
[CLS]
或 token 向量 - AutoModel 提供了更灵活、通用的入口
-
常见需求:
- 句子级向量([CLS] token)
- token 级向量(last_hidden_state)
- 语义检索、聚类、相似度计算
-
BertModel.from_pretrained("bert-base-chinese")
-
明确指定是 BERT 模型
-
返回:
last_hidden_state
→ 序列每个 token 的隐藏状态pooler_output
→[CLS]
token 经过 Linear + Tanh
-
-
AutoModel.from_pretrained("bert-base-chinese")
- 自动识别模型类型:BERT、RoBERTa、ELECTRA 等
- 接口统一,代码可复用在不同模型上
- 返回的也是
last_hidden_state
和pooler_output
(如果模型支持 pooler)
微调载入:AutoModelFor……/BertFor……
-
微调 = 在下游任务上训练 BERT 的参数(或者部分参数)
-
下游任务不同,输出层不同:
- 文本分类 → 分类头(Linear + softmax/cross-entropy)
- 序列标注(NER、POS) → token 分类头(每个 token 一个预测)
- 问答 → start/end 位置预测头
任务 | Hugging Face 类 | 说明 |
---|---|---|
文本分类 | BertForSequenceClassification / AutoModelForSequenceClassification |
在 BERT 上加分类头,直接计算 loss |
序列标注 | BertForTokenClassification / AutoModelForTokenClassification |
在每个 token 上加分类头,可接 CRF 或直接 softmax |
问答 | BertForQuestionAnswering / AutoModelForQuestionAnswering |
输出 start/end logits |
-
BertForXXX
→ 明确 BERT 模型 -
AutoModelForXXX
→ 自动识别模型类型(BERT、RoBERTa、DeBERTa…),接口统一 -
这些类内部都封装了:
- BERT 基础编码器
- 任务特定头(分类/序列标注/QA)
- Loss 计算逻辑
微调 vs 推理区别总结
from transformers import AutoModel
import torch.nn as nn
model_name = "bert-base-chinese"
hidden_size = 768
num_labels = 2
base_model = AutoModel.from_pretrained(model_name)
classifier = nn.Linear(hidden_size, num_labels)
# 前向传播
outputs = base_model(input_ids, attention_mask=attention_mask)
pooled_output = outputs.pooler_output # [CLS] 向量过 Linear+Tanh
logits = classifier(pooled_output) # 分类头
等价代码是:
from transformers import BertForSequenceClassification
model = BertForSequenceClassification.from_pretrained(model_name, num_labels=2)
# 前向传播(内部自动算 pooled_output 并过分类器)
outputs = model(input_ids, attention_mask=attention_mask)
logits = outputs.logits
特点 | 推理 | 微调训练 |
---|---|---|
模型类 | AutoModel / BertModel |
BertForSequenceClassification / AutoModelForSequenceClassification 等 |
输出 | 向量表示 (last_hidden_state 、pooler_output ) |
logits + loss(用于反向传播) |
任务头 | 无 | 必须有,匹配下游任务 |
参数更新 | 不更新 | 更新(全量或部分冻结) |
使用场景 | 特征提取、相似度计算 | 分类、序列标注、问答等训练 |
2. 代码实现
(1) 加载数据
from transformers import BertTokenizer, BertForSequenceClassification
from transformers import Trainer, TrainingArguments
from datasets import load_dataset, Dataset # 这里的datasets指的是Hugging Face Datasets库
import numpy as np
dataset = load_dataset("imdb") # 例子: IMDB 情感分类,2分类
labels = np.array(dataset["train"]["label"])
print(dataset)
print(np.unique(labels)) # 不同标签
print(len(np.unique(labels))) # 数量
#### 代码输出
# DatasetDict({
# train: Dataset({
# features: ['text', 'label'],
# num_rows: 25000
# })
# test: Dataset({
# features: ['text', 'label'],
# num_rows: 25000
# })
# unsupervised: Dataset({
# features: ['text', 'label'],
# num_rows: 50000
# })
# })
# [0 1]
# 2
(2) 加载分词器
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
# 定义分词函数,输入是一个 batch(字典,里面有 "text" 字段,值是一个列表)
def tokenize(batch):
# 使用 tokenizer 把文本转成模型需要的输入:input_ids, attention_mask
# padding="max_length":固定长度填充到 max_length
# truncation=True:超过 max_length 就截断
# max_length=128:最大序列长度 128
return tokenizer(batch["text"], padding="max_length", truncation=True, max_length=128)
# 对整个数据集应用分词函数,函数返回的字段会添加到dataset中
# dataset.map(function, batched=False, batch_size=1000, remove_columns=None, num_proc=None)
# * `function`:处理每条数据或每个 batch 的函数
# * 输入:一条数据(`batched=False`)或 batch(`batched=True`)
# * 输出:字典(字段名 → 值或列表)
# * `batched`:
# * `False`(默认):每次处理一条样本
# * `True`:每次处理一个 batch(字典的值是 list),效率更高
# * `batch_size`:当 `batched=True` 时,每个 batch 的大小,默认 1000
# * `remove_columns`:可以删除不需要的字段;eg:remove_columns=["text", "id"]
# * `num_proc`:多进程加速
dataset = dataset.map(tokenize, batched=True)
(3) 修改数据格式
# Hugging Face Trainer 默认要求标签字段名为 "labels"
# 所以需要把原始的 "label" 列重命名为 "labels"
dataset = dataset.rename_column("label", "labels")
# 设置数据格式为 PyTorch Tensor,并只保留需要的字段
# 这样 DataLoader 取数据时会直接得到 torch.Tensor
dataset.set_format(type="torch", columns=["input_ids", "attention_mask", "labels"])
(4) 加载预训练模型
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)#num_labels=下游分类任务的类别数;2表示正负情感
print(model) # 显示模型整体结构,包括每个层
#### 代码输出
# BertForSequenceClassification(
# (bert): BertModel(
# (embeddings): BertEmbeddings(
# (word_embeddings): Embedding(30522, 768, padding_idx=0)
# (position_embeddings): Embedding(512, 768)
# (token_type_embeddings): Embedding(2, 768)
# (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
# (dropout): Dropout(p=0.1, inplace=False)
# )
# (encoder): BertEncoder(
# (layer): ModuleList(
# (0-11): 12 x BertLayer(
# (attention): BertAttention(
# (self): BertSdpaSelfAttention(
# (query): Linear(in_features=768, out_features=768, bias=True)
# (key): Linear(in_features=768, out_features=768, bias=True)
# (value): Linear(in_features=768, out_features=768, bias=True)
# (dropout): Dropout(p=0.1, inplace=False)
# )
# (output): BertSelfOutput(
# (dense): Linear(in_features=768, out_features=768, bias=True)
# (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
# (dropout): Dropout(p=0.1, inplace=False)
# )
# )
# (intermediate): BertIntermediate(
# (dense): Linear(in_features=768, out_features=3072, bias=True)
# (intermediate_act_fn): GELUActivation()
# )
# (output): BertOutput(
# (dense): Linear(in_features=3072, out_features=768, bias=True)
# (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
# (dropout): Dropout(p=0.1, inplace=False)
# )
# )
# )
# )
# (pooler): BertPooler(
# (dense): Linear(in_features=768, out_features=768, bias=True)
# (activation): Tanh()
# )
# )
# (dropout): Dropout(p=0.1, inplace=False)
# (classifier): Linear(in_features=768, out_features=2, bias=True)
# )
(可选) 自定义评估函数
# 如果需要评估指标,可以自定义评估函数,传入trainer作为参数compute_metrics
# 如果你没有定义compute_metrics,那么:
# trainer.evaluate()默认只会返回loss(以及epoch等训练状态信息),比如:{'eval_loss': 0.45, 'epoch': 1.0}
from sklearn.metrics import accuracy_score
def compute_metrics(p):
preds = p.predictions.argmax(-1)
labels = p.label_ids
return {"accuracy": accuracy_score(labels, preds)}
(5) 配置训练参数
training_args = TrainingArguments(
output_dir="./results", # 模型保存目录,保存 checkpoint、日志等
learning_rate=2e-5, # 学习率,控制权重更新步长
per_device_train_batch_size=128, # 每个设备(GPU/CPU)上的训练 batch size
per_device_eval_batch_size=128, # 每个设备上的验证 batch size
num_train_epochs=2, # 训练总轮数,每轮遍历一次训练集
weight_decay=0.01, # 权重衰减系数,防止过拟合(L2 正则化)
report_to=[] # 禁用 W&B 或其他日志工具
# 定义 Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
compute_metrics=compute_metrics
)
(6)训练模型(参数微调)
# 在 Hugging Face 的 Trainer 中,会自动检测 GPU 并把模型和数据移动到可用的 GPU 上,所以通常不需要手动 .to(device)。
trainer.train()
(7)模型评估
# Transformers 旧版本(< 3.x)的Trainer还没有evaluation_strategy,trainer.train()不会自动评估,必须手动调用evaluate()。
# trainer.evaluate(eval_dataset=dataset["test"])
# Trainer已经定义eval_dataset的情况下,这里可以不传入eval_dataset参数
trainer.evaluate()# Hugging Face 的 Trainer 在调用 compute_metrics 返回结果时,会自动加上 eval_ 前缀。
Transformers(>= 3.x,推荐用 4.x+) 的训练参数增加了evaluation_strategy,可以在训练时自动评估。
training_args = TrainingArguments(
output_dir="./results",
evaluation_strategy="epoch", # 每个 epoch 自动评估
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=3,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"], # 提供验证集
compute_metrics=compute_metrics,
)
# 训练(会在每个 epoch 自动调用 evaluate)
trainer.train()
# 如果需要,可以手动再评估一次/评估其它数据集
# results = trainer.evaluate()
# print(results)
(8)模型预测
# 检查测试集的标签
labels = np.array(dataset["unsupervised"]["labels"])
print(np.unique(labels)) # 不同标签
print(len(np.unique(labels))) # 数量
#### 代码输出
# [-1]
# 1
# 删除 labels(预测阶段不需要)
pred_dataset = dataset["unsupervised"].remove_columns("labels")
# 确保模型需要的字段存在,并设置为 Torch Tensor
pred_dataset.set_format(type="torch", columns=["input_ids", "attention_mask", "token_type_ids"])
# 在默认训练设备上预测
# Trainer 会自动使用训练时的 device(CPU 或 GPU)
predictions = trainer.predict(pred_dataset)
# 获取模型输出 logits
logits = predictions.predictions
# 取预测类别(整数索引)
preds = np.argmax(logits, axis=-1)
# 打印前 20 个预测结果
print(preds[:20])
# ================================
# 可选:映射回标签名称
# 假设你训练时有 label2id = {"negative":0, "positive":1}
label2id = {"negative":0, "positive":1}
id2label = {v:k for k,v in label2id.items()}
pred_labels = [id2label[i] for i in preds]
print(pred_labels[:20])
3. 知识补充
字典
- 定义:字典是 Python 内置的数据类型,用于存储 键-值对(key-value)。
- 语法:
my_dict = {"apple": 3, "banana": 5, "orange": 2}
-
特点:
- 键唯一:同一个字典中不能有重复键。
- 键可以是不可变类型:比如字符串、整数、元组(只要元素不可变)。
- 值可以是任意类型:数字、字符串、列表、甚至字典。
访问字典
print(my_dict["apple"]) # 3
- 不存在的 key 会报 KeyError
- 可以用
my_dict.get("apple")
避免报错,返回None
或指定默认值。
嵌套字典(二重索引)
- 字典的值可以是另一个字典,相当于“二重索引”:
nested_dict = {
"fruit": {"apple": 3, "banana": 5},
"vegetable": {"carrot": 4, "spinach": 2}
}
print(nested_dict["fruit"]["apple"]) # 3
-
你可以理解为:
- 第一级索引:
"fruit"
- 第二级索引:
"apple"
- 第一级索引:
DatasetDict
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
unsupervised: Dataset({
features: ['text', 'label'],
num_rows: 50000
})
})
- 它是 Hugging Face
datasets
库中的一个类,用来管理多个Dataset
对象,本质是一个类字典对象。 - 用法上表现得像字典(可以用 key 访问子集):
dataset["train"] # 返回 Dataset 对象
dataset["test"]
dataset["unsupervised"]
-
顶层:
DatasetDict
,类似字典,key 为"train"
,"test"
,"unsupervised"
-
第二层:每个 key 对应一个
Dataset
对象-
Dataset
对象本质是 表格型数据,包含:features
→ 列名及类型(如text
,label
)num_rows
→ 样本数量
-
-
所以它不是纯 Python dict,而是 类似嵌套字典的类结构,但功能更强大(支持
map
、filter
、train_test_split
等操作)。
二重索引感受
- 你可以通过 两次索引访问数据:
# 获取训练集第一条样本文本
dataset["train"][0]["text"]
# 获取训练集第一条样本标签
dataset["train"][0]["label"]
- 类似“嵌套字典”,但底层实际上是 Dataset 对象提供的接口,不是普通 dict。
DatasetDict.rename_column
的行为
DatasetDict
是一个 管理多个 Dataset 的字典型对象- 当你执行:
dataset = dataset.rename_column("label", "labels")
- 实际上会 遍历
DatasetDict
里所有的子集(train/test/unsupervised),对每个Dataset
调用.rename_column()
- 因此你不需要指定具体的子集,整个
DatasetDict
内的所有 Dataset 都会统一修改列名。
对单个 Dataset 也可以操作
如果只想修改某个子集的列名:
dataset["train"] = dataset["train"].rename_column("label", "labels")
- 只会修改训练集
- 验证集和测试集不受影响
链式操作方便
dataset = dataset.rename_column("label", "labels").map(tokenize, batched=True)
- 可以直接链式调用,不用分开操作每个子集
label == -1
- Hugging Face 的
DatasetDict
通常包含多个子集(train
、test
、unsupervised
等)。 - 为了 统一处理,每个子集通常保留相同的列名:
['text', 'label'] # 训练、验证、测试集一致
-
好处:
- 方便 批量处理(tokenize、map、set_format)
- 代码可以 通用,不需要针对不同子集写不同逻辑
即便测试集或未标注数据集的 label 全是 -1,统一列名可以避免在训练或预测时报 KeyError。
-
-1
通常用来标记 未知/无标签:- 在分类任务中,
0 ~ num_labels-1
是有效标签 -1
表示“这个样本没有标签,不参与 loss 计算”
- 在分类任务中,
-
好处:
- 某些工具或 pipeline 要求 dataset 有
label
列,即使无标签,也要占位 - 统一接口:Trainer / DataLoader 在处理
label_ids
时不会报错,只要知道-1
不计算 loss
- 某些工具或 pipeline 要求 dataset 有
trainer.evaluate()
-
主要用途:对 验证集/测试集 进行 评估
-
特点:
- 会返回 loss
- 如果定义了
compute_metrics
,会返回你定义的评估指标(accuracy、F1 等) - 默认使用
eval_dataset
(创建 Trainer 时指定的验证集)
-
调用示例:
metrics = trainer.evaluate()
print(metrics)
# 输出类似:{'eval_loss':0.45, 'eval_accuracy':0.88, ...}
trainer.predict()
-
主要用途:对 任意数据集 做 预测
-
特点:
- 返回 logits(模型输出)
- 返回 labels(如果数据集有 labels)
- 返回 metrics(如果定义了
compute_metrics
且数据集有 labels) - 可以用于训练集、测试集或者未标注数据集(unsupervised)
-
调用示例:
predictions = trainer.predict(test_dataset)
logits = predictions.predictions
labels = predictions.label_ids # 如果有
metrics = predictions.metrics # 如果定义了 compute_metrics
-
test_dataset
可以是:- Hugging Face
Dataset
对象 - PyTorch
DataLoader
- 自定义 Dataset 类
- Hugging Face
-
必须字段:
input_ids
、attention_mask
- 对于 BERT 等需要 token_type_ids 的模型,还要有
token_type_ids
-
数据类型:
torch.Tensor
(一般用dataset.set_format(type="torch")
设置)
潜在问题
-
标签越界:
- 如果模型是二分类 (
num_labels=2
) ,但 dataset 中 labels 有2
或3
,或者标签全为-1,就会触发 CUDAdevice-side assert
。
- 如果模型是二分类 (
-
预测阶段只想要 logits:
- 这时不需要 labels,可以删除:
pred_dataset = test_dataset.remove_columns("labels") predictions = trainer.predict(pred_dataset)
方法 | 是否返回 logits | 是否返回 metrics | 默认数据集 |
---|---|---|---|
evaluate | 否(仅 loss) | 是 | eval_dataset |
predict | 是 | 可选(需要 labels+compute_metrics) | 需要传入 dataset |
更多推荐
所有评论(0)