作业:

一、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 微调 
模型定义 手动定义 EmbeddingLSTM、全连接层,代码约 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 的学习分两步:

  1. 预训练:就像让一个学生先读很多很多书(比如维基百科、各种文章),让他自己总结语言规律。这个阶段不需要老师教,模型自己通过做“填空题”和“句子配对题”来学习。

  2. 微调:当我们要做具体任务时(比如判断电影评论是好评还是差评),就把预训练好的 BERT 拿出来,在它的头顶加一个简单的分类器,再用我们自己的数据稍微训练一下。因为 BERT 已经懂语言了,所以只需要很少的数据就能学得很好。

这叫迁移学习,就像学会了骑自行车,再去学骑电动车就很容易一样。

三、BERT 长什么样?

BERT 用的是 Transformer 的编码器部分,但比原来的更大、更深。常见的有两种:

  • BERT-Base:12 层,每层 768 个神经元,12 个注意力头(参数 1.1 亿)

  • BERT-Large:24 层,每层 1024 个神经元,16 个注意力头(参数 3.4 亿)

它的输入不是单纯一个词向量,而是由三个向量相加组成的:

  1. 词元嵌入:每个词自己的向量。

  2. 位置嵌入:告诉模型这个词在句子的第几个位置(BERT 用的是可学习的,最大支持 512 个位置)。

  3. 片段嵌入:如果输入是两个句子(比如“我喜欢猫”和“它很可爱”),第一个句子的所有词加一种标记,第二个句子的词加另一种标记,用来区分它们。

四、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 中)有两大部件:

  1. 掩码多头自注意力

    • 它跟普通的自注意力很像,但多了个“掩码”。这个掩码会遮住未来的词,保证模型在预测下一个词时,只能看到前面已经出现的词,不能偷看后面的。

    • 这样模型才能学会真正的“续写”。

  2. 前馈网络(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) 分词:

  • 它的词表里主要是英文单词和一部分常用的子词(比如 inged)。

  • 遇到汉字时,它会先把汉字转成 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% 以上)。


更多推荐