NLP预训练模型:从原理到实战,一篇讲透GPT、BERT与T5

0 阅读24分钟

2018年至今的NLP技术演进,一幅完整的发展蓝图

一、引言:NLP的技术革命

自然语言处理(NLP)领域在过去的近十年里经历了一场深刻的技术革命。如果让我用一句话概括这场革命的核心——那就是“预训练+微调”范式的确立与普及。

在Transformer 架构 出现之前,NLP领域长期受困于一个核心矛盾:模型想表现好,需要海量标注数据;而高质量标注数据的获取,成本高、周期长、专业门槛高。传统的RNN和LSTM架构虽然在一定程度上解决了序列建模问题,但串行计算机制导致训练效率低下,梯度消失问题更是限制了深层网络的表达能力。

2017年,Google团队发表了题为《Attention Is All You Need》的论文,提出了Transformer架构,彻底改变了NLP领域的格局。自此,一场围绕Transformer展开的预训练模型军备竞赛拉开帷幕。

二、为什么Transformer成为预训练模型的“地基”?

在深入具体的模型之前,有必要先理解Transformer为何能成为所有预训练模型的基础架构。

1. 并行计算:从“串行瓶颈”到“涡轮增压”

RNN必须按顺序逐词处理,好比一个人只能一页一页地读书。而Transformer可以同时处理整个序列,就像能够瞬间翻阅整本书的“超级读者”。这种并行化特性使得训练速度提升数十倍,让大规模预训练成为可能。

2. 全局自注意力:长距离依赖不再是问题

自 注意力机制 允许模型在处理每个词时,“关注”输入序列中的所有其他词。通过Query、Key、Value三者的矩阵运算,Transformer可以直接建立任意位置之间的联系,完美解决了RNN处理长文本时“读到后面忘了前面”的困境。

3. 结构灵活:统一的语言建模框架

Transformer的编码器-解码器架构极其通用。只取解码器,得到自回归生成模型(GPT系列);只取编码器,得到双向理解模型(BERT系列);全都要,得到序列到序列模型(T5系列)。这种“乐高式”的设计思想,为后续的模型演化提供了无限可能。

三、三大流派:解码器、编码器、编码器-解码器

根据Transformer的使用方式不同,预训练模型大致可分为三类:

1)解码器(Decoder-only)模型:GPT系列

核心思想:自回归语言建模。预测下一个词是什么,所有生成任务的基础。

代表模型:OpenAI的GPT系列(GPT-1→GPT-2→GPT-3→GPT-4→GPT-4o)。

2)编码器(Encoder-only)模型:BERT系列

核心思想:双向上下文理解。同时利用词左侧和右侧的信息,为理解类任务而生。

代表模型:Google的BERT及其变体(RoBERTa、ALBERT、ELECTRA等)。

3)编码器-解码器(Encoder-Decoder)模型:T5系列

核心思想:统一框架,将所有任务转化为“文本到文本”问题。

代表模型:Google的T5( Text -to-Text Transfer Transformer)。

下图清晰展示了2018年至2023年间各大模型的发展脉络:

四、GPT:从1.17亿到万亿参数的演进之路

4.1 GPT-1:奠基之作(2018)

GPT-1是OpenAI于2018年6月提出的首个生成式预训练模型,论文题为《Improving Language Understanding by Generative Pre-Training》。它仅使用了Transformer的解码器部分,参数量约为1.17亿。

模型结构细节

GPT-1采用了12层解码器堆叠,每个解码器层包含掩码多头自注意力(12头)和前馈网络两个子层,模型维度为768。与原始Transformer的一个关键区别在于:GPT-1使用了可学习的位置嵌入(learnable positional embedding) ,而非不可训练的三角函数编码。这意味着模型可以在训练过程中自动优化位置向量。

预训练:生成式语言建模

GPT的预训练采用自回归语言建模:基于已观察到的前文上下文,预测当前位置的词。训练样本可以直接从原始文本中自动构建,无需人工标注,极大地降低了数据成本。

在实践中,GPT-1使用了BooksCorpus语料库,包含约7000本小说的完整文本,总规模约8亿词。该语料语言自然、上下文完整,非常适合训练具备长距离依赖建模能力的语言模型。

微调:统一输入格式设计

GPT的微调策略极具巧思:在预训练模型顶部添加线性输出层,并将各种下游任务统一转化为文本输入格式。

以图中的文本分类任务为例,假设我们有一个带标注的微调数据集如下:

首先,将每条评论转为 token 序列,并添加特殊标记 [Start] 与 [Extract],形成模型标准输入格式:

然后,将转换后的序列送入 GPT 模型。模型逐层处理后,输出每个位置的预测。我们只提取序列中最后一个位置 [Extract] 对应的输出,再通过新添加的线性输出层完成分类预测,最中输出标签“0”或“1”。如下图所示:

通过这种方式,GPT 在保留预训练模型结构和参数的基础上,仅添加极少量新参数(如线性层),便可高效完成从语言建模到多种下游任务的迁移。

此外,统一的输入格式设计进一步简化了多任务处理流程,使 GPT 能以一致的方式应对多种 NLP 任务,从而展现出强大的通用性与扩展性。

4.2 GPT-2:规模扩展与零样本学习(2019)

GPT-2将参数量扩展至15亿,首次尝试无监督多任务学习——试图用同一个模型适配不同的文本任务。它生成的文本流畅度震惊了学术界,OpenAI甚至一度因担忧滥用而拒绝发布完整模型。

4.3 GPT-3:涌现能力的诞生(2020)

GPT-3是GPT系列的技术分水岭。参数量跃升至1750亿,在万亿级数据上预训练。更重要的是,GPT-3首次展现出涌现能力:数学推理、代码生成、少样本学习,这些能力在小规模模型中完全不存在,却在规模突破某个临界点后突然涌现。GPT-3也是第一个通过商业API向公众开放的模型,开启了生成式AI创业的热潮。

4.4 GPT-4与GPT-4o:多模态与实时交互(2023-2024)

GPT-4参数量超过万亿,支持多模态输入(文本+图像),复杂逻辑推理能力大幅提升。2024年3月发布的GPT-4o进一步实现了多模态实时交互,支持视频和音频输入,向通用智能迈出了关键一步。

4.5 GPT-5争议:预训练是否已遇瓶颈?

值得注意的是,2025年底的行业报告指出,自GPT-4o发布以来,OpenAI在核心预训练领域遭遇了约两年半的瓶颈期,未完成针对下一代前沿模型的完整大规模预训练。这一现状为GPT-5未能达到业界预期提供了重要解释。这引发了业内对“Scaling Law是否依然有效”的深刻反思——当模型规模遇到天花板,技术创新将向何处去?

五、BERT:双向编码的革命

5.1 核心思想与模型规模

BERT(Bidirectional Encoder Representations from Transformers)由Google于2018年10月提出,论文题为《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》。

BERT的核心创新在于采用Transformer编码器结构,通过双向自注意力机制,在建模每个token表示时同时整合左右两个方向的上下文信息。

BERT提供两种规格:

模型版本

层数

模型维度

注意力头数

参数量

BERT-base

12

768

12

1.1亿

BERT-large

24

1024

16

3.4亿

5.2 输入表示:三重嵌入的巧妙设计

BERT的输入表示由三部分嵌入相加而成:

  • Token Embedding

    :词本身的语义表示

  • Position Embedding

    :位置信息(可学习)

  • Segment Embedding

    :区分句子对中的两个句子

此外,BERT引入了两个特殊符号:

  • [CLS]:句首标志,其输出向量用于下游分类任务

  • [SEP]:句间分隔符

5.3 预训练:MLM + NSP

BERT的预训练包含两个核心任务:

掩码语言模型(MLM)——实现真正的双向建模

BERT随机遮盖输入序列中约15%的token,训练模型根据上下文预测被遮盖的词。遮盖策略如下:

  • 80%的被遮盖token替换为[MASK]

  • 10%替换为随机词

  • 10%保持原词不变

这种“留一手”的设计很巧妙:若100%用[MASK]替换,预训练和微调阶段就会出现不一致(微调时没有[MASK]);若保留10%不变,则强迫模型真正理解上下文语义,而非机械地学习[MASK]到原词的映射。

下一句预测(NSP)——学习句子间关系

  • 50%的训练样本是上下文中真实相邻的句子(正例)

  • 50%是从语料中随机采样的非相邻句子(反例)

模型使用[CLS]的输出进行二分类判断。

5.4 微调:四类典型任务的适配方式

BERT通过添加简单的任务特定层适配下游任务:

(a) 句子对分类任务(如蕴含判断、语义相似度)

输入格式:[CLS] 句子1 [SEP] 句子2 [SEP],使用[CLS]的输出接入线性层进行分类。

(b) 单句分类任务(如情感分析、语法判断)

输入格式:[CLS] 句子 [SEP],同样使用[CLS]的输出向量。

(c) 问答任务(如SQuAD)

输入格式:[CLS] 问题 [SEP] 段落 [SEP],对每个token分别预测其作为答案起始位置和结束位置的概率。

(d) 序列标注任务(如命名实体识别NER)

对每个token的输出向量单独进行分类,如判断是否为人名(B-PER)、地名(B-LOC)等。

5.5 BERT的重要变体

BERT发布后,学术界和工业界涌现了大量变体:

  • RoBERTa

    :Facebook出品,核心思想是“大力出奇迹”——更大的batch size、更多的训练数据、更长的训练时间、动态Mask策略。结果显示原始BERT可能训练不足,远未充分学习数据中的语言知识。

  • ALBERT

    :通过参数共享大幅减少参数量,同时引入句子顺序预测(SOP)替代NSP,在保持性能的同时提升了训练效率。

  • ELECTRA

    :采用生成器-判别器架构,用替换token检测(RTD)替代MLM,训练效率更高。

  • DistilBERT/TinyBERT

    :通过知识蒸馏将模型压缩至更小体积,适合边缘设备部署。

  • SpanBERT

    :改预训练为连续片段遮盖(Span Masking),并引入边界目标(Span Boundary Objective),在问答任务上表现更优。

六、T5:将所有任务统一为“文本到文本”

6.1 核心理念

T5(Text-to-Text Transfer Transformer)由Google于2019年10月提出,论文题为《Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer》。

T5的核心思想极其简洁优雅:将所有NLP任务统一表示为“文本到文本”的转换问题。无论输入是文本分类、问答还是翻译,模型的输入输出均是自然语言形式的字符串。

例如:

  • 翻译:输入“translate English to German: That is good.” → 输出“Das ist gut.”

  • 情感分类:输入“sentiment: This movie was great.” → 输出“positive”

  • 问答:输入“question: What is the capital of France? context: France is a country...” → 输出“Paris”

6.2 预训练:损坏跨度预测

T5的预训练目标被称为Corrupted span prediction,具体过程:

  1. 随机遮盖输入文本中的若干连续片段

  2. 将每个被遮盖的连续片段替换为一个特殊token(如、)

  3. 令模型学习生成这些遮盖片段的内容

这种方式既保留了模型的双向建模能力(编码器部分),又为训练提供了明确的“生成式”学习信号(解码器部分),使模型能够更自然地适配下游生成任务。

6.3 架构特点

T5采用完整的Transformer编码器-解码器架构,兼顾了BERT的双向理解能力和GPT的生成能力。它也是第一个在完整Transformer架构上实现“预训练+微调”范式的模型,为后来的统一模型(如BART、mT5等)奠定了基础。

七、HuggingFace:让预训练模型触手可及

HuggingFace Transformers库已成为NLP开发者的必备工具,它提供了数千个预训练模型的统一接口,极大地简化了模型的使用和微调过程。

7.1 环境搭建

# 创建虚拟环境
python -m venv transformers_env
 
# 激活环境(Windows)
.\transformers_env\Scripts\activate
 
# 安装核心依赖
pip install transformers==4.37.2
pip install torch==2.2.0
pip install datasets==2.17.0
pip install accelerate==0.27.0

7.2 快速上手:AutoTokenizer与AutoModel

from transformers import AutoTokenizer, AutoModel
 
# 加载中文BERT的分词器和模型
tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
model = AutoModel.from_pretrained('bert-base-chinese')
 
# 文本预处理
text = "这是一段测试文本"
inputs = tokenizer(text, return_tensors="pt")  # 返回PyTorch张量
 
# 模型推理
outputs = model(**inputs)
# outputs.last_hidden_state: (batch_size, seq_len, hidden_size)

7.3 Pipeline:一行代码搞定推理

from transformers import pipeline
 
# 情感分析pipeline
classifier = pipeline("sentiment-analysis", model="bert-base-chinese")
result = classifier("这家餐厅的味道太棒了!")
print(result)  # [{'label': 'POSITIVE', 'score': 0.99...}]

八、案例实操:基于BERT的评论情感分析

下面我们将理论付诸实践,基于BERT构建一个电商评论情感分析系统。项目整体结构如下:

review_analyze_bert/
├── data/                      # 数据目录
│   ├── processed/             # 预处理后的数据
│   └── raw/                   # 原始数据
├── logs/                      # 训练日志
├── models/                    # 保存训练好的模型参数
├── pretrained/                # 本地预训练模型目录
└── src/                       # 源码目录
    ├── config.py              # 超参配置
    ├── dataset.py             # 自定义Dataset
    ├── model.py               # 模型结构定义
    ├── preprocess.py             # 数据预处理脚本
    ├── train.py               # 模型训练脚本
    ├── predict.py             # 模型推理脚本
    └── evaluate.py            # 模型评估脚本

8.1 配置文件(config.py)

from pathlib import Path    # 路径定义
 
# 1. 目录路径
# 项目根目录
ROOT_DIR = Path(__file__).parent.parent
# 数据目录
RAW_DATA_DIR = ROOT_DIR / 'data' / 'raw'
PROCESSED_DATA_DIR = ROOT_DIR / 'data' / 'processed'
# 模型目录
MODEL_DIR = ROOT_DIR / 'models'
# 日志目录
LOG_DIR = ROOT_DIR / 'logs'
 
# 2. 文件
RAW_DATA_FILE = 'online_shopping_10_cats.csv'
BEST_MODEL = 'best_model.pt'    # 最优模型参数文件
 
# 3. 超参数
SEQ_LEN = 128     # 序列长度
BATCH_SIZE = 64
EMBEDDING_SIZE = 128
HIDDEN_SIZE = 768
 
LEARNING_RATE = 1e-5
EPOCHS = 10

设计说明:将所有可配置参数集中管理,便于调整和复用。SEQ_LEN=128是基于BERT的512上限和实际评论长度综合考量;LEARNING_RATE=1e-5是BERT微调的经验值,过大可能导致预训练知识被破坏。

8.2 数据预处理(preprocess.py)

# 数据预处理
 
from config import *
 
from datasets import load_dataset, ClassLabel  # 加载数据集
from transformers import AutoTokenizer
 
 
def preprocess():
    print("-------开始数据预处理...-------")
 
    # 1. 读取csv文件,得到字典,提取Dataset
    dataset = load_dataset('csv', data_files=str(RAW_DATA_DIR/RAW_DATA_FILE))['train']
    # print(dataset)
 
    # 2. 去掉cat列,数据过滤
    dataset = dataset.remove_columns(['cat'])
    dataset = dataset.filter( lambda x: x['review'] is not None )
    # print(dataset)
 
    # 3. 对原始语料做划分,按label分层抽样
    dataset = dataset.cast_column('label', ClassLabel(names=['n', 'p']))
    dataset_dict = dataset.train_test_split(test_size=0.2, stratify_by_column='label')
    # print(dataset_dict)
 
    # 4. 加载中文BERT的分词器和模型,注:不能科学上网记得用国内镜像
    tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
 
    # 5. 构建数据集
    def batch_encode(example):
        inputs = tokenizer(
            example['review'],
            padding="max_length",
            max_length=128,
            truncation=True,
        )
        # 添加标签字段labels
        inputs['labels'] = example['label']
        return inputs
 
    dataset_dict = dataset_dict.map(batch_encode, batched=True, remove_columns=['label', 'review'])
    print(dataset_dict)
 
    # 6. 保存数据集到文件
    dataset_dict.save_to_disk( PROCESSED_DATA_DIR )
 
    print("-------数据预处理完成-------")
 
 
if __name__ == '__main__':
    preprocess()

代码解析:上述代码完成了从原始CSV到模型输入的完整转换流程。train_test_split中的seed=42确保结果可复现;stratify_by_column='label'保证正负样本在训练集和测试集中的分布与原始数据一致。分词时同时生成input_ids(token对应的数字ID)和attention_mask(标识哪些位置是真实token,哪些是填充),这是BERT输入的标准格式。

打印结果:

Map: 100%|██████████| 50218/50218 [00:03<00:00, 15182.18 examples/s]
Map: 100%|██████████| 12555/12555 [00:00<00:00, 19263.53 examples/s]
DatasetDict({
    train: Dataset({
        features: ['input_ids', 'token_type_ids', 'attention_mask', 'labels'],
        num_rows: 50218
    })
    test: Dataset({
        features: ['input_ids', 'token_type_ids', 'attention_mask', 'labels'],
        num_rows: 12555
    })
})
Saving the dataset (1/1 shards): 100%|██████████| 50218/50218 [00:00<00:00, 1323873.25 examples/s]
Saving the dataset (1/1 shards): 100%|██████████| 12555/12555 [00:00<00:00, 1090868.33 examples/s]
-------数据预处理完成-------

8.3 自定义数据集(dataset.py)

import torch
from torch.utils.data import DataLoader
 
from config import *
 
from datasets import load_from_disk
 
 
# 获取DataLoader的函数
def get_dataloader(train=True):
    path = str(PROCESSED_DATA_DIR / ('train' if train else 'test'))
    dataset = load_from_disk(path)
    dataset.set_format( type='torch' )
    dataloader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
    return dataloader
 
if __name__ == '__main__':
    train_dataloader = get_dataloader(train=True)
    test_dataloader = get_dataloader(train=False)
 
    for batch in train_dataloader:
        for k, v in batch.items():
            print(k, ' → ', v.shape)
        break

8.4 模型定义(model.py)

import torch
import torch.nn as nn
 
from config import *
 
from transformers import AutoModel
 
# 自定义神经网络类(基于GRU)
class ReviewAnalysisModel(nn.Module):
    # 初始化
    def __init__(self,  freeze_bert=True):
        super().__init__()
        # 加载本地预训练的BERT模型,注:不能科学上网记得用国内镜像
        self.bert =  AutoModel.from_pretrained('bert-base-chinese')
        # 分类器:接收[CLS]向量(维度hidden_size),输出二分类的logit
        self.linear = nn.Linear(in_features=self.bert.config.hidden_size, out_features=1)
 
        # 是否冻结BERT参数(只训练分类器部分)
        # freeze_bert=True适用于小数据集,可防止过拟合,同时大幅降低训练成本
        if freeze_bert:
            for param in self.bert.parameters():
                param.requires_grad = False
 
 
    # 前向传播
    def forward(self, input_ids, attention_mask, token_type_ids):
        # BERT 前向传播,得到输出
        output = self.bert(input_ids, attention_mask, token_type_ids)
        # 提取output中CLS对应的输出隐状态,形状 (N, hidden_size)
        cls_hidden_stat = output.pooler_output
 
        # 全连接层整合特征,得到预测输出,形状 (N, 1),再降维成一维 (N,)
        result = self.linear(cls_hidden_stat).squeeze(-1)
 
        return result
 
if __name__ == '__main__':
 
    model = ReviewAnalysisModel()
    print(model)

模型设计思路:BERT的[CLS] token在预训练中被专门训练用于聚合整个序列的语义信息,因此用它做分类是标准做法。freeze_bert参数提供了一种灵活的训练策略:冻结BERT参数时只训练分类器,适合小数据集场景;解冻时则进行全参数微调,适合大数据集场景,效果通常更好但需要更多计算资源。

8.5 模型训练(train.py)

import torch
from torch import nn, optim
 
from tqdm import tqdm   # 进度条工具
 
from config import *
from dataset import get_dataloader  # 获取数据加载器
from model import ReviewAnalysisModel  # 模型
 
from torch.utils.tensorboard import SummaryWriter   # 日志写入器
import time # 时间库
 
 
# 定义训练引擎函数:训练一个epoch,返回平均损失
def train_one_epoch(model, train_loader, loss, optimizer, device):
    model.train()
 
    total_loss = 0
 
    # 按批次进行迭代,每一批是一个 k-v 字典
    for batch in tqdm(train_loader, desc='训练:'):
        inputs = { k:v.to(device) for k,v in batch.items() }
        targets = inputs.pop('labels').to(dtype=torch.float)
        # 前向传播,解包字典作为参数
        outputs = model(**inputs)
        # 计算损失
        loss_value = loss(outputs, targets)
        # 反向传播
        loss_value.backward()
        # 更新参数
        optimizer.step()
        # 梯度清零
        optimizer.zero_grad()
        # 累加损失
        total_loss += loss_value.item()
    return total_loss / len(train_loader)
 
# 训练整体流程
def train():
    # 选择运行设备
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f"device: {device}")
 
    # 创建数据加载器
    train_loader = get_dataloader()
    print("数据集加载完成")
 
    # 初始化模型并移动到设备
    # freeze_bert=False表示对BERT进行全参数微调,效果通常更好
    model = ReviewAnalysisModel(freeze_bert=False).to(device)
 
    # BCEWithLogitsLoss = Sigmoid + BCELoss,数值稳定性更好
    loss = nn.BCEWithLogitsLoss()
 
    # Adam优化器,学习率设置为1e-5(BERT微调的标准配置)
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
 
    # 7. 定义一个tensorboard写入器
    writer = SummaryWriter(log_dir=LOG_DIR / time.strftime('%Y-%m-%d_%H-%M-%S'))
 
    # 8. 核心训练流程,按epoch进行迭代
    min_loss = float('inf')     # 记录最小训练损失
    for epoch in range(EPOCHS):
        print("="*10, f"EPOCH:{epoch+1}", "="*10)
        this_loss = train_one_epoch(model, train_loader, loss, optimizer, device)
        print("本轮训练损失:", this_loss)
 
        # 将损失写入日志
        writer.add_scalar('loss', this_loss, epoch+1)
 
        # 判断损失是否下降,保存最优模型
        if this_loss < min_loss:
            min_loss = this_loss
            torch.save( model.state_dict(), MODEL_DIR / BEST_MODEL )
            print("模型保存成功!")
    # 关闭写入器
    writer.close()
 
if __name__ == '__main__':
    train()

注:我的显卡内存是16g,如果显卡内存不够会导致使用共享内存(就是电脑内存),这样会导致训练速度变慢,可以降低BATCH_SIZE大小,让其完全使用显卡内存就好。

训练策略说明:使用BCEWithLogitsLoss而非手动Sigmoid+BCELoss是因为前者在数值计算上更稳定(将Sigmoid计算融合进损失函数中)。torch.save(model.state_dict(), ...)只保存模型参数而非完整模型对象,便于后续加载和部署,也节省了存储空间。TensorBoard的add_scalar记录了每个epoch的loss曲线,便于监控训练过程。

打印结果:

========== EPOCH:9 ==========
训练:: 100%|██████████| 785/785 [02:42<00:00,  4.83it/s]
本轮训练损失: 0.02145332312359694
模型保存成功!
========== EPOCH:10 ==========
训练:: 100%|██████████| 785/785 [02:43<00:00,  4.80it/s]
本轮训练损失: 0.017724360691703807
模型保存成功!

8.6 模型推理(predict.py)

import torch
from config import *
from model import ReviewAnalysisModel
from transformers import AutoTokenizer
 
# 核心预测逻辑函数,返回一批数据的预测概率
def predict_batch(model, inputs):
    model.eval() # 设置为评估模式(禁用Dropout)
    # 前向传播
    with torch.no_grad():  # 禁用梯度计算,节省显存
        outputs = model(**inputs)   # 形状(batch_size,)
    # 转换为预测概率
    batch_results = torch.sigmoid(outputs)
    return batch_results.tolist() # 转换成列表返回
 
def predict(text, model, tokenizer, device):
    # 1. 准备数据:文本处理
    inputs = tokenizer(
        text,
        padding='max_length',
        truncation=True,
        max_length=SEQ_LEN,
        return_tensors='pt',
    )
    inputs = {k: v.to(device) for k, v in inputs.items()}
 
    # 2. 预测
    # 前向传播,得到预测概率
    result = predict_batch(model, inputs)
 
    return result[0]    # 只有唯一的一个数据
 
def run_predict():
    # 1. 确定设备
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
    # 2. 获取词表,注:不能科学上网记得用国内镜像
    tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
 
    # 3. 加载模型
    model = ReviewAnalysisModel().to(device)
    model.load_state_dict( torch.load( MODEL_DIR/BEST_MODEL ) )
    print("模型加载成功!")
 
    # 6. 程序运行流程
    print("欢迎使用文本情感分析模型!输入q或者quit退出...")
    while True: # 核心:一个死循环
        # 捕获用户输入
        user_input = input("> ")
        # 判断:如果是q或者quit,直接退出
        if user_input.strip() in ['q', 'quit']:
            print("欢迎下次再来!")
            break
        # 判断:如果是空白,提示信息后继续循环
        if user_input.strip() == '':
            print("请输入有效内容!")
            continue
 
        # 根据预测概率,判断是正向还是负向评价
        result = predict(user_input, model, tokenizer, device)
        if result > 0.5:
            print(f"正向评价 (置信度: {result})")
        else:
            print(f"负向评价 (置信度:{1 - result})")
 
if __name__ == '__main__':
    # text = "我们公司"
    # top5_tokens = predict(text)
    # print(top5_tokens)
    run_predict()

推理优化:torch.no_grad()上下文管理器是关键优化点,它在推理时禁用梯度追踪,可显著降低内存占用和计算开销。model.eval()与model.train()的区别在于:前者禁用Dropout和BatchNorm的运行时统计更新,确保每次推理结果一致。

8.7 模型评估(evaluate.py)

import torch
from tqdm import tqdm
from config import *
from model import ReviewAnalysisModel
from dataset import get_dataloader
from predict import predict_batch    # 预测核心逻辑,得到批数据预测概率
 
# 验证核心逻辑,返回评价指标(准确率)
def evaluate(model, dataloader, device):
    correct_count = 0
    total_count = 0
 
    with torch.no_grad():
        # 按批次前向传播
        for batch in tqdm(dataloader, desc='评估:'):
            labels = batch.pop('labels').tolist()
            inputs = {k: v.to(device) for k, v in batch.items()}
 
            # 前向传播,得到预测概率
            batch_results = predict_batch(model, inputs)   # 形状(batch_size,)
            # 做拉链,对比每条数据的预测概率和目标分类
            for target, result in zip(labels, batch_results):
                total_count += 1
                # 判断预测概率是否大于0.5,转成预测分类标签
                result = 1 if result > 0.5 else 0
                if result == target:
                    correct_count += 1
 
    return correct_count / total_count
 
# 评估主流程
def run_evaluate():
    # 1. 确定设备
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
    # 3. 加载模型
    model = ReviewAnalysisModel().to(device)
    model.load_state_dict( torch.load( MODEL_DIR/BEST_MODEL ) )
    print("模型加载成功!")
 
    # 4. 获取测试数据集(加载器)
    test_dataloader = get_dataloader(train=False)
 
    # 5. 调用评估逻辑
    acc = evaluate(model, test_dataloader, device)
 
    print("评估结果:")
    print("准确率: ", acc)
 
if __name__ == '__main__':
    run_evaluate()

打印结果:

模型加载成功!
评估:: 100%|██████████| 197/197 [00:14<00:00, 13.29it/s]
评估结果:
准确率:  0.9518916766228595

九、总结与展望

9.1 核心收获

  1. Transformer是当之无愧的基石

    。自注意力机制带来的并行计算能力和长距离依赖建模能力,使得大规模预训练成为可能。

  2. 三大流派各有千秋

  3. Decoder-only(GPT系列)

     :天生适合生成任务,经过规模扩展后涌现出令人惊叹的推理和代码生成能力

  4. Encoder-only(BERT系列)

     :双向上下文理解能力使其在理解类任务上表现卓越

  5. Encoder-Decoder(T5系列)

     :统一框架的设计哲学极具前瞻性,为多任务统一建模提供了范本

  6. “预训练+微调”已成为NLP的标准范式

    。预训练阶段学习通用语言知识,微调阶段只需少量标注数据即可适配具体任务,显著降低了AI应用的门槛。

  7. HuggingFace生态系统大大降低了开发门槛

    。从几行代码加载预训练模型,到完整的微调流程,再到生产级部署,HuggingFace提供了全链路工具支持。

9.2 当前挑战与未来方向

  1. 预训练的瓶颈

    :GPT-5的争议暴露了一个深层次问题——单纯靠堆参数、堆数据的“Scaling Law”似乎遇到了天花板。OpenAI在GPT-4o后约两年半未完成下一代前沿模型的完整预训练。这是否意味着预训练范式本身需要革新?

  2. 推理效率的追求

    :大模型在实际部署中的推理成本仍然高昂。从知识蒸馏(DistilBERT)到量化压缩,再到MoE(混合专家)架构,效率优化是持续的研究热点。

  3. 多模态融合

    :GPT-4o已支持文本、图像、音频的实时交互。未来的预训练模型将不再是单一模态的语言模型,而是真正的“多模态通用智能体”。

  4. Agent与工具使用

    :从“模型”到“Agent”的转变正在发生。模型不再只是被动的文本生成器,而是能够主动调用工具、规划步骤、执行任务的智能体。

  5. 开源生态的崛起

    :LLaMA等开源模型打破了闭源壁垒,让中小机构和研究者也能基于开源大模型进行二次开发。开源与闭源模型的竞争正在重塑产业格局。

9.3 给初学者的建议

如果你刚刚接触预训练模型,我建议按以下路径循序渐进:

  1. 先理解Transformer架构

    。这是所有预训练模型的基础,搞懂自注意力机制、位置编码、多头注意力等核心概念,后面的学习会事半功倍。

  2. 从HuggingFace pipeline开始

    。一行代码体验情感分析、文本生成等任务,建立直观认知。

  3. 动手跑一个完整的微调项目

    。就像本文的案例实操,从数据预处理到训练再到推理评估,完整走一遍流程。

  4. 阅读经典论文

    。BERT、GPT-1、T5的原始论文是必读材料,它们清晰地阐述了模型的设计动机和技术细节。

  5. 关注模型演化而非盲目追新

    。理解RoBERTa为何在BERT基础上改进、ALBERT为何引入参数共享,比知道最新的模型名称更有价值。

预训练模型的故事远未结束,它在重塑自然语言处理的同时,也在深刻改变人机交互的方式。希望本文能帮你建立起对预训练模型的系统性认知,并在实践中找到自己的方向。