BERT

BERT不能像Word2Vec那样直接“查表”得到词向量,而是需要将整个句子输入模型,通过模型的前向计算得到句子中每个词(或整个句子)的上下文相关向量表示。

  1. 计算资源:BERT模型较大,在CPU上运行可能较慢。如果处理大量数据,考虑使用GPU或更小的模型(如 distilbert-base-uncased)。
  2. 微调 (Fine-tuning) vs. 特征提取 (Feature Extraction)
    • 特征提取:如上所示,冻结BERT的权重,只将其作为一个固定的“特征提取器”。提取出的向量可以输入到另一个分类器(如SVM、随机森林)中进行训练。适合数据量较小或计算资源有限的场景。
    • 微调解冻BERT的权重,并在你的评论数据集上对其进行端到端的训练。这通常能获得最佳性能,但需要更多的数据和计算资源。
  3. 模型选择:根据你的评论语言选择模型。
    • 中文评论:bert-base-chinese
    • 英文评论:bert-base-uncased (不区分大小写) 或 bert-base-cased (区分大小写)
    • 其他语言:Hugging Face Hub上提供了多种语言的预训练模型。
  4. 序列长度: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_statepooler_output(如果模型支持 pooler)
微调载入:AutoModelFor……/BertFor……
  • 微调 = 在下游任务上训练 BERT 的参数(或者部分参数)

  • 下游任务不同,输出层不同:

    1. 文本分类 → 分类头(Linear + softmax/cross-entropy)
    2. 序列标注(NER、POS) → token 分类头(每个 token 一个预测)
    3. 问答 → 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_statepooler_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}
  • 特点

    1. 键唯一:同一个字典中不能有重复键。
    2. 键可以是不可变类型:比如字符串、整数、元组(只要元素不可变)。
    3. 值可以是任意类型:数字、字符串、列表、甚至字典。

访问字典

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,而是 类似嵌套字典的类结构,但功能更强大(支持 mapfiltertrain_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 通常包含多个子集(traintestunsupervised 等)。
  • 为了 统一处理,每个子集通常保留相同的列名:
['text', 'label']  # 训练、验证、测试集一致
  • 好处:

    1. 方便 批量处理(tokenize、map、set_format)
    2. 代码可以 通用,不需要针对不同子集写不同逻辑

即便测试集或未标注数据集的 label 全是 -1,统一列名可以避免在训练或预测时报 KeyError。

  • -1 通常用来标记 未知/无标签

    • 在分类任务中,0 ~ num_labels-1 是有效标签
    • -1 表示“这个样本没有标签,不参与 loss 计算”
  • 好处:

    1. 某些工具或 pipeline 要求 dataset 有 label 列,即使无标签,也要占位
    2. 统一接口:Trainer / DataLoader 在处理 label_ids 时不会报错,只要知道 -1 不计算 loss

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 可以是:

    1. Hugging Face Dataset 对象
    2. PyTorch DataLoader
    3. 自定义 Dataset 类
  • 必须字段

    • input_idsattention_mask
    • 对于 BERT 等需要 token_type_ids 的模型,还要有 token_type_ids
  • 数据类型

    • torch.Tensor(一般用 dataset.set_format(type="torch") 设置)

潜在问题

  1. 标签越界

    • 如果模型是二分类 (num_labels=2) ,但 dataset 中 labels 有 23,或者标签全为-1,就会触发 CUDA device-side assert
  2. 预测阶段只想要 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
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐