商品分类项目(大模型工程师入门必读)

写给谁:想成为大模型开发工程师的零基础学习者
目标:读完这个文档,你能完全理解一个完整的深度学习 NLP 项目


目录


第一章 项目全景

1.1 这个项目到底干什么?

一句话:让电脑自动判断商品属于什么类别。

举个例子,你给电脑输入:

输入:好奇心钻装纸尿裤L40片9-14kg
输出:母婴

电脑就能告诉你这个商品属于"母婴"类别。

这在工业界叫什么?文本分类(Text Classification),是 NLP(自然语言处理)最基础、最经典的任务。

为什么这对大模型工程师重要? 因为文本分类是所有 NLP 任务的入门砖。你学会了这个,后面学命名实体识别、文本生成、对话系统等任务都会更容易。

1.2 项目文件结构

product_classification/
│
├── data/                          ← 数据
│   ├── raw/                       ← 原始数据(人能看懂的文字)
│   │   ├── train.txt              ← 训练数据(给AI学习用的题目)
│   │   ├── valid.txt              ← 验证数据(学习过程中的模拟考)
│   │   └── test.txt               ← 测试数据(最终的高考)
│   └── processed/                 ← 处理后的数据(AI能看懂的数字)
│
├── pretrained/                    ← 预训练的BERT模型(已经学好中文的AI大脑)
│   ├── config.json                ← 模型结构配置(大脑有多少神经元)
│   ├── model.safetensors          ← 模型权重文件(大脑的记忆,392MB)
│   ├── tokenizer.json             ← 分词器(把文字切成词的工具)
│   ├── tokenizer_config.json      ← 分词器配置
│   └── vocab.txt                  ← 词表(AI认识的所有字,共21128个)
│
├── checkpoints/                   ← 训练好的模型(学完后的AI)
│   ├── best/                      ← 最佳模型
│   ├── last/                      ← 最新检查点(训练中断后可以继续)
│   │   └── checkpoint.pt
│   └── labels.txt                 ← 类别标签列表(30个类别)
│
├── logs/                          ← TensorBoard训练日志
│
├── src/                           ← 源代码(你的学习重点!)
│   ├── main.py                    ← 程序入口(28行)
│   ├── configuration/
│   │   ├── __init__.py            ← 空文件(标记这是Python包)
│   │   └── config.py              ← 配置文件(26行)
│   ├── process/
│   │   ├── __init__.py            ← 空文件
│   │   ├── preprocess.py          ← 数据预处理(43行)
│   │   └── dataset.py             ← 数据加载(43行)
│   ├── runner/
│   │   ├── __init__.py            ← 空文件
│   │   ├── train.py               ← 训练(273行,最核心!)
│   │   ├── predict.py             ← 预测(62行)
│   │   └── evaluate.py            ← 评估(51行)
│   └── web/
│       ├── __init__.py            ← 空文件
│       ├── app.py                 ← Web应用(29行)
│       ├── schema.py              ← 数据模型(4行)
│       └── service.py             ← 服务层(7行)
│
└── test/
    └── fast_api_test.py           ← FastAPI学习示例(可忽略)

1.3 数据长什么样?

data/raw/train.txt 的内容(TSV格式,Tab键分隔):

label       text_a
母婴        好奇心钻装纸尿裤L40片9-14kg
蔬菜        基地玉米.
酒饮冲调    240ML*15养元2430六个核桃
玩具        76-022斯伯丁篮球
香烟        红塔山软13mg经典195
  • label:商品类别(这是AI要学习的"答案")
  • text_a:商品标题(这是AI要看的"题目")
  • 共 30 个类别:3C数码、个护、书籍、乳品、休闲食品、健康、健康食品、办公、宠物、家居、家电、服饰内衣、母婴、水产、水果、汽车用品、清洁、玩具、礼品、粮油速食、美妆、肉禽蛋、蔬菜、运动、酒饮冲调、钟表配饰、鞋靴箱包、餐饮、香烟、鲜花绿植

1.4 项目运行命令

# 在项目根目录下执行(以 src 为工作目录)

# 第1步:数据预处理(把文字变成数字)
python src/main.py preprocess

# 第2步:训练模型(教AI学习)
python src/main.py train

# 第3步:评估模型(给AI考试打分)
python src/main.py evaluate

# 第4步:预测(让AI做题)
python src/main.py predict

# 第5步:启动Web服务(给别人用)
python src/main.py run_app

第二章 核心概念预备知识

在看代码之前,你必须先理解这些概念。我用最通俗的语言解释。

2.1 什么是深度学习?

传统编程:程序员写规则 → 电脑按规则执行
深度学习:程序员给数据 → 电脑自己学规则

类比:

  • 传统编程 = 你告诉小孩"有四条腿、会汪汪叫的是狗"
  • 深度学习 = 你给小孩看1000张狗的照片和1000张猫的照片,他自己学会分辨

2.2 什么是 BERT?

BERT = 一个已经学过中文的AI大脑(预训练模型)

它的"简历":

  • 全名:Bidirectional Encoder Representations from Transformers(基于Transformer的双向编码器表示)
  • 发明者:Google,2018年
  • 训练数据:大量中文文本(维基百科、新闻、书籍等)
  • 能力:理解中文的词义、语法、上下文

本项目的BERT配置(来自 pretrained/config.json):

配置项 通俗解释
hidden_size 768 神经元的"宽度",每个词用768个数字来表示
num_hidden_layers 12 有12层,越深理解能力越强
num_attention_heads 12 12个"注意力头",可以同时关注不同方面
vocab_size 21128 认识21128个字和词
max_position_embeddings 512 最多处理512个字

2.3 什么是微调(Fine-tuning)?

预训练(Pre-training):在大量无标注文本上学习语言(已经做好了,模型在pretrained/里)
    ↓
微调(Fine-tuning):在特定任务数据上调整参数(我们要做的)
    ↓
得到专用模型:能做商品分类的AI

类比:

  • 预训练 = 一个人上了大学,学了语文、数学、英语
  • 微调 = 他毕业后去公司上班,学习具体业务

2.4 什么是分词(Tokenization)?

原始文本:"好奇心钻装纸尿裤"
    ↓ 分词器
词元序列:["好", "奇", "心", "钻", "装", "纸", "尿", "裤"]
    ↓ 查词表
数字序列:[1962, 1906, 2552, 7032, 6814, 4989, 2399, 6841]
    ↓ 加特殊标记
最终输入:[101, 1962, 1906, 2552, 7032, 6814, 4989, 2399, 6841, 102]
          [CLS]                                              [SEP]
  • 101 = [CLS]标记,BERT用它来表示"整个句子的意思"
  • 102 = [SEP]标记,表示句子结束

为什么要分词? 因为AI不认字,只认数字。分词器就是"翻译官"。

2.5 训练的核心循环

┌─────────────────────────────────────────────────────────┐
│                    一个训练步骤                           │
│                                                         │
│  输入数据 → 前向传播 → 得到预测 → 计算损失                    │
│                                    ↓                    │
│  更新参数 ← 优化器更新 ← 反向传播 ← 计算梯度                  │
│      ↓                                                  │
│  重复以上过程,直到损失足够小                                │
└─────────────────────────────────────────────────────────┘

通俗解释:

  1. 前向传播:把题目给AI做,得到答案
  2. 计算损失:把AI的答案和正确答案比较,算出错了多少
  3. 反向传播:告诉AI哪里错了,错得有多离谱(梯度)
  4. 更新参数:AI根据反馈调整自己的"思维方式"(参数)

2.6 关键术语表

术语 通俗解释 为什么重要
Epoch 把所有数据从头到尾看一遍 控制训练轮数
Batch 一次看几条数据 内存有限,不能一次全看
Batch Size 一个batch有多少条数据 影响训练速度和效果
Learning Rate 每次调整参数的幅度 太大震荡不收敛,太小学得慢
Loss AI答错的程度 越小越好,训练的目标就是让loss变小
Gradient 损失对参数的导数 告诉参数该往哪个方向调
Optimizer 优化器(如Adam) 决定怎么根据梯度更新参数
Backward 反向传播 计算梯度的过程
Forward 前向传播 从输入得到输出的过程
State_dict 模型参数字典 保存/加载模型用的
Checkpoint 检查点/存档 训练中断后可以从这里继续

第三章 逐行代码详解 - main.py

文件src/main.py(28行)
作用:程序的入口,根据命令行参数决定执行哪个功能。

# from sys import argv
  • # 开头的是注释,Python不会执行它
  • 这行被注释掉了,说明作者之前用 sys.argv 来获取命令行参数,后来换成了 argparse
  • sys.argv 是一种简单但不优雅的方式(需要手动处理错误)
  • 拓展:在实际工作中,注释掉但不删除的代码很常见,方便以后回溯
from argparse import ArgumentParser
  • from ... import ...:从某个模块导入某个东西
  • argparse:Python标准库,专门用来解析命令行参数
  • ArgumentParser:参数解析器类
  • 标准库 = Python自带的库,不需要 pip install
  • 拓展argparse 会自动生成 --help 帮助信息,比 sys.argv 专业得多
if __name__ == '__main__':
  • 这是Python的"入口守卫"
  • __name__ 是Python的一个内置变量:
    • 直接运行这个文件时,__name__ 的值是 '__main__'
    • 被别人 import 时,__name__ 的值是文件名(如 'main'
  • 为什么要写这个? 防止被导入时意外执行代码
  • 拓展:每个Python项目的入口文件都应该有这行,这是Python编程的基本规范
    # 获取命令行传入的参数,作为脚本名称
    # arg = argv[1]
  • 这两行是注释,记录了之前的实现方式
  • argv[1] 表示取命令行的第2个参数(第1个是脚本名本身)
    parser = ArgumentParser(usage='usage: main.py action')
  • 创建一个参数解析器对象
  • usage='usage: main.py action':用户输入 --help 时显示的用法提示
  • 缩进:Python用缩进来表示代码块,这里缩进4个空格,表示在 if 语句内部
    parser.add_argument('action', choices=['preprocess', 'train', 'predict', 'evaluate', 'run_app'])
  • 添加一个名为 action位置参数(必须填的参数)

  • choices=[...]:限制只能选这几个值之一

  • 如果用户输入 python main.py hello,会报错:

    error: argument action: invalid choice: 'hello'
    
  • 拓展:位置参数 vs 可选参数

    • 位置参数:python main.py train(必须填)
    • 可选参数:python main.py --epochs 100(可以不填,有默认值)
    # 解析出当前要执行的操作
    action = parser.parse_args().action
  • parser.parse_args():解析命令行输入,返回一个 Namespace 对象
  • .action:取出其中 action 属性的值
  • 例如:输入 python main.py train,那么 action = 'train'
  • 链式调用parser.parse_args().action 是两步操作写在一起
    match action:
  • Python 3.10 新增的模式匹配语法
  • 类似其他语言的 switch-case
  • 根据 action 的值,跳转到对应的 case 分支
  • 拓展:如果你的Python版本低于3.10,可以用 if-elif-else 代替
        case 'preprocess':
            from process.preprocess import preprocess
            preprocess()
  • action == 'preprocess' 时执行
  • 延迟导入(lazy import):只在需要时才 import
    • 好处1:启动快(不加载不需要的模块)
    • 好处2:省内存
    • 好处3:避免不必要的依赖冲突
  • from process.preprocess import preprocess
    • process:文件夹名(Python包)
    • preprocess:文件名(.py 后缀省略)
    • preprocess:函数名
  • preprocess():调用这个函数
        case 'train':
            from runner.train import train
            train()
        case 'predict':
            from runner.predict import predict
            predict()
        case 'evaluate':
            from runner.evaluate import evaluate
            evaluate()
        case 'run_app':
            from web.app import run_app
            run_app()
  • 其余四个分支,结构完全一样
  • 每个分支:导入对应模块 → 调用对应函数
  • 注意case 后面的缩进是8个空格(两层缩进)

拓展知识 - Python 包(Package)

process/              ← 这是一个Python包(因为有__init__.py)
├── __init__.py       ← 标记这是包(可以为空)
├── preprocess.py     ← 包里的一个模块
└── dataset.py        ← 包里的另一个模块

第四章 逐行代码详解 - config.py

文件src/configuration/config.py(26行)
作用:集中管理所有配置常量。好处是修改配置时只需要改这一个文件。

from pathlib import Path
  • 导入 Path 类,用于处理文件路径

  • pathlib 是 Python 3 推荐的路径处理库

  • Path vs 字符串路径

    # 字符串方式(容易出错,Windows用\,Linux用/)
    path = "F:\\PycharmProjects\\data\\train.txt"
    
    # Path方式(自动处理,推荐)
    path = Path("F:/PycharmProjects") / "data" / "train.txt"
    
  • / 运算符:Path对象重载了 / 运算符,用来拼接路径,非常直观

# 1. 目录
ROOT_DIR = Path(__file__).parent.parent.parent
  • __file__:当前文件的绝对路径
    • 例如:F:\PycharmProjects\product_classification\src\configuration\config.py
  • .parent:获取父目录(上一级文件夹)
    • 第1个 .parent...\src\configuration\
    • 第2个 .parent...\src\
    • 第3个 .parent...\product_classification\(项目根目录)
  • 所以 ROOT_DIR = 项目根目录的绝对路径
  • 拓展:这种方式非常巧妙,不管项目放在哪个路径下,都能自动找到根目录
DATA_DIR = ROOT_DIR / 'data'
  • 数据目录:项目根目录/data/
  • ROOT_DIR / 'data' 等价于 Path.joinpath(ROOT_DIR, 'data')
RAW_DATA_DIR = DATA_DIR / 'raw'
  • 原始数据目录:项目根目录/data/raw/
PROCESSED_DATA_DIR = DATA_DIR / 'processed'
  • 处理后数据目录:项目根目录/data/processed/
LOGS_DIR = ROOT_DIR / 'logs'
  • 日志目录:项目根目录/logs/
MODELS_DIR = ROOT_DIR / 'checkpoints'
  • 模型保存目录:项目根目录/checkpoints/
# 2. 文件
RAW_TRAIN_DATA = 'train.txt'
RAW_TEST_DATA = 'test.txt'
RAW_VALID_DATA = 'valid.txt'
  • 定义原始数据文件名
  • 使用时:RAW_DATA_DIR / RAW_TRAIN_DATAdata/raw/train.txt
MODEL_NAME = str(ROOT_DIR / 'pretrained')
  • 预训练模型的路径(转为字符串)
  • Transformers库的 from_pretrained() 需要字符串类型的路径
  • str():把 Path 对象转换为字符串
LABELS_FILE = 'labels.txt'
  • 标签文件名,保存在 checkpoints/labels.txt
# 3. 超参数
BATCH_SIZE = 64
  • 每个batch有64条数据
  • 为什么是64? 经验值。太小(如8)训练不稳定,太大(如256)显存不够
  • 拓展:常见batch_size:16, 32, 64, 128, 256,通常是2的幂次
LEARNING_RATE = 1e-5
  • 学习率 = 0.00001
  • 为什么这么小? BERT微调时通常用小学习率(1e-5到5e-5),避免破坏预训练学到的知识
  • 科学计数法1e-5 = 1 × 10^(-5) = 0.00001
EPOCHS = 100
  • 最多训练100轮(配合早停机制,通常会提前结束)
  • 1个epoch = 把所有训练数据看一遍
# 模型保存的迭代次数
SAVE_STEPS = 100
  • 每训练100个batch,就做一次验证评估 + 保存检查点

拓展 - 超参数调优

超参数 常见范围 影响
BATCH_SIZE 16-256 越大训练越稳,但需要更多显存
LEARNING_RATE 1e-5 ~ 5e-4 越大学得快但可能不稳定
EPOCHS 10-100 配合早停,通常不需要太大

第五章 逐行代码详解 - preprocess.py

文件src/process/preprocess.py(43行)
作用:把人类能看懂的文字数据,转换成AI能看懂的数字数据。

from configuration.config import *
  • import *:导入 config.py 中定义的所有变量

  • 导入后可以直接使用 RAW_DATA_DIRBATCH_SIZEMODEL_NAME

  • 拓展import * 在大型项目中可能导致命名冲突,但在配置文件中使用很常见

  • 也可以这样写(更明确但更啰嗦):

    from configuration.config import RAW_DATA_DIR, RAW_TRAIN_DATA, RAW_VALID_DATA, RAW_TEST_DATA, BATCH_SIZE, MODEL_NAME, MODELS_DIR, LABELS_FILE, PROCESSED_DATA_DIR
    
from transformers import AutoTokenizer
  • AutoTokenizer:HuggingFace的自动分词器类
  • “Auto” = 自动选择,会根据模型名称自动判断用哪种分词器
  • 本项目用的是BERT的分词器(WordPiece)
  • 拓展:HuggingFace是AI领域最重要的开源社区,transformers库是做NLP的必备工具
from datasets import load_dataset, ClassLabel
  • load_dataset:HuggingFace的数据集加载函数
  • ClassLabel:用于把文字标签编码成数字的类
  • datasets库:HuggingFace专门用于数据处理的库,高效且易用
def preprocess():
  • 定义 preprocess 函数
  • 这个函数封装了整个预处理流程
    print('Preprocessing data...')
  • 打印提示信息,告诉用户程序在干什么
  • 拓展:在实际项目中,应该用 logging 模块代替 print
    # 1. 从文件加载多个数据集(字典)
    dataset_dict = load_dataset('csv', delimiter='\t', data_files={
        'train': str(RAW_DATA_DIR/RAW_TRAIN_DATA),
        'valid': str(RAW_DATA_DIR/RAW_VALID_DATA),
        'test': str(RAW_DATA_DIR/RAW_TEST_DATA)
    })
  • load_dataset():加载数据集

  • 'csv':指定数据格式(TSV是CSV的变种,分隔符不同)

  • delimiter='\t':指定分隔符为Tab键(\t是Tab的转义字符)

  • data_files={...}:指定文件路径的字典

    • key:数据集名称(‘train’, ‘valid’, ‘test’)
    • value:文件路径的字符串形式
  • str(...):Path对象转字符串(load_dataset需要字符串路径)

  • 返回值 dataset_dict 是一个 DatasetDict 对象,结构如下:

    DatasetDict({
        train: Dataset({features: ['label', 'text_a'], num_rows: 72855})
        valid: Dataset({features: ['label', 'text_a'], num_rows: 18214})
        test:  Dataset({features: ['label', 'text_a'], num_rows: 18214})
    })
    
    print(dataset_dict)
  • 打印数据集结构信息,方便调试
    # 2. 处理标签,进行类别编码
    # 2.1 提取所有的标签,保存成列表
    labels_list = sorted(set(dataset_dict['train']['label']))
  • dataset_dict['train']:获取训练集(Dataset对象)
  • ['label']:获取标签列,返回一个Python列表,如 ['母婴', '蔬菜', '母婴', ...]
  • set(...):转为集合,自动去重 → {'母婴', '蔬菜', '玩具', ...}
  • sorted(...):排序 → ['3C数码', '个护', '书籍', ...]
  • 为什么要排序? 保证每次运行结果一致。如果不排序,set的顺序是不确定的
  • 为什么要从训练集提取? 训练集应该包含所有类别
    print(labels_list)
  • 打印类别列表,确认有30个类别
    # 2.2 对标签进行编码转换
    dataset_dict = dataset_dict.cast_column('label', ClassLabel(names=labels_list))
  • cast_column('label', ClassLabel(...)):把’label’列从字符串转为数字
  • ClassLabel(names=labels_list):创建标签编码器,告诉它有哪些类别
  • 转换结果:'母婴' → 12'蔬菜' → 22,等等(按拼音/笔画排序后的索引)
  • 注意cast_column 返回新的 DatasetDict,不会修改原来的
    print(dataset_dict['train'][:3])
  • 打印前3条数据,查看编码后的结果
    # 2.3 将标签列表保存成文件
    with open(MODELS_DIR / LABELS_FILE, 'w', encoding='utf-8') as f:
        f.write('\n'.join(labels_list))
  • MODELS_DIR / LABELS_FILE:路径拼接 → checkpoints/labels.txt

  • open(..., 'w', encoding='utf-8'):以写入模式打开文件

    • 'w' = write(写入,会覆盖已有内容)
    • 'r' = read(只读)
    • 'a' = append(追加)
    • encoding='utf-8':指定编码,支持中文
  • with ... as f::上下文管理器,自动关闭文件,即使出错也会关闭

  • '\n'.join(labels_list):用换行符连接列表元素

    • 例如:['母婴', '蔬菜', '玩具']'母婴\n蔬菜\n玩具'
  • 写入后的文件内容

    3C数码
    个护
    书籍
    乳品
    ...
    
    # 3. 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
  • 从预训练模型路径加载分词器
  • MODEL_NAME 在config.py中定义为 str(ROOT_DIR / 'pretrained')
  • 分词器会读取 pretrained/vocab.txt,知道每个字对应什么编号
  • 拓展:分词器和模型是配套的,不能混用。BERT模型必须用BERT的分词器
    # 4. 处理标题文本,转换为模型输入
    def encode(batch):
  • 定义一个内部函数 encode,用于处理一批数据
  • batch 是一个字典:{'text_a': ['标题1', '标题2', ...], 'label': [0, 1, ...]}
        inputs = tokenizer(batch['text_a'], truncation=True)
  • 用分词器对文本进行编码
  • batch['text_a']:商品标题列表
  • truncation=True:如果文本超过512个token,自动截断
  • 返回的 inputs 是一个字典:
    • input_ids:每个字的编号,如 [[101, 1962, 1906, ...], [101, 1823, ...]]
    • attention_mask:注意力掩码,标记哪些是真实token(1),哪些是填充(0)
    • token_type_ids:句子类型ID(单句分类全为0)
        inputs['labels'] = batch['label']
  • 把标签(数字)也加入inputs字典
  • 为什么叫 ‘labels’? 因为 AutoModelForSequenceClassification 默认用这个key来获取标签
        return inputs
  • 返回处理后的数据
    dataset_dict = dataset_dict.map(encode, batched=True, batch_size=BATCH_SIZE, remove_columns=['label', 'text_a'])
  • map():对数据集的每一批数据应用 encode 函数
  • batched=True:批量处理(一次处理一批,比逐条快很多)
  • batch_size=BATCH_SIZE:每批64条
  • remove_columns=['label', 'text_a']:删除原始的文字列(已经不需要了)
  • 处理后的数据只包含:input_idsattention_masktoken_type_idslabels(全是数字)
    print(dataset_dict['train'][:3])
  • 打印前3条处理后的数据,确认转换正确
    # 5. 保存数据集
    dataset_dict.save_to_disk(PROCESSED_DATA_DIR)
  • 保存处理后的数据到 data/processed/ 目录
  • 使用Arrow格式(高效的列式存储格式)
  • 为什么要保存? 处理数据很耗时,保存后下次直接加载
if __name__ == '__main__':
    preprocess()
  • 入口守卫,允许单独运行此文件

第六章 逐行代码详解 - dataset.py

文件src/process/dataset.py(43行)
作用:提供数据集加载和DataLoader创建功能。

from datasets import load_from_disk
  • 从磁盘加载之前保存的数据集(preprocess.py保存的)
from transformers import AutoTokenizer, DataCollatorWithPadding
  • AutoTokenizer:分词器(虽然在这个文件中没有直接使用,但导入了)

  • DataCollatorWithPadding:数据整理器,负责填充(padding)

  • 为什么要填充? 一个batch里的文本长度不同,但模型需要固定长度的输入

    文本1: [好, 奇, 心]          → 长度3
    文本2: [基, 地, 玉, 米, 啊]  → 长度5
    填充后:
    文本1: [好, 奇, 心, 0, 0]   → 长度5(用0填充)
    文本2: [基, 地, 玉, 米, 啊]  → 长度5
    
from torch.utils.data import DataLoader
  • PyTorch的数据加载器
  • DataLoader的功能
    • 分批加载数据(batch)
    • 打乱顺序(shuffle)
    • 多进程加载(num_workers)
    • 调用collate_fn整理数据
from configuration.config import *
  • 导入所有配置
# 获取数据集
def get_dataset(dataset_type='train'):
  • 定义获取数据集的函数
  • dataset_type:数据集类型,默认为 ‘train’
    data_path = str(PROCESSED_DATA_DIR / dataset_type)
  • 拼接路径,如 data/processed/train
    dataset = load_from_disk(data_path)
  • 从磁盘加载Arrow格式的数据集
    return dataset
  • 返回 Dataset 对象
def get_dataloader(tokenizer, dataset_type='train'):
  • 定义创建DataLoader的函数
  • 需要传入tokenizer,因为DataCollatorWithPadding需要它来知道填充用什么token
    # 加载数据集
    # data_path = str(PROCESSED_DATA_DIR/dataset_type)
    # dataset = load_from_disk(data_path)
    dataset = get_dataset(dataset_type)
  • 注释掉的是之前的写法,现在改用 get_dataset 函数(代码复用)
    # 定义一个"对齐函数",做批量填充
    collate_fn = DataCollatorWithPadding(
        tokenizer,
        padding=True,
        return_tensors='pt'
    )
  • 创建数据整理器
  • tokenizer:分词器,用于获取填充token的ID(通常是0)
  • padding=True:启用填充,把短序列填充到batch中最长序列的长度
  • return_tensors='pt':返回PyTorch张量(‘pt’ = PyTorch)
    # 创建DataLoader
    dataloader = DataLoader(
        dataset=dataset,
        batch_size=BATCH_SIZE,
        shuffle=(True if dataset_type == 'train' else False),
        collate_fn=collate_fn
    )
  • 创建DataLoader
  • dataset:数据集
  • batch_size=BATCH_SIZE:每批64条
  • shuffle=(True if dataset_type == 'train' else False)
    • 训练集:True(打乱顺序,防止模型记住顺序)
    • 验证/测试集:False(不打乱,结果可复现)
  • collate_fn=collate_fn:数据整理函数
    return dataloader
  • 返回DataLoader对象
if __name__ == '__main__':
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    train_loader = get_dataloader(tokenizer, dataset_type='train')
  • 测试代码:加载分词器,创建训练集DataLoader
    for batch in train_loader:
        for k, v in batch.items():
            print(f"key {k} -> shape: {v.shape}")
        break
  • 遍历第一个batch,打印每个字段的形状

  • 输出示例:

    key input_ids -> shape: torch.Size([64, 32])
    key attention_mask -> shape: torch.Size([64, 32])
    key token_type_ids -> shape: torch.Size([64, 32])
    key labels -> shape: torch.Size([64])
    
  • [64, 32] = 64条数据,每条32个token

  • break:只看第一个batch就停

拓展 - DataLoader 的工作流程

原始数据集 (Dataset)
    ↓
DataLoader
    ├─ 1. 打乱顺序(shuffle)
    ├─ 2. 按batch_size分组
    ├─ 3. 调用collate_fn填充对齐
    └─ 4. 返回一个batch的张量

第七章 逐行代码详解 - train.py(核心)

文件src/runner/train.py(273行)
作用:这是整个项目最核心的文件!实现了完整的模型训练逻辑。

7.1 导入部分(第1-15行)

# import sys
# print(sys.path)
import time
  • 注释掉的 sys 导入和 print(sys.path) 是调试用的
  • import time:时间模块,用于生成时间戳(日志目录名)
from dataclasses import dataclass
  • dataclass:装饰器,自动生成 __init____repr__ 等方法

  • 没有dataclass要这样写

    class TrainConfig:
        def __init__(self, epochs, batch_size, lr, ...):
            self.epochs = epochs
            self.batch_size = batch_size
            self.lr = lr
            # ... 很多重复代码
    
  • 有了dataclass只要这样写

    @dataclass
    class TrainConfig:
        epochs: int = 100
        batch_size: int = 64
        lr: float = 1e-5
        # ... 简洁多了!
    
import torch
  • PyTorch深度学习框架
  • 拓展:PyTorch vs TensorFlow
    • PyTorch:更Pythonic,动态图,学术界首选
    • TensorFlow:Google开发,静态图,工业部署多
    • 2024年后,PyTorch在学术界和工业界都占主导
from torch.utils.data import DataLoader
  • 数据加载器
from torch.utils.tensorboard import SummaryWriter
  • TensorBoard的写入器
  • TensorBoard:Google的训练可视化工具,可以画loss曲线、accuracy曲线等
  • 怎么用? 训练完后运行 tensorboard --logdir=logs,然后浏览器打开
from tqdm import tqdm
  • 进度条库
  • tqdm 源自阿拉伯语 “taqaddum”(进度)
  • 效果:[Train: Epoch-1]: 45%|████████████████ | 450/1000 [01:30<01:50]
from transformers import AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPadding
  • AutoTokenizer:分词器
  • AutoModelForSequenceClassification:自动序列分类模型(在BERT上加分类头)
  • DataCollatorWithPadding:数据整理器
from sklearn.metrics import accuracy_score, f1_score
  • accuracy_score:准确率 = 正确预测数 / 总数
  • f1_score:F1分数 = 精确率和召回率的调和平均
  • 拓展:为什么用F1而不是只用accuracy?
    • 如果数据不平衡(如90%是A类,10%是B类),全部预测A也有90%准确率,但没有意义
    • F1同时考虑精确率和召回率,更全面
from configuration.config import *
from process.dataset import get_dataset
  • 导入配置和数据集加载函数

7.2 TrainConfig 数据类(第17-39行)

# 提取所有训练相关的配置,定义成一个数据类
# class TrainConfig:
#     def __init__(self, epochs=EPOCHS, lr=LEARNING_RATE, batch_size=BATCH_SIZE, logs_dir=LOGS_DIR, output_dir=MODELS_DIR):
#         self.epochs = epochs
#         self.lr = lr
#         self.batch_size = batch_size
#         self.logs_dir = logs_dir
#         self.output_dir = output_dir
  • 注释掉的是之前的写法(手写 __init__),现在改用 @dataclass
@dataclass
class TrainConfig:
  • @dataclass:自动生成 __init__ 等方法
  • 定义训练配置类
    # 训练超参数
    epochs: int = EPOCHS          # 训练轮数,默认100
    batch_size: int = BATCH_SIZE  # 批大小,默认64
    lr: float = LEARNING_RATE     # 学习率,默认1e-5
    save_steps: int = SAVE_STEPS  # 每多少步保存,默认100
  • 每个属性都有类型注解(intfloat)和默认值
  • 默认值来自config.py
    # 目录路径
    logs_dir: str = LOGS_DIR       # 日志目录
    output_dir: str = MODELS_DIR   # 模型输出目录
    # 早停配置
    early_stop_metric: str = 'loss'     # 默认监控验证损失
    early_stop_patience: int = 5        # 连续5次没改进就停止
  • 早停:如果模型连续5次评估都没有进步,就停止训练
  • 为什么用loss而不是accuracy? loss是连续值,更敏感
    # AMP控制开关
    use_amp: bool = True
  • AMP(Automatic Mixed Precision):混合精度训练
  • 用float16代替float32计算,速度快、省内存
  • 拓展:float16 vs float32
    • float32:32位浮点数,精度高,但占用内存大
    • float16:16位浮点数,精度低一点,但占用内存小一半,计算速度快
    • AMP:关键操作用float32,其他用float16,兼顾精度和速度

7.3 Trainer 类 - 初始化(第42-67行)

# 面向对象的训练方式:包装一个训练器类
class Trainer:
  • 定义训练器类,封装训练逻辑
  • 面向对象的好处:把相关的方法和数据封装在一起,代码更清晰
    def __init__(self, model, train_dataset, valid_dataset, collate_fn, compute_metrics_fn, device, train_config=TrainConfig()):
  • 构造函数,创建Trainer对象时调用
  • 参数:
    • model:AI模型
    • train_dataset:训练数据集
    • valid_dataset:验证数据集
    • collate_fn:数据整理函数
    • compute_metrics_fn:计算评估指标的函数
    • device:计算设备(CPU或GPU)
    • train_config=TrainConfig():训练配置,默认使用TrainConfig的默认值
        self.train_config = train_config
  • 保存训练配置到实例属性
  • self:代表当前对象实例
        self.model = model.to(device)
  • 把模型移到指定设备
  • model.to(device):如果device是GPU,就把模型的所有参数移到GPU显存中
  • 为什么要移到GPU? GPU有几千个小核心,并行计算能力强,训练速度快10-100倍
        self.device = device
  • 保存设备信息
        self.train_dataset = train_dataset
        self.valid_dataset = valid_dataset
        self.collate_fn = collate_fn
        self.compute_metrics_fn = compute_metrics_fn
  • 保存其他参数
        # 内部定义属性:训练过程中用到的工具对象
        self.optimizer = torch.optim.Adam(self.model.parameters(), lr=self.train_config.lr)
  • 创建Adam优化器
  • self.model.parameters():获取模型所有可训练参数(权重和偏置)
  • lr=self.train_config.lr:学习率
  • Adam是什么? 最常用的优化算法,能自适应调整每个参数的学习率
  • 拓展:其他常见优化器
    • SGD:随机梯度下降,经典但需要手动调学习率
    • AdamW:Adam + 权重衰减,BERT微调推荐用这个
        self.writer = SummaryWriter(log_dir=LOGS_DIR / time.strftime("%Y-%m-%d_%H-%M-%S"))
  • 创建TensorBoard写入器
  • LOGS_DIR / time.strftime(...):日志目录带时间戳,如 logs/2026-06-05_14-30-00
  • time.strftime("%Y-%m-%d_%H-%M-%S"):格式化当前时间
        # AMP梯度缩放器
        self.scaler = torch.amp.GradScaler(device=device, enabled=self.train_config.use_amp)
  • 创建梯度缩放器(AMP专用)
  • 为什么需要缩放? float16的数值范围小,小梯度可能变成0(下溢)。缩放器把loss放大,计算完梯度后再缩小回来
        # 训练过程中用到的全局变量
        # self.min_loss = float('inf')
        self.step = 0
  • self.step:全局步数计数器
  • 注释掉的 min_loss 是之前的实现
        self.early_stop_best_score = -float('inf')  # 初始最佳评估得分
  • 初始化最佳得分为负无穷大
  • -float('inf'):负无穷大,任何实际得分都比它大
  • 为什么要用负无穷? 因为我们要找最大值(score越大越好),第一次比较时任何分数都会超过它
        self.early_stop_counter = 0     # 容忍度计数器
  • 记录连续多少次没有改进
        # 定义检查点保存路径
        self.best_model_path = Path(self.train_config.output_dir) / 'best'
  • 最优模型保存路径:checkpoints/best/
  • Path:来自config.py的import *(config.py导入了pathlib.Path)
        self.checkpoint_path = Path(self.train_config.output_dir) / 'last' / 'checkpoint.pt'
  • 检查点保存路径:checkpoints/last/checkpoint.pt
  • 最优模型 vs 检查点
    • best/:只保存效果最好的模型(用于预测和部署)
    • last/checkpoint.pt:保存完整的训练状态(用于断点续训)

7.4 Trainer 类 - 内部方法(第69-151行)

    # 定义内部方法:获取加载器
    def _get_loader(self, dataset):
  • _ 开头的方法是"内部方法"(约定俗成,不建议外部调用)
  • 参数 dataset:数据集对象
        # 创建DataLoader
        dataloader = DataLoader(
            dataset=dataset,
            batch_size=self.train_config.batch_size,
            shuffle=True,
            collate_fn=self.collate_fn,
        )
        return dataloader
  • 创建DataLoader,参数和dataset.py中类似
    # 定义内部方法:训练一个批次(一次迭代)
    def _train_one_step(self, inputs):
  • 这是训练的核心! 一个step = 训练一个batch
        self.model.train()
  • 设置模型为训练模式
  • 训练模式 vs 评估模式
    • model.train():Dropout层会随机丢弃神经元,BatchNorm用当前batch的统计量
    • model.eval():Dropout不丢弃,BatchNorm用全局统计量
  • Dropout:训练时随机关闭一些神经元,防止过拟合
        inputs = { k:v.to(self.device) for k,v in inputs.items() }
  • 字典推导式:把所有输入张量移到指定设备(GPU/CPU)

  • 等价于:

    new_inputs = {}
    for k, v in inputs.items():
        new_inputs[k] = v.to(self.device)
    inputs = new_inputs
    
        with torch.autocast(device_type=self.device.type, dtype=torch.float16, enabled=self.train_config.use_amp):
  • torch.autocast:自动混合精度上下文管理器
  • 在这个代码块内,PyTorch会自动把某些计算转为float16
  • device_type=self.device.type:设备类型(‘cuda’或’cpu’)
  • dtype=torch.float16:使用16位浮点数
  • enabled=self.train_config.use_amp:是否启用(可以通过配置关闭)
            # 前向传播,得到结果
            outputs = self.model(**inputs)
  • 前向传播:把输入数据送入模型,得到输出
  • **inputs:字典解包,等价于 self.model(input_ids=..., attention_mask=..., labels=...)
  • outputs 包含:
    • outputs.loss:损失值
    • outputs.logits:模型的原始输出(每个类别的得分)
            loss = outputs.loss
  • 获取损失值
  • 损失(Loss):衡量模型预测与真实标签的差距
  • 损失越大 = 错得越离谱
  • 损失越小 = 预测越准确
        # 反向传播
        self.scaler.scale(loss).backward()
  • self.scaler.scale(loss):把loss放大(AMP的技巧)
  • .backward():反向传播,计算所有参数的梯度
  • 梯度 = 损失对参数的导数,表示"参数往哪个方向调能减小损失"
        # 更新参数
        self.scaler.step(self.optimizer)
  • 优化器根据梯度更新参数
  • scaler会先取消缩放,再更新
        self.scaler.update()
  • 更新缩放因子(根据梯度是否溢出自动调整)
        # 梯度清零
        self.optimizer.zero_grad()
  • 必须清零! PyTorch默认会累加梯度
  • 如果不清零,梯度会越来越大,训练会不稳定
        return loss.item()
  • loss.item():把张量转为Python数字
  • 返回这个batch的损失值

7.5 Trainer 类 - 早停逻辑(第99-119行)

    # 定义内部方法:判断是否早停(含模型保存逻辑)
    def _should_early_stop(self, metrics):
  • 判断是否应该提前停止训练
        # 将指标转换为当前评估得分
        metric = metrics[self.train_config.early_stop_metric]
  • 获取监控的指标值(默认是loss)
        score = -metric if self.train_config.early_stop_metric == 'loss' else metric
  • 统一为"越大越好"的逻辑:
    • 如果监控loss(越小越好)→ 取负数
    • 如果监控accuracy(越大越好)→ 直接用
        # 判断模型保存逻辑
        if score > self.early_stop_best_score:
  • 如果当前得分超过历史最佳
            # 如果大于历史最佳得分,就保存最优模型
            self.early_stop_best_score = score
            self.early_stop_counter = 0
  • 更新最佳得分,重置计数器
            self.model.save_pretrained(self.best_model_path)
  • 保存最优模型到 checkpoints/best/
  • save_pretrained 会保存:模型权重、配置文件
            tqdm.write("保存最佳模型...")
  • 在进度条上方打印信息(不会破坏进度条显示)
            return False
  • 返回False = 不停止训练
        else:
            # 如果没有改进,不保存,并更新容忍计数器
            self.early_stop_counter += 1
            # 判断是否超过容忍度上限
            if self.early_stop_counter >= self.train_config.early_stop_patience:
                return True
            else:
                return False
  • 如果没有改进:计数器+1
  • 如果计数器 >= 5(容忍度):返回True(停止训练)
  • 否则:返回False(继续训练)

7.6 Trainer 类 - 检查点(第121-151行)

    # 定义内部方法:保存检查点
    def _save_checkpoint(self):
  • 保存完整的训练状态(用于断点续训)
        # 定义检查点:字典对象
        checkpoint = {
            'model_state_dict': self.model.state_dict(),
            'optimizer_state_dict': self.optimizer.state_dict(),
            'scaler_state_dict': self.scaler.state_dict(),
            'step': self.step,
            'early_stop_best_score': self.early_stop_best_score,
            'early_stop_counter': self.early_stop_counter,
        }
  • state_dict = 状态字典,保存了对象的所有内部状态
    • model_state_dict:模型的所有权重参数
    • optimizer_state_dict:优化器的状态(学习率、动量等)
    • scaler_state_dict:梯度缩放器的状态
  • 还保存了训练进度和早停状态
        # 保存到文件
        tqdm.write("保存检查点...")
        torch.save(checkpoint, self.checkpoint_path)
  • torch.save():把对象保存到文件
    # 定义内部方法:加载检查点
    def _load_checkpoint(self):
  • 加载检查点,实现断点续训
        # 查看检查点路径是否存在,如果存在就加载
        if self.checkpoint_path.exists():
  • .exists():检查文件是否存在
            tqdm.write("发现检查点,加载并继续训练...")
            # 从文件获取检查点对象(字典)
            checkpoint = torch.load(self.checkpoint_path)
  • torch.load():从文件加载对象
            # 分别恢复所有的状态
            self.model.load_state_dict(checkpoint['model_state_dict'])
            self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
            self.scaler.load_state_dict(checkpoint['scaler_state_dict'])
            self.step = checkpoint['step']
            self.early_stop_best_score = checkpoint['early_stop_best_score']
            self.early_stop_counter = checkpoint['early_stop_counter']
  • 恢复所有状态,从上次中断的地方继续训练
        else:
            tqdm.write("没有检测到检查点,从头开始训练...")
  • 没有检查点就从头开始

7.7 Trainer 类 - train方法(第153-188行)

    # 核心方法:训练
    def train(self):
  • 训练的主方法
        # 加载检查点
        self._load_checkpoint()
  • 先加载检查点(如果有)
        # 获取训练集加载器
        train_loader = self._get_loader(self.train_dataset)
  • 创建训练数据的DataLoader
        # 循环迭代
        for epoch in range(self.train_config.epochs):
  • 外层循环:遍历每个epoch(最多100轮)
            for inputs in tqdm(train_loader, desc=f"[Train: Epoch-{epoch + 1}]"):
  • 内层循环:遍历每个batch
  • tqdm(...):显示进度条
  • desc=f"[Train: Epoch-{epoch + 1}]":进度条描述文字
  • f-string:Python 3.6+ 的字符串格式化,{epoch + 1} 会被替换为实际值
                train_loss = self._train_one_step(inputs)
  • 训练一个batch,返回损失值
                self.step += 1
  • 全局步数+1
                # 如果迭代了100次,就记录loss、判断是否保存模型
                if self.step % self.train_config.save_steps == 0:
  • % 是取余运算符,step % 100 == 0 表示step是100的倍数
  • 即每100步执行一次
                    # 记录训练loss
                    tqdm.write(f'[Train: Epoch {epoch + 1} | Step {self.step} | Loss: {train_loss:.6f}]')
  • 打印训练信息
  • :.6f:保留6位小数
                    self.writer.add_scalar('loss', train_loss, self.step)
  • 记录到TensorBoard
  • 'loss':指标名称
  • train_loss:指标值
  • self.step:横坐标(步数)
                    # 传入验证集,得到评估指标
                    metrics = self.evaluate()
  • 在验证集上评估模型
                    metrics_str = ' | '.join([ f'{k}:{v:.4f}' for k, v in metrics.items() ])
  • 列表推导式:把metrics字典格式化为字符串列表
  • ' | '.join(...):用 | 连接
  • 结果类似:'loss:0.1234 | accuracy:0.9567 | f1:0.9556'
                    tqdm.write(f'[Evaluate]{metrics_str}')
  • 打印评估结果
                    # 早停和保存模型逻辑
                    if self._should_early_stop(metrics):
                        tqdm.write('早停!')
                        return
  • 如果应该早停,打印信息并退出train方法
                    # 定时保存模型(检查点)
                    self._save_checkpoint()
  • 保存检查点

7.8 Trainer 类 - evaluate方法(第190-219行)

    # 核心方法:验证评估,返回评价指标,字典形如{'loss': 1.253, 'accuracy': 0.95, 'f1': 0.92}
    def evaluate(self) -> dict:
  • 评估方法,返回类型是字典(-> dict 是类型注解)
        # 获取验证集加载器
        dataloader = self._get_loader(self.valid_dataset)
        self.model.eval()
  • 设置为评估模式(关闭Dropout等)
        # 记录总损失
        total_loss = 0
        # 记录所有数据的预测标签和真实标签
        all_preds = []
        all_labels = []
  • 初始化变量
        with torch.no_grad():
  • 禁用梯度计算,节省内存和计算
  • 为什么? 评估时不需要反向传播,不需要计算梯度
            # 按批次迭代,进行验证
            for inputs in tqdm(dataloader, desc='[Evaluation]'):
  • 遍历验证集
                inputs = { k:v.to(self.device) for k,v in inputs.items() }
  • 移到设备
                # 前向传播
                outputs = self.model(**inputs)
  • 前向传播
                # 获取并累加损失
                total_loss += outputs.loss.item()
  • 累加损失
                # 获取输出并预测结果
                preds = torch.argmax(outputs.logits, dim=-1)
  • outputs.logits:模型输出,形状 [batch_size, num_classes]
  • torch.argmax(dim=-1):在最后一个维度取最大值的索引
  • 例如:[[0.1, 0.8, 0.1], [0.7, 0.2, 0.1]][1, 0]
                all_preds.extend(preds.tolist())
  • preds.tolist():张量转列表
  • .extend():把列表元素追加到另一个列表
                # 获取真实标签
                labels = inputs['labels']
                all_labels.extend(labels.tolist())
  • 收集真实标签
        # 评估指标1:平均损失
        loss = total_loss / len(dataloader)
  • 计算平均损失(总损失 / batch数)
        # 评估指标2:调用外部传入的计算函数,得到指标的字典
        metrics = self.compute_metrics_fn(all_preds, all_labels)
  • 调用外部函数计算accuracy和f1
        return {'loss': loss, **metrics}
  • **metrics:字典解包
  • 结果:{'loss': 0.1234, 'accuracy': 0.9567, 'f1': 0.9556}

7.9 train函数(第221-273行)

def train():
  • 训练的主入口函数(不是类的方法,是独立的函数)
    print("Training...")
  • 打印提示
    # 1. 定义设备
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  • 检查是否有GPU
  • 有GPU → 'cuda',没有 → 'cpu'
  • torch.cuda.is_available():检查CUDA(NVIDIA GPU计算平台)是否可用
    # 2. 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
  • 加载分词器
    # 3. 加载分类标签,加入模型配置
    with open(MODELS_DIR/LABELS_FILE, 'r', encoding='utf-8') as f:
        labels = f.read().splitlines()
  • 读取标签文件
  • f.read():读取全部内容
  • .splitlines():按行分割成列表
    id2label = { id:label for id, label in enumerate(labels) }
  • 字典推导式 + enumerate
  • enumerate(labels):同时获取索引和值 → [(0, '3C数码'), (1, '个护'), ...]
  • 结果:{0: '3C数码', 1: '个护', 2: '书籍', ...}
    label2id = { label:id for id, label in enumerate(labels) }
  • 反向映射:{'3C数码': 0, '个护': 1, '书籍': 2, ...}
    # 4. 加载模型
    model = AutoModelForSequenceClassification.from_pretrained(
        MODEL_NAME,
        num_labels=len(labels),
        id2label=id2label,
        label2id=label2id,
    )
  • 加载预训练模型并添加分类头
  • MODEL_NAME:预训练模型路径
  • num_labels=len(labels):30个类别
  • id2labellabel2id:标签映射,保存到模型配置中
  • 内部原理
    1. 加载BERT模型(12层Transformer)
    2. [CLS] token的输出(768维向量)
    3. 加一个线性层:768 → 30(类别数)
    4. 输出30个类别的得分
    # 5. 数据集和相关函数
    train_dataset = get_dataset('train')
    valid_dataset = get_dataset('valid')
  • 加载训练集和验证集
    # 定义一个"对齐函数",做批量填充
    collate_fn = DataCollatorWithPadding(
        tokenizer,
        padding=True,
        return_tensors='pt'
    )
  • 创建数据整理器
    # 定义一个"计算评估指标函数",得到字典返回
    def compute_metrics_fn(predictions, labels) -> dict:
        accuracy = accuracy_score(labels, predictions)
        f1 = f1_score(labels, predictions, average='weighted')
        return {'accuracy': accuracy, 'f1': f1}
  • 定义计算指标的函数
  • average='weighted':按类别样本数加权平均(处理类别不平衡)
    # 7. 定义训练器
    trainer = Trainer(
        model=model,
        train_dataset=train_dataset,
        valid_dataset=valid_dataset,
        collate_fn=collate_fn,
        compute_metrics_fn=compute_metrics_fn,
        device=device
    )
  • 创建Trainer实例
  • 注意:没有传 train_config,使用默认值
    # 训练
    trainer.train()
  • 开始训练!
if __name__ == "__main__":
    train()
  • 入口守卫

第八章 逐行代码详解 - predict.py

文件src/runner/predict.py(62行)
作用:用训练好的模型进行预测。

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from configuration.config import *
  • 导入需要的库
# 实现自定义的预测器
class Predictor:
  • 定义预测器类
    # 初始化
    def __init__(self, model, tokenizer, device):
        self.model = model.to(device)
        self.tokenizer = tokenizer
        self.device = device
  • 初始化:保存模型、分词器、设备
    # 核心方法:推理预测,传入商品标题文本,得到分类结果
    def predict(self, text: str | list[str]):
  • 预测方法
  • text: str | list[str]:类型注解,表示可以是字符串或字符串列表
  • str | list[str] 是 Python 3.10+ 的写法,旧版本用 Union[str, List[str]]
        # 判断输入,统一成列表形式
        is_str = isinstance(text, str)
        if is_str:
            text = [text]
  • 如果输入是单个字符串,转为列表
  • 为什么要转? 统一处理,后面的代码只需要处理列表
  • isinstance(text, str):检查类型
        # 1. 用分词器,得到模型输入(字典)
        inputs = self.tokenizer(
            text,
            padding=True,
            truncation=True,
            return_tensors='pt',
        )
  • 对文本进行分词编码
  • padding=True:填充到相同长度
  • truncation=True:超长截断
  • return_tensors='pt':返回PyTorch张量
        # 2. 前向传播
        self.model.eval()
  • 设置为评估模式
        with torch.no_grad():
  • 禁用梯度计算
            inputs = {k: v.to(self.device) for k, v in inputs.items()}
            outputs = self.model(**inputs)
  • 移到设备,前向传播
        # 3. 根据预测输出,得到分类标签
        preds = torch.argmax(outputs.logits, dim=-1).tolist()
  • 取最大值索引 → 预测的类别编号
        results = [ self.model.config.id2label[pred] for pred in preds ]
  • 把编号转换为标签名称
  • self.model.config.id2label:模型配置中的编号→标签映射
        # 4. 如果是单条文本,转换为一个分类标签返回
        if is_str:
            return results[0]
        return results
  • 如果输入是单条字符串,返回单个结果;否则返回列表
# 预测主流程
def predict():
  • 预测的主入口函数
    # 1. 定义设备
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    # 2. 加载训练好的模型
    model = AutoModelForSequenceClassification.from_pretrained(MODELS_DIR / 'best')
  • 加载最优模型(checkpoints/best/)
    # 3. 创建分词器
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    # 4. 创建预测器
    predictor = Predictor(model, tokenizer, device)
    # 5. 传入测试数据,推理预测
    text = "好奇心钻装纸尿裤L40片9-14kg"
    result = predictor.predict(text)
    print(f"{text}: {result}")
  • 测试单条预测
    texts = ['瓦伦丁Wurenbacher小麦西柚啤酒500ml*12听整箱装德国原装进口果啤', '911-267遥控车', '640G正航牛奶早餐饼干']
    results = predictor.predict(texts)
    print(results)
  • 测试批量预测
if  __name__ == '__main__':
    predict()
  • 入口守卫

第九章 逐行代码详解 - evaluate.py

文件src/runner/evaluate.py(51行)
作用:在测试集上评估模型的最终效果。

import torch
from sklearn.metrics import accuracy_score, f1_score
from transformers import AutoTokenizer, AutoModelForSequenceClassification, DataCollatorWithPadding
from configuration.config import *
from process.dataset import get_dataset
from runner.train import Trainer
  • 导入需要的库
  • 注意:从 train.py 导入 Trainer 类(复用其evaluate方法)
def evaluate():
    print("Evaluating model...")
  • 评估主函数
    # 1. 定义设备
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    # 2. 加载分词器
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    # 3. 加载模型
    model = AutoModelForSequenceClassification.from_pretrained(MODELS_DIR/'best')
  • 加载最优模型
    # 4. 数据集和相关函数
    test_dataset = get_dataset('test')
  • 加载测试集(不是验证集!)
    # 定义一个"对齐函数",做批量填充
    collate_fn = DataCollatorWithPadding(
        tokenizer,
        padding=True,
        return_tensors='pt'
    )
    # 定义一个"计算评估指标函数",得到字典返回
    def compute_metrics_fn(predictions, labels) -> dict:
        accuracy = accuracy_score(labels, predictions)
        f1 = f1_score(labels, predictions, average='weighted')
        return {'accuracy': accuracy, 'f1': f1}
    # 5. 定义训练器
    trainer = Trainer(
        model=model,
        train_dataset=None,       # 评估时不需要训练集
        valid_dataset=test_dataset, # 把测试集当作验证集传入
        collate_fn=collate_fn,
        compute_metrics_fn=compute_metrics_fn,
        device=device
    )
  • 巧妙复用:把测试集传入 valid_dataset 参数,复用 Trainer 的 evaluate 方法
    # 评估
    metrics = trainer.evaluate()
    print("Evaluation finished.")
    print(f"评估结果:{metrics}")
  • 执行评估并打印结果
if __name__ == "__main__":
    evaluate()
  • 入口守卫

第十章 逐行代码详解 - Web服务三件套

10.1 schema.py(4行)

文件src/web/schema.py
作用:定义请求数据的格式(数据验证模型)。

from pydantic import BaseModel

class Product(BaseModel):
    title: str
  • pydantic:数据验证库,FastAPI内置支持
  • BaseModel:Pydantic的基类
  • Product 类:定义商品数据格式,只有一个字段 title(字符串)
  • 自动功能
    • 请求缺少 title → 返回422错误
    • title 不是字符串 → 返回422错误
    • 自动生成API文档(访问 /docs

10.2 service.py(7行)

文件src/web/service.py
作用:服务层,分离接口逻辑和业务逻辑。

class ProductService:
    # 初始化,传入一个预测器
    def __init__(self, predictor):
        self.predictor = predictor
    # 预测分类
    def predict_category(self, title):
        return self.predictor.predict(title)
  • 为什么要加这一层? 分层架构:

    接口层(app.py)    → 处理HTTP请求
    服务层(service.py) → 业务逻辑
    预测层(predict.py) → 模型推理
    
  • 各层职责单一,便于测试和维护

10.3 app.py(29行)

文件src/web/app.py
作用:创建FastAPI Web应用,提供HTTP接口。

import torch
import uvicorn
from fastapi import FastAPI
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from configuration.config import *
from runner.predict import Predictor
from web.schema import Product
from web.service import ProductService
  • uvicorn:ASGI服务器,运行FastAPI应用
  • FastAPI:现代Web框架,自动生成API文档
app = FastAPI()
  • 创建FastAPI应用实例
# 创建一个预测器对象
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AutoModelForSequenceClassification.from_pretrained(MODELS_DIR/'best')
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
  • 全局作用域加载模型(应用启动时只加载一次)
  • 为什么放在全局? 如果放在路由函数里,每次请求都要加载模型,非常慢
predictor = Predictor(model=model, tokenizer=tokenizer, device=device)
  • 创建预测器
# 创建服务层对象
service = ProductService(predictor=predictor)
  • 创建服务层
@app.post("/predict")
def predict(product: Product):
  • @app.post("/predict"):定义POST路由
  • product: Product:请求体,FastAPI自动解析JSON并验证
    category_label = service.predict_category(product.title)
    return {"商品名": product.title, "类别": category_label}
  • 调用服务层预测,返回结果
def run_app():
    uvicorn.run("web.app:app", host="0.0.0.0", port=8000)
  • 启动Web服务器
  • "web.app:app":模块路径:变量名
  • host="0.0.0.0":监听所有网络接口
  • port=8000:端口号

使用方式

# 启动服务
python src/main.py run_app

# 调用接口
curl -X POST "http://localhost:8000/predict" \
     -H "Content-Type: application/json" \
     -d '{"title": "好奇心钻装纸尿裤L40片9-14kg"}'

# 返回
# {"商品名":"好奇心钻装纸尿裤L40片9-14kg","类别":"母婴"}

第十一章 完整流程串讲与数据流图

11.1 数据流图

【第1步:预处理 preprocess.py】

train.txt (文字)          processed/train (数字)
┌─────────────┐         ┌─────────────────────┐
│ 母婴        │         │ input_ids: [101,...] │
│ 好奇心钻装  │ ──→     │ attention_mask: [1,] │
│ 纸尿裤      │ 分词编码 │ token_type_ids: [0,] │
│             │         │ labels: 12           │
└─────────────┘         └─────────────────────┘

【第2步:训练 train.py】

processed/train     DataLoader      Trainer
┌──────────┐       ┌──────────┐    ┌────────────────────────┐
│ 数字数据 │ ──→   │ 每次64条 │ ──→│ 前向传播 → loss        │
│          │       │ 打乱顺序 │    │ 反向传播 → 梯度        │
│          │       │ 填充对齐 │    │ 更新参数 → 更好的模型  │
└──────────┘       └──────────┘    └────────────────────────┘
                                            │
                                            ↓
                                   checkpoints/best/ (最优模型)

【第3步:预测 predict.py】

"好奇心钻装纸尿裤" → 分词器 → BERT模型 → argmax → "母婴"

【第4步:部署 app.py】

POST请求 → FastAPI → Predictor → 返回JSON

11.2 项目架构图

┌─────────────────────────────────────────────────────────┐
│                      main.py (入口)                      │
│         根据命令行参数,分发到不同功能                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  preprocess.py     train.py      evaluate.py            │
│  ┌──────────┐    ┌──────────┐   ┌──────────┐           │
│  │ 读取数据  │    │ 加载模型  │   │ 加载模型  │           │
│  │ 标签编码  │    │ 训练循环  │   │ 测试评估  │           │
│  │ 文本分词  │    │ 早停保存  │   │ 输出指标  │           │
│  └──────────┘    └──────────┘   └──────────┘           │
│       ↓               ↓              ↓                  │
│  ┌──────────────────────────────────────────┐           │
│  │          config.py (配置中心)             │           │
│  │   路径、超参数、文件名等所有配置            │           │
│  └──────────────────────────────────────────┘           │
│       ↓               ↓              ↓                  │
│  ┌──────────────────────────────────────────┐           │
│  │          dataset.py (数据加载)            │           │
│  │   get_dataset()  get_dataloader()         │           │
│  └──────────────────────────────────────────┘           │
│                                                         │
│  predict.py ←──── app.py ←──── service.py               │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐           │
│  │ Predictor│   │ FastAPI  │   │ 业务逻辑  │           │
│  │ 推理预测  │   │ HTTP接口  │   │ 调用预测  │           │
│  └──────────┘   └──────────┘   └──────────┘           │
│                                                         │
└─────────────────────────────────────────────────────────┘

第十二章 大模型工程师知识拓展

12.1 从BERT到GPT:预训练模型的演进

模型 年份 架构 特点 应用
BERT 2018 Encoder 双向理解 分类、NER、问答
GPT-2 2019 Decoder 单向生成 文本生成
T5 2019 Encoder-Decoder 统一框架 翻译、摘要
GPT-3 2020 Decoder 1750亿参数 Few-shot学习
ChatGPT 2022 Decoder RLHF对齐 对话、通用任务
GPT-4 2023 多模态 图文理解 通用AI

本项目用的是BERT(Encoder架构),适合分类任务。 如果你要做大模型开发,还需要学习:

  • Decoder架构(GPT系列)
  • RLHF(人类反馈强化学习)
  • Prompt Engineering(提示工程)

12.2 BERT的预训练任务

BERT在预训练时做两个任务:

任务1:MLM(Masked Language Model)遮盖语言模型

输入:我 爱 [MASK] 国 的 天安门
输出:我 爱 中 国 的 天安门

随机遮盖15%的词,让模型预测被遮盖的词。

任务2:NSP(Next Sentence Prediction)下一句预测

句子A:我爱中国
句子B:天安门在北京    → IsNext=True

句子A:我爱中国
句子B:今天天气很好    → IsNext=False

判断两句话是否是上下句关系。

12.3 Transformer 架构简介

输入文本
    ↓
┌─────────────────────────────────────┐
│           Embedding 层               │
│  词向量 + 位置编码 + 段落编码          │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│        Transformer Block ×12         │
│  ┌───────────────────────────────┐  │
│  │  Multi-Head Self-Attention     │  │
│  │  (多头自注意力)               │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │  Feed-Forward Network          │  │
│  │  (前馈神经网络)               │  │
│  └───────────────────────────────┘  │
│  + Layer Norm + Residual Connection  │
└─────────────────────────────────────┘
    ↓
┌─────────────────────────────────────┐
│          分类头(线性层)              │
│        768维 → 30个类别               │
└─────────────────────────────────────┘
    ↓
输出:每个类别的概率

12.4 作为大模型工程师,你还需要学什么?

阶段 知识点 本项目涉及
基础 Python编程
基础 PyTorch
基础 NLP基础(分词、编码)
进阶 预训练模型(BERT)
进阶 微调技术
进阶 模型部署(FastAPI)
高级 Transformer原理 部分
高级 大模型训练(分布式)
高级 RLHF对齐
高级 Prompt Engineering
高级 RAG(检索增强生成)
高级 Agent(智能体)

12.5 实际面试中可能问到的问题

Q1:这个项目的BERT模型有多少参数?
A:约1.1亿(110M)。计算:vocab_size × hidden_size + num_layers × (4 × hidden_size²) ≈ 110M

Q2:为什么用F1而不是Accuracy?
A:数据可能不平衡。F1同时考虑精确率和召回率,更适合评估不平衡数据集。

Q3:什么是早停?为什么要用?
A:监控验证集指标,连续N次没有改进就停止。防止过拟合——模型在训练集上越来越好,但在新数据上变差。

Q4:如何优化这个模型?
A:

  1. 换更大的预训练模型(如RoBERTa-large)
  2. 数据增强(同义词替换、回译)
  3. 调参(学习率、batch_size)
  4. 冻结底层参数,只微调顶层
  5. 使用对抗训练(FGM、PGD)

第十三章 常见问题

Q1:__init__.py 是什么?为什么是空的?
A:告诉Python"这个文件夹是一个Python包"。可以为空,但必须存在。

Q2:为什么训练和预测时用 model.train()model.eval()
A:某些层在训练和评估时行为不同:

  • Dropout:训练时随机丢弃,评估时不丢弃
  • BatchNorm:训练时用当前batch的统计量,评估时用全局统计量

Q3:没有GPU能运行吗?
A:能!代码会自动检测并使用CPU。但训练会很慢(可能几小时到几天)。建议用Google Colab免费GPU。

Q4:loss 一直在下降,但accuracy没有提升?
A:正常!loss是连续值,accuracy是离散值。loss下降说明模型在学习,accuracy可能需要loss下降到一定程度才会跳变。

Q5:如何添加新的商品类别?
A:在原始数据中添加新类别样本 → 重新运行preprocess → 重新运行train。


附录

A. 安装依赖

pip install torch torchvision
pip install transformers datasets
pip install scikit-learn tensorboard
pip install fastapi uvicorn pydantic
pip install tqdm

B. 运行命令

cd F:\PycharmProjects\product_classification

# 数据预处理
python src/main.py preprocess

# 训练
python src/main.py train

# 评估
python src/main.py evaluate

# 预测
python src/main.py predict

# 启动Web服务
python src/main.py run_app

# 查看训练日志
tensorboard --logdir=logs

C. 实际运行结果

好奇心钻装纸尿裤L40片9-14kg  →  母婴       ✓
瓦伦丁小麦西柚啤酒500ml*12听  →  酒饮冲调   ✓
911-267遥控车                 →  玩具       ✓
640G正航牛奶早餐饼干          →  休闲食品   ✓

D. 代码统计

文件 行数 作用
main.py 28 程序入口
config.py 26 配置管理
preprocess.py 43 数据预处理
dataset.py 43 数据加载
train.py 273 模型训练(核心)
predict.py 62 模型预测
evaluate.py 51 模型评估
app.py 29 Web应用
schema.py 4 数据模型
service.py 7 服务层
总计 566
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐