Datawhale 大模型算法全栈基础篇 202602第5次笔记
作业:
一、BERT
BERT 是 Transformer 编码器的“升级版”,通过更大的规模、更聪明的预训练任务,成为理解语言的超级明星!
| 方面 | Transformer 编码器 | BERT |
|---|---|---|
| 层数 | 6层(原始论文) | 12层或24层,更深 |
| 位置编码 | 固定正弦/余弦 | 可学习的位置嵌入 |
| 输入表示 | 词嵌入 + 位置编码 | 词嵌入 + 位置嵌入 + 片段嵌入 |
| 特殊词元 | 无 | 有 [CLS], [SEP], [MASK], [PAD], [UNK] |
| 预训练 | 没有独立预训练 | 有 MLM 和 NSP 两个预训练任务 |
| 训练范式 | 端到端训练 | 预训练 + 微调 |
| 输出用途 | 所有位置的隐藏状态 | 所有位置 + pooler_output ([CLS] 专用) |
| 应用 | 通用编码器 | 自然语言理解任务(分类、NER、问答等) |
BERT(来自Transformer的双向编码器表示)本质上是一个基于Transformer编码器架构的预训练语言模型,但两者在结构、训练方式、输入处理和任务目标等方面存在显著差异。以下是BERT与原始Transformer编码器的主要区别:
1. 模型规模
-
Transformer编码器:原始论文中采用6层(N=6),隐藏层维度512,注意力头数8。
-
BERT:规模更大,例如BERT-base有12层,隐藏层768,12个注意力头;BERT-large有24层,隐藏层1024,16个注意力头。
2. 位置编码
-
Transformer编码器:使用固定的正弦/余弦位置编码。
-
BERT:采用可学习的位置嵌入,并在预训练时固定最大序列长度(通常为512),这使得模型能适应不同长度的输入但受限于最大位置嵌入表的大小。
3. 输入表示
-
Transformer编码器:输入通常由词嵌入和位置编码相加组成。
-
BERT:输入由三种嵌入相加构成:词元嵌入(Token Embedding)、位置嵌入(Position Embedding)和片段嵌入(Segment Embedding)。片段嵌入用于区分句子对中的不同句子(如句子A和B),这是BERT处理句子对任务的基础。
4. 特殊词元
-
Transformer编码器:无特定预定义的特殊词元(除非任务需要)。
-
BERT:引入了多个特殊词元:[CLS](用于分类的聚合表示)、[SEP](分隔句子)、[MASK](用于掩码语言模型)、[PAD](填充)、[UNK](未知词)。这些词元在预训练和微调中扮演关键角色。
5. 预训练任务
-
Transformer编码器:最初是作为机器翻译模型的一部分,与解码器联合训练,本身没有独立的预训练任务。
-
BERT:设计了两个无监督预训练任务:
-
掩码语言模型(MLM):随机遮盖部分输入词元,让模型根据上下文预测被遮盖的词,实现真正的双向上下文理解。
-
下一句预测(NSP):判断两个句子是否为连续的原文,帮助模型学习句子间的关系。
-
6. 训练范式
-
Transformer编码器:通常用于特定任务的端到端训练(如翻译任务中的编码器部分)。
-
BERT:采用“预训练 + 微调”范式,先在大规模通用语料上通过无监督任务预训练,再在下游任务上微调整个模型,显著提升了迁移学习的效果。
7. 双向性
-
两者均为双向:Transformer编码器的自注意力机制天然支持双向上下文访问,即每个词元都能关注序列中的所有其他词元。
-
BERT强调“深度双向”:通过MLM任务,BERT在训练过程中真正强迫模型同时利用左右两侧的上下文信息,而不仅仅是像双向LSTM那样浅层拼接两个单向表示。
8. 输出用途
-
Transformer编码器:输出所有位置的隐藏状态,可用于各种下游任务。
-
BERT:除最后一层的所有位置输出外,还提供 pooler_output(即[CLS]位置的向量经过线性层和Tanh激活),专门用于句子级分类任务。
9. 应用范围
-
Transformer编码器:可作为通用序列编码器,广泛应用于机器翻译、文本分类、序列标注等。
-
BERT:专攻自然语言理解任务,如情感分析、命名实体识别、问答系统等,但不适合文本生成(因其双向性)。
10. 生态与工具
-
Transformer编码器:可作为自定义组件实现,但缺乏统一的预训练版本。
-
BERT:拥有丰富的预训练模型库(如Hugging Face的transformers库),支持多种语言和规模,极大地简化了实际应用。
总结而言,BERT是对Transformer编码器架构的成功扩展和优化,通过增大模型规模、引入创新的预训练任务和输入设计,使模型能够学习到更深层次的语言表示,从而在众多自然语言理解任务中取得突破性效果。而原始的Transformer编码器更多地作为一个基础组件,其潜力通过BERT这样的预训练模型得以充分释放。
二、HuggingFace
lstm_on_rotten.py
"""
LSTM 文本分类器,在 rotten_tomatoes 数据集上运行(取前100条训练样本)
用于与 BERT 微调结果进行对比
"""
import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from datasets import load_dataset
# ==================== 1. 设置随机种子 ====================
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
set_seed(42)
# ==================== 2. 加载数据集 ====================
print("加载 rotten_tomatoes 数据集...")
dataset = load_dataset("rotten_tomatoes")
train_texts = dataset["train"]["text"]
train_labels = dataset["train"]["label"]
test_texts = dataset["test"]["text"]
test_labels = dataset["test"]["label"]
print(f"原始训练集大小: {len(train_texts)}")
print(f"测试集大小: {len(test_texts)}")
print(f"类别数: {len(set(train_labels))}")
# 为了与 BERT 实验一致,只取前 100 条训练样本
train_texts = train_texts[:100]
train_labels = train_labels[:100]
print(f"取前 100 条作为训练集,测试集保持 {len(test_texts)} 条")
# 从训练集中划分验证集(20%)
train_texts, val_texts, train_labels, val_labels = train_test_split(
train_texts, train_labels, test_size=0.2, random_state=42
)
print(f"最终训练集大小: {len(train_texts)}")
print(f"验证集大小: {len(val_texts)}")
# ==================== 3. 构建词表 ====================
max_features = 10000 # 词汇表最大数量
vectorizer = CountVectorizer(max_features=max_features, stop_words='english')
vectorizer.fit(train_texts) # 只在训练集上拟合
word2idx = {word: i+1 for i, word in enumerate(vectorizer.get_feature_names_out())}
vocab_size = len(word2idx) + 1 # +1 留作 padding (0)
print(f"词表大小(含填充符0): {vocab_size}")
# 获取分词器(与 vectorizer 一致)
tokenizer = vectorizer.build_tokenizer()
def text_to_sequence(text, max_len=200):
"""将文本转为整数 ID 序列,并填充/截断到固定长度"""
tokens = tokenizer(text.lower())
ids = [word2idx.get(token, 0) for token in tokens]
if len(ids) > max_len:
ids = ids[:max_len]
else:
ids = ids + [0] * (max_len - len(ids))
return ids
max_len = 200 # 与 BERT 的 max_length=128 略有不同,但可调整
# 转换所有文本
X_train = np.array([text_to_sequence(text, max_len) for text in train_texts])
X_val = np.array([text_to_sequence(text, max_len) for text in val_texts])
X_test = np.array([text_to_sequence(text, max_len) for text in test_texts])
y_train = np.array(train_labels)
y_val = np.array(val_labels)
y_test = np.array(test_labels)
print(f"训练数据形状: {X_train.shape}")
print(f"验证数据形状: {X_val.shape}")
print(f"测试数据形状: {X_test.shape}")
# ==================== 4. 转换为 PyTorch 张量 ====================
X_train_t = torch.tensor(X_train, dtype=torch.long)
y_train_t = torch.tensor(y_train, dtype=torch.long)
X_val_t = torch.tensor(X_val, dtype=torch.long)
y_val_t = torch.tensor(y_val, dtype=torch.long)
X_test_t = torch.tensor(X_test, dtype=torch.long)
y_test_t = torch.tensor(y_test, dtype=torch.long)
train_dataset = torch.utils.data.TensorDataset(X_train_t, y_train_t)
val_dataset = torch.utils.data.TensorDataset(X_val_t, y_val_t)
test_dataset = torch.utils.data.TensorDataset(X_test_t, y_test_t)
batch_size = 16
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# ==================== 5. 定义 LSTM 模型 ====================
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes, num_layers=2, dropout=0.5):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, bidirectional=True, dropout=dropout if num_layers>1 else 0)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_dim * 2, num_classes) # 双向,所以 hidden_dim*2
def forward(self, x):
# x: (batch, seq_len)
embedded = self.embedding(x) # (batch, seq_len, embed_dim)
lstm_out, (hidden, cell) = self.lstm(embedded)
# 取最后一个时间步的输出(双向时自动拼接)
last_hidden = lstm_out[:, -1, :] # (batch, hidden_dim*2)
last_hidden = self.dropout(last_hidden)
logits = self.fc(last_hidden) # (batch, num_classes)
return logits
# 超参数(与 BERT 的隐藏维度 768 不同,但保持合理)
embed_dim = 128
hidden_dim = 64
num_layers = 2
num_classes = 2
model = LSTMClassifier(vocab_size, embed_dim, hidden_dim, num_classes, num_layers)
print(model)
# ==================== 6. 训练准备 ====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# ==================== 7. 训练循环(含验证和早停)====================
def evaluate(model, loader):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
return 100 * correct / total
best_val_acc = 0.0
patience = 5
patience_counter = 0
num_epochs = 20
print("开始训练 LSTM...")
for epoch in range(1, num_epochs+1):
model.train()
total_loss = 0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_train_loss = total_loss / len(train_loader)
val_acc = evaluate(model, val_loader)
print(f"Epoch {epoch:2d}, Train Loss: {avg_train_loss:.4f}, Val Acc: {val_acc:.2f}%")
if val_acc > best_val_acc:
best_val_acc = val_acc
patience_counter = 0
torch.save(model.state_dict(), "best_lstm_model.pth")
print(f" -> 保存最佳模型 (val acc {best_val_acc:.2f}%)")
else:
patience_counter += 1
if patience_counter >= patience:
print(f"验证准确率连续 {patience} 轮未提升,停止训练。")
break
# ==================== 8. 测试 ====================
model.load_state_dict(torch.load("best_lstm_model.pth"))
test_acc = evaluate(model, test_loader)
print(f"\n测试集准确率: {test_acc:.2f}%")
05_load_dataset.py
import os
import numpy as np
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
TrainingArguments,
Trainer,
DataCollatorWithPadding
)
import evaluate
# 可选:设置 Hugging Face 镜像(国内加速)
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
# 1. 加载数据集
print("加载数据集...")
dataset = load_dataset("rotten_tomatoes")
# 2. 加载分词器
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
# 3. 定义分词函数并预处理数据集
def tokenize_function(examples):
return tokenizer(examples["text"], truncation=True, max_length=128)
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# 4. 取小部分数据用于快速演示(可根据需要调整或使用全量数据)
small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(100))
small_eval_dataset = tokenized_datasets["validation"].shuffle(seed=42).select(range(100))
# 5. 加载模型(二分类)
model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased", num_labels=2)
# 6. 设置训练参数
training_args = TrainingArguments(
output_dir="./trainer_demo",
eval_strategy="epoch", # 每轮结束后评估
num_train_epochs=3, # 训练轮数
per_device_train_batch_size=16, # 训练批次大小
per_device_eval_batch_size=16, # 评估批次大小
weight_decay=0.01, # 权重衰减
save_strategy="epoch", # 每轮保存一次
load_best_model_at_end=True, # 训练结束后加载最佳模型
metric_for_best_model="accuracy", # 以准确率为准选择最佳模型
report_to="none" # 不向外部报告
)
# 7. 定义评估指标(准确率)
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
# 8. 数据收集器(动态填充)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
# 9. 初始化 Trainer(注意:已移除 tokenizer 参数)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=small_train_dataset,
eval_dataset=small_eval_dataset,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
# 10. 开始训练
print("开始训练...")
trainer.train()
# 11. 最终评估
results = trainer.evaluate()
print("评估结果:", results)
# 训练完成后,评估测试集
test_results = trainer.evaluate(tokenized_datasets["test"])
print("测试集结果:", test_results)
一、实验数据概览
| 模型 | 训练样本数 | 验证准确率 | 测试准确率 | 备注 |
|---|---|---|---|---|
| BERT (distilbert-base-uncased) | 100 | 61% (第3轮) | 59.01% | 使用预训练模型微调,测试集准确率 59.0% |
| LSTM (自定义) | 80 | 100% (过拟合) | 50.00% | 从零训练,严重过拟合,测试集准确率 50% |
关键观察:
-
LSTM 在仅 80 条训练数据上就达到了 100% 的验证准确率,但测试准确率仅 50%(随机猜测水平),说明模型完全过拟合,没有学到任何可泛化的特征。
-
BERT 在 100 条数据上验证准确率为 63%,虽然不高,但明显优于随机,表明其利用预训练知识学到了部分模式。
注意:
-
BERT 的验证准确率取第3轮的 61%(日志显示
eval_accuracy: 0.61)。 -
BERT 的测试准确率为 59.01%(日志显示
eval_accuracy: 0.590056...)。 -
LSTM 的训练样本数略少(80 vs 100),但差异不大,对比结论依然成立。
二、对比分析
1. 模型构建对比
| 方面 | Word2Vec + LSTM | BERT 微调 |
|---|---|---|
| 模型定义 | 手动定义 Embedding、LSTM、全连接层,代码约 20 行。 |
使用 AutoModelForSequenceClassification.from_pretrained 一行加载预训练模型并自动添加分类头。 |
| 词向量 | 从零学习词嵌入(nn.Embedding 随机初始化),词表仅基于训练集构建(大小 660)。 |
使用 BERT 预训练好的子词嵌入(WordPiece),词表 30522,包含丰富的语言知识。 |
| 序列处理 | 手动用 CountVectorizer 分词,填充至固定长度 200。 |
分词器自动处理,DataCollatorWithPadding 动态填充,支持 attention mask。 |
| 参数量 | 约 100 万(LSTM 层数少,隐藏层 64)。 | 约 6700 万(DistilBERT),远大于 LSTM。 |
| 结构特点 | 单向/双向 LSTM,捕捉局部顺序依赖。 | 多层 Transformer,自注意力机制捕捉全局依赖。 |
2. 训练流程对比
| 方面 | LSTM | BERT |
|---|---|---|
| 训练轮数 | 6 轮后早停(验证准确率已达 100%)。 | 3 轮(验证准确率 63%)。 |
| 学习率 | 0.001(常用值)。 | 2e-5(极小学习率)。 |
| 优化器 | Adam。 | AdamW(带权重衰减)。 |
| 正则化 | Dropout 0.5,无其他。 | 模型自带 Dropout,训练参数 weight_decay=0.01。 |
| 过拟合程度 | 严重过拟合:验证 100%,测试 50%。 | 轻微过拟合:验证 63%,测试未知但应高于随机。 |
| 代码复杂度 | 需手动实现训练循环、早停、保存模型。 | 使用 Trainer API,代码简洁。 |
3. 最终性能对比
| 指标 | LSTM | BERT |
|---|---|---|
| 验证准确率 | 100%(完全记住验证集) | 63%(有一定泛化能力) |
| 测试准确率 | 50.00%(随机水平) | 59.01%(明显优于随机,体现预训练优势) |
结论:
-
在极少量样本下,从零训练的 LSTM 完全过拟合,测试准确率仅为 50%(相当于随机猜测)。
-
BERT 凭借预训练知识,在相同条件下测试准确率达到 59.01%,显著高于 LSTM,证明了迁移学习的有效性。
-
若使用全量数据,BERT 的优势会更明显(通常可达 80% 以上),而 LSTM 可能提升到 70% 左右。
三、总结
-
LSTM 需要大量数据才能学习到有效的词向量和语义表示,而 BERT 通过预训练弥补了数据不足。
-
Transformer 的自注意力机制比 RNN 更适合捕捉长距离依赖,但在小样本下这个优势不显著,反而容易过拟合。
-
实验结果验证了预训练模型在小样本场景下的实用价值。
笔记:
第五节 BERT
一、为什么需要 BERT?
以前学过的 Word2Vec 给每个词一个固定的向量,但同一个词在不同句子里的意思可能不一样。比如:
-
“我出了一件破防装备”(游戏里破防指降低防御)
-
“NLP 算法给我学破防了”(网络用语,指心态崩溃)
在 Word2Vec 里,这两个“破防”的向量是一样的,这就不对啦!
BERT 就是来解决这个问题的。它能根据上下文,给同一个词生成不同的向量,真正理解词在句子里的意思。
二、BERT 是怎么学的?
BERT 的学习分两步:
-
预训练:就像让一个学生先读很多很多书(比如维基百科、各种文章),让他自己总结语言规律。这个阶段不需要老师教,模型自己通过做“填空题”和“句子配对题”来学习。
-
微调:当我们要做具体任务时(比如判断电影评论是好评还是差评),就把预训练好的 BERT 拿出来,在它的头顶加一个简单的分类器,再用我们自己的数据稍微训练一下。因为 BERT 已经懂语言了,所以只需要很少的数据就能学得很好。
这叫迁移学习,就像学会了骑自行车,再去学骑电动车就很容易一样。
三、BERT 长什么样?
BERT 用的是 Transformer 的编码器部分,但比原来的更大、更深。常见的有两种:
-
BERT-Base:12 层,每层 768 个神经元,12 个注意力头(参数 1.1 亿)
-
BERT-Large:24 层,每层 1024 个神经元,16 个注意力头(参数 3.4 亿)
它的输入不是单纯一个词向量,而是由三个向量相加组成的:
-
词元嵌入:每个词自己的向量。
-
位置嵌入:告诉模型这个词在句子的第几个位置(BERT 用的是可学习的,最大支持 512 个位置)。
-
片段嵌入:如果输入是两个句子(比如“我喜欢猫”和“它很可爱”),第一个句子的所有词加一种标记,第二个句子的词加另一种标记,用来区分它们。
四、BERT 的特殊词元
BERT 在输入里加了一些特殊符号,它们各有各的任务:
-
[CLS]:放在句子最前面,它的最终输出代表整个句子的意思,用来做分类任务。
-
[SEP]:用来分隔两个句子,放在句子结尾或两个句子之间。
-
[MASK]:预训练时用,把一些词盖住,让模型猜。
-
[PAD]:把不同长度的句子补成一样长,计算时会忽略它。
-
[UNK]:遇到不认识的字就用它代替。
五、BERT 的两个特殊作业(预训练任务)
为了让 BERT 真正理解语言,研究人员给它布置了两个作业:
1. 掩码语言模型(MLM)——完形填空
在输入句子里,随机把 15% 的词遮住(换成 [MASK]),然后让模型猜被遮住的词是什么。但是,如果训练时全是 [MASK],到真正使用时没有 [MASK],模型会不适应。所以又加了点花样:
-
80% 的概率真的换成 [MASK]
-
10% 的概率换成另一个随机词
-
10% 的概率保持不变
这样模型既要学会利用上下文填空,也要学会识别错误词,还要学会理解真实词。
比如句子:“我爱吃苹果”
-
如果“爱”被选中:
-
80% 变成“我 [MASK] 吃苹果”
-
10% 变成“我恨吃苹果”(随机换词)
-
10% 还是“我爱吃苹果”
-
2. 下一句预测(NSP)——判断句子是否连续
给模型两个句子 A 和 B,让它判断 B 是不是 A 的下一句。
-
50% 的情况是真正的下一句(IsNext)
-
50% 的情况是从别处随机拿的句子(NotNext)
这个任务通过 [CLS] 的输出做二分类。虽然后来有人觉得这个任务不一定有用,但原始 BERT 靠它确实学会了句子间的关系。
六、BERT 能做什么?
预训练好的 BERT,可以微调到各种任务上:
1. 文本分类(比如情感分析)
-
把句子加上 [CLS] 和 [SEP] 输入 BERT。
-
取出 [CLS] 位置的向量,接一个全连接层,输出类别。
-
用任务数据微调整个模型。
2. 词元分类(比如命名实体识别)
-
把句子输入 BERT。
-
取出每个词元的向量,在每个位置上接一个全连接层,给每个词打标签。
-
同样微调整个模型。
3. 其他任务
比如问答:把问题和段落作为两个句子输入,让模型预测答案在段落中的起始和结束位置。
七、用 BERT 要注意什么?
-
最大长度:BERT 的位置嵌入最大支持 512 个词元(含特殊词元),所以句子太长就要截断。
-
别忘了加特殊词元:一定要有 [CLS] 开头,[SEP] 结尾或中间。现在用
tokenizer会自动加,但手动处理时要注意。 -
可以用不同层的输出:有时候把最后几层的向量拼起来,比只用最后一层效果更好。
八、BERT 和 Transformer 编码器有啥区别?
(这就是练习的答案啦!)
| 方面 | Transformer 编码器 | BERT |
|---|---|---|
| 层数 | 6层(原始论文) | 12层或24层,更深 |
| 位置编码 | 固定正弦/余弦 | 可学习的位置嵌入 |
| 输入表示 | 词嵌入 + 位置编码 | 词嵌入 + 位置嵌入 + 片段嵌入 |
| 特殊词元 | 无 | 有 [CLS], [SEP], [MASK], [PAD], [UNK] |
| 预训练 | 没有独立预训练 | 有 MLM 和 NSP 两个预训练任务 |
| 训练范式 | 端到端训练 | 预训练 + 微调 |
| 输出用途 | 所有位置的隐藏状态 | 所有位置 + pooler_output ([CLS] 专用) |
| 应用 | 通用编码器 | 自然语言理解任务(分类、NER、问答等) |
总结:BERT 是 Transformer 编码器的“升级版”,通过更大的规模、更聪明的预训练任务,成为理解语言的超级明星!
"""
bert_demo.py
完整 BERT 中文特征提取示例
使用 bert-base-chinese 模型,演示如何加载模型、预处理文本、提取句子和词元特征。
"""
import os
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' # 必须在导入transformers之前
import torch
from transformers import AutoTokenizer, AutoModel
# 可选:设置 Hugging Face 镜像(国内加速)
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
# ==================== 1. 环境和模型配置 ====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_name = "bert-base-chinese" # 中文 BERT 模型
texts = ["我来自中国", "我喜欢自然语言处理"] # 待处理文本
print(f"使用设备: {device}")
print(f"加载模型: {model_name}")
# ==================== 2. 加载分词器和模型 ====================
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name).to(device)
model.eval() # 切换到评估模式(禁用 Dropout 等)
print("\n模型加载完成!")
# ==================== 3. 文本预处理 ====================
# tokenizer 会自动添加 [CLS] 和 [SEP],并进行填充/截断
inputs = tokenizer(
texts,
padding=True, # 填充到 batch 内最大长度
truncation=True, # 超过 max_length 则截断
return_tensors="pt" # 返回 PyTorch 张量
).to(device)
print("\nTokenizer 输出:")
for key, value in inputs.items():
print(f"{key}: \n{value}\n")
# ==================== 4. 模型推理 ====================
with torch.no_grad(): # 不计算梯度,节省内存
outputs = model(**inputs)
# ==================== 5. 提取特征 ====================
# outputs.last_hidden_state 形状: (batch_size, seq_len, hidden_size) = (2, 最长序列长度, 768)
last_hidden_state = outputs.last_hidden_state
# (1) 提取句子级别的特征:取 [CLS] 位置的向量
sentence_features = last_hidden_state[:, 0, :] # shape: (2, 768)
# (2) 提取第一个句子的词元级别特征(去掉 [CLS] 和 [SEP])
# 第一个句子 "我来自中国" 对应的 token 序列为: [CLS] 我 来 自 中 国 [SEP]
# 索引 1 到 5 就是 "我 来 自 中 国" 这五个字
first_sentence_tokens = last_hidden_state[0, 1:6, :] # shape: (5, 768)
# 如果存在 pooler_output(BERT 专门用于 [CLS] 的池化输出),也可以提取
pooler_output = getattr(outputs, "pooler_output", None)
print("\n特征提取结果:")
print(f"句子特征 shape: {sentence_features.shape}") # [2, 768]
if pooler_output is not None:
print(f"pooler_output shape: {pooler_output.shape}") # [2, 768]
print(f"第一个句子的词元特征 shape: {first_sentence_tokens.shape}") # [5, 768]
# ==================== 6. 可选:查看分词结果 ====================
# 将 input_ids 解码为可读的 token 序列(便于理解)
for i, text in enumerate(texts):
tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][i])
print(f"\n句子 {i+1} 的 tokens: {tokens}")
输出:
模型加载完成!
Tokenizer 输出:
input_ids:
tensor([[ 101, 2769, 3341, 5632, 704, 1744, 102, 0, 0, 0, 0],
[ 101, 2769, 1599, 3614, 5632, 4197, 6427, 6241, 1905, 4415, 102]],
device='cuda:0')
token_type_ids:
tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], device='cuda:0')
attention_mask:
tensor([[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0')
特征提取结果:
句子特征 shape: torch.Size([2, 768])
pooler_output shape: torch.Size([2, 768])
第一个句子的词元特征 shape: torch.Size([5, 768])
句子 1 的 tokens: ['[CLS]', '我', '来', '自', '中', '国', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]']
句子 2 的 tokens: ['[CLS]', '我', '喜', '欢', '自', '然', '语', '言', '处', '理', '[SEP]']
-
input_ids:每个 token 对应的 ID。
101是[CLS],102是[SEP],0是[PAD]。第一句 “我来自中国” 有 5 个汉字 + [CLS] + [SEP] = 7 个 token,后面补了 4 个[PAD]与第二句对齐。第二句有 9 个汉字 + 两个特殊符 = 11 个 token。 -
token_type_ids:全 0,表示只有一个句子(如果是句子对任务,这里会区分句子 A 和 B)。
-
attention_mask:1 表示真实 token,0 表示填充 token。模型在计算注意力时会忽略填充位。
特征提取结果
句子特征 shape: torch.Size([2, 768]) pooler_output shape: torch.Size([2, 768]) 第一个句子的词元特征 shape: torch.Size([5, 768])
-
句子特征:取
[CLS]位置的向量,每个句子得到一个 768 维向量,可用于分类等任务。 -
pooler_output:也是
[CLS]向量经过一个全连接层 + Tanh 的结果,常用于句子对任务(NSP)。 -
词元特征:第一个句子中“我”、“来”、“自”、“中”、“国”这五个字对应的向量,每个字也是 768 维,可用于序列标注任务(如命名实体识别)。
解码后的 token 序列
句子 1 的 tokens: ['[CLS]', '我', '来', '自', '中', '国', '[SEP]', '[PAD]', '[PAD]', '[PAD]', '[PAD]'] 句子 2 的 tokens: ['[CLS]', '我', '喜', '欢', '自', '然', '语', '言', '处', '理', '[SEP]']
可以看到中文 BERT 的分词器(基于字)把每个汉字单独作为一个 token,并自动添加了 [CLS] 和 [SEP],短句被填充了 [PAD]。这完全符合预期。
总结
-
我们用几行代码就加载了一个强大的中文 BERT 模型。
-
分词器把句子变成数字 ID,并自动添加特殊标记和填充。
-
模型推理后,我们可以拿到每个字的向量(
last_hidden_state)和句子向量([CLS])。 -
这些向量就是 BERT 对文本的理解,可以用于各种下游任务。
-
通过打印模型结构,我们看到了 BERT 内部的嵌入层、12 层编码器和池化层,和理论完全对应。
第二节 GPT
一、GPT 是干什么的?
前面我们学了 BERT,它特别擅长“理解”语言,比如判断一句话是好评还是差评。但如果我们想让电脑自己写故事、跟你聊天、帮你写邮件,那就要用 GPT 了。
GPT 的全称是“生成式预训练 Transformer”,它的核心任务就是 续写——给你一段开头,它能把后面的内容接下去,而且接得非常像人写的。
举个例子:
-
你输入:“今天天气真好,我们一起去”
-
GPT 可能会输出:“公园玩吧,顺便带点零食。”
它就像一个超级厉害的“接话员”!
二、GPT 是怎么学的?
GPT 的学习方法很简单,就是一直做 “猜下一个词” 的练习。比如给一句 “我喜欢吃”,模型要猜下一个字可能是 “苹果” 还是 “香蕉”。通过在海量的书、文章、网页上反复做这种练习,模型就慢慢学会了语法、常识、甚至一些逻辑推理。
这种任务叫做 因果语言模型(CLM)。为什么叫“因果”呢?因为在猜下一个词的时候,模型只能看到前面的词,不能偷看后面的词。就像玩游戏时,只能根据已经出现的线索去猜下一步,不能提前看答案。
三、GPT 的进化史
GPT 不是一下子就变得这么厉害的,它经历了三代成长:
1. GPT-1:先预训练,再微调
-
先用大量无标签文本做“猜词”练习(预训练)。
-
然后针对具体任务(比如写诗、翻译),用少量带标签的数据稍微训练一下(微调)。
-
这样就能适应各种任务,但每个任务还得准备自己的数据。
2. GPT-2:零样本学习
-
把模型做大(15亿参数),用更多高质量数据训练。
-
神奇的事情发生了:即使不给任何例子,模型也能直接按指令做事!
-
比如输入 “翻译成法语:cheese”,它就会输出 “fromage”。它好像自己在预训练中学会了翻译。
3. GPT-3:上下文学习
-
模型大到吓人(1750亿参数),这时它学会了 看例子学习。
-
可以在输入里先给几个例子(比如 苹果 -> apple,猫 -> cat),然后它就会照着例子把新词翻译出来。
-
这就是 提示(Prompt) 的魔力——不需要改模型,只需要设计好提示词,模型就能按要求干活。
现在的大模型(比如 ChatGPT)都继承了这种能力,给它几个例子或者直接下指令,它就能理解并执行。
四、GPT 长什么样?
GPT 用的是 Transformer 的解码器,但是做了一些调整。一个标准的解码器层(在 GPT 中)有两大部件:
-
掩码多头自注意力
-
它跟普通的自注意力很像,但多了个“掩码”。这个掩码会遮住未来的词,保证模型在预测下一个词时,只能看到前面已经出现的词,不能偷看后面的。
-
这样模型才能学会真正的“续写”。
-
-
前馈网络(FFN)
-
跟 BERT 里的完全一样,负责把注意力得到的结果做进一步加工,提取更深层的特征。
-
注意:GPT 没有编码器,所以也没有交叉注意力层。它就是一个纯粹的解码器堆叠,一层叠一层,越深越强大。
GPT 各代的架构变化:
-
GPT-1:12 层,768 维,12 个注意力头,最大长度 512。
-
GPT-2:把层数增加到 48 层(不同版本不同),最大长度 1024,还把 LayerNorm 移到了每个子层的前面(Pre-Norm),这样训练更稳定。
-
GPT-3:最大 96 层,12288 维,最大长度 2048,参数量达到 1750 亿。
五、GPT 的输入和特殊词元
GPT 的输入比 BERT 简单,只有两种嵌入相加:
-
词元嵌入(Word Embedding):每个词自己的向量。
-
位置嵌入(Position Embedding):可学习的,告诉模型词在第几个位置。
没有 BERT 的片段嵌入,因为 GPT 处理的是连续文本,不需要区分句子对。
特殊词元很少,最主要的是 <|endoftext|>,它表示一段文本的结束,也可以用来分隔不同示例。
六、GPT 与 BERT 大对比
| 方面 | BERT(编码器) | GPT(解码器) |
|---|---|---|
| 核心结构 | Transformer 编码器 | Transformer 解码器 |
| 注意力 | 双向(能看到全文) | 单向(掩码,只能看左边) |
| 预训练任务 | 完形填空(MLM)+ 句子配对(NSP) | 猜下一个词(CLM) |
| 输入 | 词元 + 位置 + 片段 | 词元 + 位置 |
| 最大长度 | 通常 512 | GPT-2 1024,GPT-3 2048 |
| 归一化 | Post-Norm | Pre-Norm(GPT-2/3) |
| 使用方法 | 微调(加分类头,更新参数) | 提示(设计提示词,不更新参数) |
| 擅长领域 | 理解(分类、命名实体识别、问答) | 生成(写作、对话、创作) |
gpt_demo.py
import os
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
# 1. 环境配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_name = "gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
model.eval()
# 2. 英文示例
prompt_en = "I like eating fried"
input_ids_en = tokenizer(prompt_en, return_tensors="pt")['input_ids'].to(device)
print(f"英文输入: '{prompt_en}'")
print(f"被编码为 {input_ids_en.shape[1]} 个 token: {tokenizer.convert_ids_to_tokens(input_ids_en[0])}")
print("开始为英文逐个 token 生成...")
generated_ids_en = input_ids_en
with torch.no_grad():
for i in range(5):
outputs = model(generated_ids_en)
next_token_logits = outputs.logits[:, -1, :]
next_token_id = torch.argmax(next_token_logits, dim=-1).unsqueeze(-1)
new_token = tokenizer.decode(next_token_id[0])
print(f"第 {i+1} 步, 生成 token: '{new_token.strip()}'")
generated_ids_en = torch.cat([generated_ids_en, next_token_id], dim=1)
full_decoded_text_en = tokenizer.decode(generated_ids_en[0], skip_special_tokens=True)
print(f"\n英文最终生成结果: \n'{full_decoded_text_en}'\n")
# 3. 中文示例
prompt_zh = "我喜欢吃炸"
input_ids_zh = tokenizer(prompt_zh, return_tensors="pt")['input_ids'].to(device)
print(f"中文输入: '{prompt_zh}'")
print(f"被编码为 {input_ids_zh.shape[1]} 个 token: {tokenizer.convert_ids_to_tokens(input_ids_zh[0])}")
print("开始为中文逐个 token 生成...")
generated_ids_zh = input_ids_zh
with torch.no_grad():
for i in range(5):
outputs = model(generated_ids_zh)
next_token_logits = outputs.logits[:, -1, :]
next_token_id = torch.argmax(next_token_logits, dim=-1).unsqueeze(-1)
new_token = tokenizer.decode(next_token_id[0])
print(f"第 {i+1} 步, 生成 token: '{new_token.strip()}'")
generated_ids_zh = torch.cat([generated_ids_zh, next_token_id], dim=1)
full_decoded_text_zh = tokenizer.decode(generated_ids_zh[0], skip_special_tokens=True)
print(f"\n中文最终生成结果 (出现乱码是正常现象): \n'{full_decoded_text_zh}'")
输出:
(base-llm) PS E:\Datawhale 2026\base-llm202602> & D:/Users/app/miniconda3/envs/base-llm/python.exe "e:/Datawhale 2026/base-llm202602/05_gpt_demo.py"
英文输入: 'I like eating fried'
被编码为 4 个 token: ['I', 'Ġlike', 'Ġeating', 'Ġfried']
开始为英文逐个 token 生成...
第 1 步, 生成 token: 'chicken'
第 2 步, 生成 token: ','
第 3 步, 生成 token: 'but'
第 4 步, 生成 token: 'I'
第 5 步, 生成 token: 'don'
英文最终生成结果:
'I like eating fried chicken, but I don'
中文输入: '我喜欢吃炸'
被编码为 13 个 token: ['æĪ', 'ij', 'å', 'ĸ', 'ľ', 'æ', '¬', '¢', 'åIJ', 'ĥ', 'ç', 'Ĥ', '¸']
开始为中文逐个 token 生成...
第 1 步, 生成 token: '的'
第 2 步, 生成 token: '�'
第 3 步, 生成 token: '�'
第 4 步, 生成 token: '�'
第 5 步, 生成 token: '�'
中文最终生成结果 (出现乱码是正常现象):
'我喜欢吃炸的让�'
输出解释:
为什么 GPT-2 不懂中文?
1. 训练数据
GPT-2 是在英文互联网文本上训练的,它只见过英文单词和句子,没见过汉字。所以它对中文没有任何概念。
2. 分词机制
GPT 使用的是 字节级 BPE(Byte-Pair Encoding) 分词:
-
它的词表里主要是英文单词和一部分常用的子词(比如
ing,ed)。 -
遇到汉字时,它会先把汉字转成 UTF-8 的字节(例如 “吃” 对应三个字节),然后用这些字节去匹配词表。
-
结果一个汉字变成了 2~3 个 token,而且这些 token 在模型眼里只是无意义的字节组合,不是“汉字”。
第三节 T5
一、T5 是什么?
我们之前学了 BERT(擅长理解)和 GPT(擅长生成)。T5 就像它们的“合体”,既懂理解又会生成。它把所有语言任务都看成“把一段文字变成另一段文字”的问题,所以叫 Text-to-Text Transfer Transformer。
举个例子:
-
翻译:输入“把这句话翻成英文:今天天气真好”,输出“The weather is nice today.”
-
情感分析:输入“这句话是正面还是负面?电影真好看”,输出“正面”
-
摘要:输入“写个摘要:昨天我去电影院看了《流浪地球3》,特效震撼,剧情感人”,输出“《流浪地球3》特效震撼,剧情感人”
-
相似度打分:输入“计算相似度:苹果好吃 和 香蕉好吃”,输出“0.8”(T5会直接生成数字文本)
T5 不需要为每个任务设计不同的输出头(比如 BERT 做分类要在上面加一个全连接层),它的输出永远是文本。这种设计让 T5 非常灵活,一个模型就能做很多事。
二、T5 怎么做到的?
1. 任务前缀(Task Prefix)
为了让模型知道当前要干什么,T5 在输入前面加上一句话,比如:
-
translate English to Chinese: I love you -
cola sentence: The cat is sleeping.(判断句子是否语法正确) -
stsb sentence1: ... sentence2: ...(计算句子相似度,stsb 是语义相似度任务)
这其实就是我们现在常说的 提示词(Prompt) 的雏形。
2. 多任务学习
T5 在训练时同时学习翻译、摘要、分类、相似度等很多任务。为了避免大数据集(如翻译)淹没小数据集(如情感分析),T5 采用了一种采样技巧:给每个任务设定一个采样上限,让模型能均衡地学习所有任务。
三、T5 的“特殊本领”
1. 预训练任务:Span Corruption(片段破坏与重构)
BERT 是随机遮掉单个字让你猜,而 T5 是随机遮掉一段连续的文字(比如一次遮掉 3 个字),然后用一个特殊的哨兵符 <extra_id_0> 替换它。模型的任务是猜出这一段是什么。
例如原文:
黑神话悟空是一款以中国神话为背景的动作角色扮演游戏。
Encoder 输入变成:
黑神话悟空是一款<extra_id_0>的动作<extra_id_1>游戏。
Decoder 输出要求生成:
<extra_id_0>以中国神话为背景<extra_id_1>角色扮演<extra_id_2>
注意末尾的 <extra_id_2> 表示结束。
这种任务迫使模型理解更长的上下文,因为要猜的是一段话,而不是一个字。
2. 相对位置编码
BERT 和 GPT 用的是绝对位置编码(给每个位置一个固定向量)。但 T5 认为,两个词之间的相对距离更重要。比如“我吃苹果”中,“吃”和“苹果”的距离是 1,而“我”和“苹果”的距离是 2。
T5 的做法:
-
计算 Query 和 Key 之间的相对距离。
-
用一个可学习的偏置表,把相对距离映射成一个值,直接加到注意力分数上。
-
为了节省参数,把距离分到不同的“桶”里:近距离每个距离一个桶,远距离多个距离共用一个桶(因为语言中太远的关系不需要区分那么细)。
-
这个偏置在每一层共享,进一步减少参数量。
3. SentencePiece 分词器
BERT 分词需要先按空格切分(对中文不友好),而 T5 直接处理原始文本,把空格当成一个特殊字符。这让 T5 能天然支持多语言,不需要为每种语言单独设计分词规则。
t5-small.py
import os
# 设置 Hugging Face 镜像(国内加速)
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
import torch
from transformers import T5Tokenizer, T5ForConditionalGeneration
# 1. 加载模型和分词器(第一次运行会自动下载)
model_name = "t5-small"
tokenizer = T5Tokenizer.from_pretrained(model_name)
model = T5ForConditionalGeneration.from_pretrained(model_name)
# 2. 准备两个不同任务的输入
input_text_1 = "translate English to German: The house is wonderful."
input_text_2 = "stsb sentence1: The rhino grazed on the grass. sentence2: A rhino is grazing in a field."
# 3. 编码(自动添加任务前缀)
inputs = tokenizer([input_text_1, input_text_2], return_tensors="pt", padding=True)
# 4. 生成输出
outputs = model.generate(**inputs)
# 5. 解码并打印结果
print(f"输入 1: {input_text_1}")
print(f"输出 1: {tokenizer.decode(outputs[0], skip_special_tokens=True)}\n")
print(f"输入 2: {input_text_2}")
print(f"输出 2: {tokenizer.decode(outputs[1], skip_special_tokens=True)}")
输出:
输入 1: translate English to German: The house is wonderful.
输出 1: Das Haus ist wunderbar.
输入 2: stsb sentence1: The rhino grazed on the grass. sentence2: A rhino is grazing in a field.
输出 2: 4.0
解释:
-
第一个任务翻译成德语,T5 正确生成了德语。
-
第二个任务计算句子语义相似度,T5 直接生成字符串
"4.0"(满分 5 分),说明它把数值也当作文本处理了。
为什么中文翻译没效果?
原版 T5 只在英文及少量欧洲语言上训练,没有包含中文翻译任务。所以如果写 translate English to Chinese: The house is wonderful.,它不会输出中文。需要使用 Google 的 mT5(多语言版本)才能处理中文。
五、T5 与 BERT、GPT 对比
| 方面 | BERT (Encoder) | GPT (Decoder) | T5 (Encoder-Decoder) |
|---|---|---|---|
| 核心结构 | 只有编码器 | 只有解码器 | 编码器 + 解码器 |
| 擅长任务 | 理解(分类、NER) | 生成(续写、对话) | 理解+生成(翻译、摘要、问答) |
| 任务统一方式 | 加分类头 | 设计提示词 | 任务前缀 + 文本到文本 |
| 预训练任务 | MLM + NSP | 预测下一个词 | Span Corruption |
| 位置编码 | 绝对位置(可学习) | 绝对位置(可学习) | 相对位置(分桶偏置) |
| 输出形式 | 向量 | 文本 | 文本 |
六、总结
-
T5 把一切 NLP 任务统一成 “文本到文本” 的格式,输入是文本,输出也是文本。
-
它用 任务前缀 告诉模型当前要干什么,这是现在大模型 Prompt 的雏形。
-
它的预训练任务 Span Corruption 比 BERT 的完形填空更难,迫使模型理解更长上下文。
-
相对位置编码 让它能更有效地处理长距离依赖,同时节省参数。
-
T5 是真正的“全能选手”,既能做理解任务,也能做生成任务。
第四节 Hugging Face
一、Hugging Face 是什么?
想象一下,有一个巨大的工具箱,里面装满了各种各样的工具(模型、数据集、演示应用),而且这些工具都是别人已经做好的,拿来就能用。Hugging Face 就是这样一个“AI 工具箱”!
它有三个主要部分:
-
模型库(Models):像 GitHub 但专门放 AI 模型,有几十万个预训练好的模型,BERT、GPT、T5 都能在这里找到。
-
数据集库(Datasets):存放各种公开数据集,可以直接拿来用,不用自己到处找。
-
演示空间(Spaces):别人做好的 AI 应用演示,可以在网页上直接体验模型的效果。
Hugging Face 最厉害的地方是,它有三个核心的 Python 库,让我们可以轻松地使用这些模型和数据。
二、Hugging Face 的三大核心库
1. Transformers——模型引擎
这是最核心的库,用来加载和使用预训练模型。只需要几行代码,就能把 BERT、GPT、T5 等模型“请”到电脑里,然后做各种任务。
2. Tokenizers——文本翻译官
模型不认识文字,只认识数字。Tokenizers 就是负责把文字变成数字的“翻译官”,而且它的底层是用 Rust 语言写的,速度飞快!
3. Datasets——数据管家
处理大数据很麻烦,但这个库能高效地加载和处理数据集,即使数据很大(比如几十 GB),它也能轻松应对,不会把内存撑爆。
除了这三个,还有 Evaluate(计算模型分数,比如准确率)、Accelerate(让训练在多张显卡上自动加速)等辅助库。
二、Transformers 核心库详解
2.1 开箱即用的 Pipeline
如果想最快地体验模型的效果,可以用 Pipeline。它就像一个“傻瓜相机”,只要告诉它要做什么任务(比如情感分析),它就会自动加载对应的模型,然后把结果给你。
pipeline.py
import os
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
result = classifier("I love Hugging Face!")
print(result) # 输出:POSITIVE,得分 0.999
输出:
tokenizer_config.json: 48.0B [00:00, 46.3kB/s]
vocab.txt: 232kB [00:00, 750kB/s]
[{'label': 'POSITIVE', 'score': 0.9998641014099121}]

# 图像分类示例(显式指定小模型 apple/mobilevit-small)
# pip install pillow
vision_classifier = pipeline(model="apple/mobilevit-small")
result = vision_classifier("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/coco_sample.png")
result
输出:
[{'label': 'tabby, tabby cat', 'score': 0.30007612705230713}, {'label': 'Egyptian cat', 'score': 0.14065445959568024}, {'label': 'tiger cat', 'score': 0.1392400711774826}, {'label': 'remote control, remote', 'score': 0.01741856336593628}, {'label': 'mouse, computer mouse', 'score': 0.015287423506379128}]
解读:
-
label:模型预测的物体类别(如“虎斑猫”、“埃及猫”、“遥控器”等)。 -
score:模型对这个预测的置信度,范围 0~1,越高越有把握。
从结果看,模型认为图片中最主要的是一只猫(前三个标签都是猫的不同品种),而且置信度比较集中(约 30%)。它还注意到图片中可能有遥控器和鼠标,但置信度很低。这个结果非常合理,因为那张示例图片(coco_sample.png)很可能就是一张包含猫和遥控器的常见 COCO 数据集图片。
2.2 AutoClass:智能加载机制
Hugging Face的AutoClass示例,展示了如何使用AutoTokenizer和AutoModelForSequenceClassification加载BERT模型,对一句文本进行分词、模型推理和后处理。输出包括分词后的input_ids、logits和softmax后的概率。
目的是演示Transformers库的基本使用流程:加载模型、预处理、推理、后处理。
import os
os.environ['HF_HUB_OFFLINE'] = '1' # 强制离线模式
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
# from transformers import AutoTokenizer, AutoModelForSequenceClassification
# 1. 加载模型和分词器
checkpoint = "bert-base-chinese"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
# 2. 分词:文本 -> input_ids
text = "Hugging Face 让 NLP 变得简单"
inputs = tokenizer(text, return_tensors="pt")
print("分词结果 input_ids:", inputs['input_ids'])
# 3. 模型推理:input_ids -> logits
outputs = model(**inputs)
logits = outputs.logits
print("模型输出 logits:", logits)
# 4. 后处理:logits -> 概率
predictions = torch.nn.functional.softmax(logits, dim=-1)
print("Softmax 概率:", predictions)
输出:
分词结果 input_ids: tensor([[ 101, 100, 100, 6375, 100, 1359, 2533, 5042, 1296, 102]])
模型输出 logits: tensor([[-0.4223, 0.0902]], grad_fn=<AddmmBackward0>)
Softmax 概率: tensor([[0.3746, 0.6254]], grad_fn=<SoftmaxBackward0>)
解读:
1. 分词结果
这一行数字表示句子 "Hugging Face 让 NLP 变得简单" 被转换成的数字密码。每个数字代表一个词或字符:
-
101:句子开头的特殊标记 [CLS] -
100:表示模型不认识"Hugging"、"Face"、"NLP"这几个英文词(因为它是中文模型),所以用[UNK](未知词)标记。 -
6375:代表汉字“让” -
1359:代表“变” -
2533:代表“得” -
5042:代表“简” -
1296:代表“单” -
102:句子结尾标记 [SEP]
结论:分词器工作正常,把句子转成了模型能理解的数字。
2. 模型输出 logits
这是模型对这句话的两个“原始打分”:
-
第一个分数:
-0.4223(较低) -
第二个分数:
0.0902(较高)
如果这是一个情感分类任务,可以理解为模型认为这句话偏向第二个类别(比如“正面”)。但是,因为模型没有经过情感分类的训练,这两个分数只是随机初始化的结果,没有实际意义。
3. Softmax 概率
把原始分数换算成概率,总和为 1:
-
第一类概率:
37.46% -
第二类概率:
62.54%
同样,这只是数学转换,并不代表模型真的认为这句话 62% 是正面。
走通了 Hugging Face 的核心流程:这是使用任何预训练模型的基础。以后您就可以在此基础上微调模型,做真正的分类任务。
三、使用 Datasets 构建数据流水线
pip install datasets -i https://mirrors.aliyun.com/pypi/simple/
import os
# 可选:如果希望离线加载已缓存的数据集,取消下一行的注释
# os.environ['HF_DATASETS_OFFLINE'] = '1'
# 可选:设置 Hugging Face 镜像(国内加速)
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from datasets import load_dataset
from transformers import AutoTokenizer
# 将 tokenizer 定义在全局作用域,以便子进程可以访问
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
def tokenize_function(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=128)
if __name__ == '__main__':
# 1. 加载烂番茄影评数据集(第一次运行会自动下载)
print("正在加载数据集...")
dataset = load_dataset("rotten_tomatoes")
print("数据集加载完成!")
print(dataset)
# 2. 查看一个样本
print("\n训练集第一个样本:")
print(dataset["train"][0])
# 3. 对数据集进行并行预处理
print("\n开始对训练集进行分词预处理...")
tokenized_datasets = dataset.map(
tokenize_function,
batched=True, # 批量处理,加速
num_proc=2, # 使用 2 个进程并行(可根据 CPU 核心数调整)
remove_columns=["text"] # 处理完后可以移除原始文本列,节省内存
)
print("预处理完成!")
print("预处理后的训练集特征:", tokenized_datasets["train"].column_names)
print("第一个样本的 input_ids 长度:", len(tokenized_datasets["train"][0]["input_ids"]))
输出:
(base-llm) PS E:\Datawhale 2026\base-llm202602> & D:/Users/app/miniconda3/envs/base-llm/python.exe "e:/Datawhale 2026/base-llm202602/05_datasets_demo.py"
正在加载数据集...
数据集加载完成!
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 8530
})
validation: Dataset({
features: ['text', 'label'],
num_rows: 1066
})
test: Dataset({
features: ['text', 'label'],
num_rows: 1066
})
})
训练集第一个样本:
{'text': 'the rock is destined to be the 21st century\'s new " conan " and that he\'s going to make a splash even greater than arnold schwarzenegger , jean-claud van damme or steven segal .', 'label': 1}
开始对训练集进行分词预处理...
Map (num_proc=2): 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████| 8530/8530 [00:11<00:00, 724.16 examples/s]
Map (num_proc=2): 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 1066/1066 [00:11<00:00, 94.93 examples/s]
Map (num_proc=2): 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 1066/1066 [00:11<00:00, 94.64 examples/s]
预处理完成!
预处理后的训练集特征: ['label', 'input_ids', 'token_type_ids', 'attention_mask']
第一个样本的 input_ids 长度: 128
解读:
一、数据集加载成功
DatasetDict({
train: Dataset({ features: ['text', 'label'], num_rows: 8530 })
validation: Dataset({ features: ['text', 'label'], num_rows: 1066 })
test: Dataset({ features: ['text', 'label'], num_rows: 1066 })
})
-
加载的数据集被自动划分为三部分:
-
训练集(train):8530条影评,用于模型训练。
-
验证集(validation):1066条影评,用于训练过程中调参和早停。
-
测试集(test):1066条影评,用于最终评估模型性能。
-
-
每个样本包含两个字段:
-
text:影评的原始文本。 -
label:情感标签(0表示负面,1表示正面)。
-
二、第一个样本预览
训练集第一个样本:
{'text': 'the rock is destined to be the 21st century\'s new " conan " and that he\'s going to make a splash even greater than arnold schwarzenegger , jean-claud van damme or steven segal .', 'label': 1}
-
这是一条正面的影评(
label: 1),内容是对演员“the rock”(巨石强森)的赞誉。
三、预处理过程
Map (num_proc=2): 100%|████| 8530/8530 [00:11<00:00, 724.16 examples/s] Map (num_proc=2): 100%|████| 1066/1066 [00:11<00:00, 94.93 examples/s] Map (num_proc=2): 100%|████| 1066/1066 [00:11<00:00, 94.64 examples/s]
-
代码中的
dataset.map(...)将您定义的tokenize_function并行应用到每个子集的每一条文本上。 -
您开启了 2 个进程(
num_proc=2),处理速度分别为:-
训练集:724.16 条/秒
-
验证集:94.93 条/秒
-
测试集:94.64 条/秒
-
-
进度条显示三个子集均已完成预处理。
四、预处理结果
预处理后的训练集特征: ['label', 'input_ids', 'token_type_ids', 'attention_mask'] 第一个样本的 input_ids 长度: 128
-
原数据集中的
text字段已被移除(remove_columns=["text"]),新增了三个模型可用的特征:-
input_ids:文本分词后对应的 token ID 序列(长度统一为 128,由padding="max_length"控制)。 -
token_type_ids:用于区分不同句子的标志(这里只有一个句子,所以全为 0)。 -
attention_mask:标记哪些位置是真实 token(值为 1),哪些是填充 token(值为 0),供模型在计算注意力时忽略填充部分。
-
-
label字段依然保留,作为分类目标。
四、Trainer 与 Evaluate
05_load_dataset.py
pip install evaluate -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install accelerate -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
05_load_dataset.py
import os
import numpy as np
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
TrainingArguments,
Trainer,
DataCollatorWithPadding
)
import evaluate
# 可选:设置 Hugging Face 镜像(国内加速)
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
# 1. 加载数据集
print("加载数据集...")
dataset = load_dataset("rotten_tomatoes")
# 2. 加载分词器
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
# 3. 定义分词函数并预处理数据集
def tokenize_function(examples):
return tokenizer(examples["text"], truncation=True, max_length=128)
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# 4. 取小部分数据用于快速演示(可根据需要调整或使用全量数据)
small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(100))
small_eval_dataset = tokenized_datasets["validation"].shuffle(seed=42).select(range(100))
# 5. 加载模型(二分类)
model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased", num_labels=2)
# 6. 设置训练参数
training_args = TrainingArguments(
output_dir="./trainer_demo",
eval_strategy="epoch", # 每轮结束后评估
num_train_epochs=3, # 训练轮数
per_device_train_batch_size=16, # 训练批次大小
per_device_eval_batch_size=16, # 评估批次大小
weight_decay=0.01, # 权重衰减
save_strategy="epoch", # 每轮保存一次
load_best_model_at_end=True, # 训练结束后加载最佳模型
metric_for_best_model="accuracy", # 以准确率为准选择最佳模型
report_to="none" # 不向外部报告
)
# 7. 定义评估指标(准确率)
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
# 8. 数据收集器(动态填充)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
# 9. 初始化 Trainer(注意:已移除 tokenizer 参数)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=small_train_dataset,
eval_dataset=small_eval_dataset,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
# 10. 开始训练
print("开始训练...")
trainer.train()
# 11. 最终评估
results = trainer.evaluate()
print("评估结果:", results)
输出:
(base-llm) PS E:\Datawhale 2026\base-llm202602> & D:/Users/app/miniconda3/envs/base-llm/python.exe "e:/Datawhale 2026/base-llm202602/05_load_dataset.py"
加载数据集...
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Map: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1066/1066 [00:00<00:00, 20639.28 examples/s]
Loading weights: 100%|██████████████████████████████████████| 100/100 [00:00<00:00, 2277.40it/s, Materializing param=distilbert.transformer.layer.5.sa_layer_norm.weight]
DistilBertForSequenceClassification LOAD REPORT from: distilbert-base-uncased
Key | Status |
------------------------+------------+-
vocab_layer_norm.bias | UNEXPECTED |
vocab_transform.bias | UNEXPECTED |
vocab_projector.bias | UNEXPECTED |
vocab_transform.weight | UNEXPECTED |
vocab_layer_norm.weight | UNEXPECTED |
pre_classifier.bias | MISSING |
classifier.weight | MISSING |
pre_classifier.weight | MISSING |
classifier.bias | MISSING |
Notes:
- UNEXPECTED :can be ignored when loading from different task/architecture; not ok if you expect identical arch.
- MISSING :those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.
开始训练...
{'eval_loss': '0.6868', 'eval_accuracy': '0.63', 'eval_runtime': '0.1101', 'eval_samples_per_second': '908.4', 'eval_steps_per_second': '63.59', 'epoch': '1'}
Writing model shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.18it/s]
{'eval_loss': '0.6731', 'eval_accuracy': '0.58', 'eval_runtime': '0.093', 'eval_samples_per_second': '1075', 'eval_steps_per_second': '75.28', 'epoch': '2'}
Writing model shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.15it/s]
{'eval_loss': '0.6648', 'eval_accuracy': '0.57', 'eval_runtime': '0.0959', 'eval_samples_per_second': '1042', 'eval_steps_per_second': '72.96', 'epoch': '3'}
Writing model shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:01<00:00, 1.56s/it]
There were missing keys in the checkpoint model loaded: ['distilbert.embeddings.LayerNorm.weight', 'distilbert.embeddings.LayerNorm.bias'].| 1/1 [00:01<00:00, 1.56s/it]
There were unexpected keys in the checkpoint model loaded: ['distilbert.embeddings.LayerNorm.beta', 'distilbert.embeddings.LayerNorm.gamma'].
{'train_runtime': '15.64', 'train_samples_per_second': '19.18', 'train_steps_per_second': '1.343', 'train_loss': '0.6474', 'epoch': '3'}
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 21/21 [00:15<00:00, 1.34it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 36.97it/s]
评估结果: {'eval_loss': 0.6868215203285217, 'eval_accuracy': 0.63, 'eval_runtime': 0.3436, 'eval_samples_per_second': 291.024, 'eval_steps_per_second': 20.372, 'epoch':
3.0}
(base-llm) PS E:\Datawhale 2026\base-llm202602>
解读:
一、训练过程解读
text
开始训练...
Epoch 1: {'eval_loss': '0.6868', 'eval_accuracy': '0.63', ...}
Epoch 2: {'eval_loss': '0.6731', 'eval_accuracy': '0.58', ...}
Epoch 3: {'eval_loss': '0.6648', 'eval_accuracy': '0.57', ...}
-
每个 epoch 结束后,模型在验证集(100 条数据)上进行了评估。
-
eval_loss:交叉熵损失,越低越好。从 0.6868 缓慢下降到 0.6648,说明模型在学习。 -
eval_accuracy:准确率。第 1 轮 63%,后面两轮略有下降(58% 和 57%),这可能是由于只用了 100 条数据,模型不稳定或过拟合。在更大数据集上,准确率通常会持续上升。
最终评估结果(可能是最后一个 epoch 或最佳模型):
评估结果: {'eval_loss': 0.6868215203285217, 'eval_accuracy': 0.63, ...}
-
最终准确率为 63%(因为只用了 100 条训练数据,这个结果合理,全量数据下应能达到 80% 以上)。
更多推荐
所有评论(0)