- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
思维导图如下:

python
# 导入PyTorch深度学习框架,用于构建和训练神经网络模型
import torch
# 导入PyTorch的神经网络模块,包含各种神经网络层和损失函数
import torch.nn as nn
# 导入PyTorch的计算机视觉库,包含预训练模型和数据处理工具
import torchvision
# 从torchvision中导入transforms模块,用于数据预处理和增强
# 导入datasets模块,用于加载标准数据集
from torchvision import transforms, datasets
# 导入操作系统相关模块,用于文件路径操作
import os
# 导入Python Imaging Library,用于图像处理
import PIL
# 导入pathlib模块,提供面向对象的文件系统路径操作
import pathlib
# 导入warnings模块,用于控制警告信息的显示
import warnings
# 忽略所有警告信息,使输出更干净(在生产环境中通常不推荐这样做)
warnings.filterwarnings("ignore")
# 检查CUDA是否可用,并设置设备:如果GPU可用则使用GPU,否则使用CPU
# torch.device创建一个设备对象,"cuda"表示GPU,"cpu"表示CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 打印当前使用的设备(GPU或CPU),用于确认计算设备
print(device)
# 导入pandas库,用于数据处理和分析,特别适合表格数据
import pandas as pd
# 从CSV文件加载自定义中文训练数据
# sep='\t' 指定分隔符为制表符(tab)
# header=None 表示CSV文件没有标题行
# 结果存储在train_data变量中,是一个pandas DataFrame对象
train_data = pd.read_csv('train.csv', sep='\t', header=None)
# 显示数据的前几行,用于快速检查数据格式和内容
print(train_data.head())
# 从torchtext导入get_tokenizer函数,用于获取文本分词器
from torchtext.data.utils import get_tokenizer
# 从torchtext导入build_vocab_from_iterator函数,用于从迭代器构建词汇表
from torchtext.vocab import build_vocab_from_iterator
# 导入jieba库,一个流行的中文分词工具
import jieba
# 设置中文分词方法为jieba.lcut
# jieba.lcut返回一个列表,包含分词后的所有词语
# 例如:jieba.lcut("我喜欢学习") 返回 ["我", "喜欢", "学习"]
tokenizer = jieba.lcut
# 定义一个生成器函数,用于从数据迭代器中yield分词后的文本
# 生成器函数使用yield关键字,可以逐个返回值而不一次性加载所有数据到内存
def yield_tokens(data_iter):
# 遍历数据迭代器中的每个样本
# 假设每个样本是一个元组(text, label),其中text是文本,label是标签
for text, _ in data_iter:
# 对文本进行分词,并yield分词结果
# 这将生成一个词语列表,供后续构建词汇表使用
yield tokenizer(text)
# 从yield_tokens生成器构建词汇表
# specials=["<unk>"] 指定特殊标记,<unk>表示未知词(out-of-vocabulary words)
# 注意:这里会报错,因为0.6版本的torchtext不支持specials参数
vocab = build_vocab_from_iterator(yield_tokens(train_iter), specials=["<unk>"])
# 设置默认索引,当词汇表中找不到某个词时,使用这个默认索引
# vocab["<unk>"] 获取<unk>标记的索引
# 这样,任何不在词汇表中的词都会被映射到<unk>标记
vocab.set_default_index(vocab["<unk>"])
# 测试词汇表:将给定的词语列表转换为对应的索引
# 这会返回一个列表,包含每个词语在词汇表中的索引
# 用于验证词汇表是否正常工作
print(vocab(['我','想','看','和平','精英','上','战神','必备','技巧','的','游戏','视频']))
# 定义文本处理管道:将原始文本转换为词汇表索引
# lambda x: vocab(tokenizer(x)) 是一个匿名函数
# 它首先使用tokenizer(x)对文本进行分词,然后使用vocab将分词结果转换为索引
text_pipeline = lambda x: vocab(tokenizer(x))
# 定义标签处理管道:将标签名称转换为索引
# label_name应该是一个包含所有类别名称的列表
# index(x)方法返回x在列表中的位置(索引)
label_pipeline = lambda x: label_name.index(x)
# 测试文本处理管道:将示例文本转换为词汇表索引
# 验证text_pipeline是否正常工作
print(text_pipeline('我想看和平精英上战神必备技巧的游戏视频'))
# 测试标签处理管道:将标签名称"Video-Play"转换为索引
# 验证label_pipeline是否正常工作
print(label_pipeline('Video-Play'))
# 从PyTorch导入DataLoader类,用于批量加载数据
from torch.utils.data import DataLoader
# 定义批处理函数,用于将多个样本组合成一个批次
# 这个函数会被DataLoader调用,用于处理每个批次的数据
def collate_batch(batch):
# 初始化标签列表、文本列表和偏移量列表
label_list, text_list, offsets = [], [], [0]
# 遍历批次中的每个样本
# 假设batch是一个列表,每个元素是(_text, _label)元组
for (_text, _label) in batch:
# 处理标签:将标签名称转换为索引,并添加到标签列表
label_list.append(label_pipeline(_label))
# 处理文本:将文本转换为词汇表索引,并转换为PyTorch张量
# dtype=torch.int64 指定数据类型为64位整数
processed_text = torch.tensor(text_pipeline(_text), dtype=torch.int64)
text_list.append(processed_text)
# 计算偏移量:记录每个样本的长度(词汇数量)
# 这用于在EmbeddingBag中正确索引每个样本
offsets.append(processed_text.size(0))
# 将标签列表转换为PyTorch张量
label_list = torch.tensor(label_list, dtype=torch.int64)
# 将所有文本张量连接成一个长张量
# 这样可以高效地存储变长序列
text_list = torch.cat(text_list)
# 计算累积偏移量:offsets[:-1]排除最后一个元素
# cumsum(dim=0)计算沿第0维度的累积和
# 这给出了每个样本在连接后的text_list中的起始位置
offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
# 将所有张量移动到指定设备(GPU或CPU)
return text_list.to(device), label_list.to(device), offsets.to(device)
# 创建数据加载器
# train_iter 应该是训练数据的迭代器
# batch_size=8 每个批次包含8个样本
# shuffle=False 不打乱数据顺序(通常训练时设为True,验证/测试时设为False)
# collate_fn=collate_batch 指定自定义的批处理函数
dataloader = DataLoader(train_iter,
batch_size=8,
shuffle=False,
collate_fn=collate_batch)
# 从PyTorch导入nn模块,用于定义神经网络
from torch import nn
# 定义文本分类模型类,继承自nn.Module
class TextClassificationModel(nn.Module):
# 初始化方法,定义模型结构
# vocab_size: 词汇表大小
# embed_dim: 词嵌入维度
# num_class: 分类类别数量
def __init__(self, vocab_size, embed_dim, num_class):
# 调用父类(nn.Module)的初始化方法
super(TextClassificationModel, self).__init__()
# 定义嵌入层:EmbeddingBag
# EmbeddingBag是Embedding的高效版本,特别适合处理变长序列
# vocab_size: 词汇表大小
# embed_dim: 词嵌入的维度
# sparse=False: 是否使用稀疏梯度(False表示使用密集梯度,通常训练更快)
self.embedding = nn.EmbeddingBag(vocab_size, # 词典大小
embed_dim, # 嵌入的维度
sparse=False) #
# 定义全连接层(线性层)
# 将嵌入维度映射到类别数量
self.fc = nn.Linear(embed_dim, num_class)
# 调用初始化权重方法
self.init_weights()
# 初始化权重方法,用于设置合理的初始权重值
def init_weights(self):
# 设置初始化范围
initrange = 0.5
# 为嵌入层权重设置均匀分布的初始值
# uniform_方法将权重初始化为在[-initrange, initrange]范围内的均匀分布
self.embedding.weight.data.uniform_(-initrange, initrange)
# 为全连接层权重设置均匀分布的初始值
self.fc.weight.data.uniform_(-initrange, initrange)
# 将全连接层的偏置初始化为0
self.fc.bias.data.zero_()
# 前向传播方法,定义数据如何通过模型
# text: 包含所有文本索引的张量
# offsets: 每个样本的起始位置偏移量
def forward(self, text, offsets):
# 通过嵌入层:得到词嵌入,并按样本聚合(默认是取平均)
# text包含所有样本的连接文本,offsets指定每个样本的起始位置
embedded = self.embedding(text, offsets)
# 通过全连接层:将嵌入向量映射到类别分数
return self.fc(embedded)
# 获取分类类别数量:label_name应该是一个包含所有类别名称的列表
num_class = len(label_name)
# 获取词汇表大小
vocab_size = len(vocab)
# 设置词嵌入维度为64
em_size = 64
# 创建模型实例,并移动到指定设备(GPU或CPU)
model = TextClassificationModel(vocab_size, em_size, num_class).to(device)
# 导入time模块,用于记录训练时间
import time
# 定义训练函数
def train(dataloader):
# 将模型设置为训练模式
# 这会影响某些层的行为(如Dropout、BatchNorm)
model.train()
# 初始化累加器:准确率、损失、样本计数
total_acc, train_loss, total_count = 0, 0, 0
# 设置日志间隔:每50个批次打印一次训练进度
log_interval = 50
# 记录开始时间
start_time = time.time()
# 遍历数据加载器中的每个批次
# enumerate提供索引idx和批次数据
for idx, (text, label, offsets) in enumerate(dataloader):
# 通过模型获取预测标签
predicted_label = model(text, offsets)
# 清零优化器的梯度
# PyTorch会在每次backward()后累积梯度,所以需要手动清零
optimizer.zero_grad()
# 计算损失:预测值与真实标签之间的差距
# criterion是之前定义的损失函数(CrossEntropyLoss)
loss = criterion(predicted_label, label)
# 反向传播:计算梯度
loss.backward()
# 梯度裁剪:防止梯度爆炸
# 将梯度范数限制在0.1以内
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1)
# 优化器更新模型参数
optimizer.step()
# 计算当前批次的准确率
# predicted_label.argmax(1) 获取每个样本预测概率最大的类别索引
# (predicted_label.argmax(1) == label) 比较预测和真实标签,返回布尔张量
# .sum().item() 计算正确预测的数量
total_acc += (predicted_label.argmax(1) == label).sum().item()
# 累加损失值
train_loss += loss.item()
# 累加样本数量
total_count += label.size(0)
# 每log_interval个批次打印一次训练进度
if idx % log_interval == 0 and idx > 0:
# 计算经过的时间
elapsed = time.time() - start_time
# 打印训练进度
print('| epoch {:1d} | {:4d}/{:4d} batches '
'| train_acc {:4.3f} train_loss {:4.5f}'.format(epoch, idx, len(dataloader),
total_acc / total_count, train_loss / total_count))
# 重置累加器
total_acc, train_loss, total_count = 0, 0, 0
# 重置开始时间
start_time = time.time()
# 定义评估函数(用于验证/测试)
def evaluate(dataloader):
# 将模型设置为评估模式
# 这会影响某些层的行为(如Dropout会失效,BatchNorm使用全局统计量)
model.eval()
# 初始化累加器
total_acc, train_loss, total_count = 0, 0, 0
# 不计算梯度,节省内存和计算资源
with torch.no_grad():
# 遍历数据加载器中的每个批次
for idx, (text, label, offsets) in enumerate(dataloader):
# 通过模型获取预测
predicted_label = model(text, offsets)
# 计算损失
loss = criterion(predicted_label, label)
# 累加准确率
total_acc += (predicted_label.argmax(1) == label).sum().item()
# 累加损失
train_loss += loss.item()
# 累加样本数量
total_count += label.size(0)
# 返回平均准确率和平均损失
return total_acc / total_count, train_loss / total_count
# 从torch.utils.data.dataset导入random_split函数,用于随机分割数据集
from torch.utils.data.dataset import random_split
# 从torchtext.data.functional导入to_map_style_dataset函数
# 将迭代式数据集转换为映射式数据集,支持按索引访问
from torchtext.data.functional import to_map_style_dataset
# 设置超参数
EPOCHS = 10 # 训练轮数
LR = 5 # 学习率(相对较高,后续会使用学习率调度器调整)
BATCH_SIZE = 64 # 批次大小
# 定义损失函数:交叉熵损失,适用于多分类任务
criterion = torch.nn.CrossEntropyLoss()
# 定义优化器:随机梯度下降(SGD)
# model.parameters() 获取模型所有可训练参数
# lr=LR 设置学习率
optimizer = torch.optim.SGD(model.parameters(), lr=LR)
# 定义学习率调度器:每1个epoch后,学习率乘以gamma=0.1
# 这是一种学习率衰减策略,有助于模型收敛
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.1)
# 初始化最佳验证准确率
total_accu = None
# 构建数据集
# 假设coustom_data_iter是一个自定义函数,用于创建数据迭代器
# train_data[0].values[:] 获取第一列(文本)的所有值
# train_data[1].values[:] 获取第二列(标签)的所有值
train_iter = coustom_data_iter(train_data[0].values[:], train_data[1].values[:])
# 将迭代式数据集转换为映射式数据集
train_dataset = to_map_style_dataset(train_iter)
# 随机分割数据集:80%用于训练,20%用于验证
# [int(len(train_dataset) * 0.8), int(len(train_dataset) * 0.2)] 指定分割比例
split_train_, split_valid_ = random_split(train_dataset,
[int(len(train_dataset) * 0.8), int(len(train_dataset) * 0.2)])
# 创建训练数据加载器
train_dataloader = DataLoader(split_train_, batch_size=BATCH_SIZE,
shuffle=True, collate_fn=collate_batch)
# 创建验证数据加载器
valid_dataloader = DataLoader(split_valid_, batch_size=BATCH_SIZE,
shuffle=True, collate_fn=collate_batch)
# 开始训练循环
for epoch in range(1, EPOCHS + 1):
# 记录epoch开始时间
epoch_start_time = time.time()
# 训练模型
train(train_dataloader)
# 评估模型在验证集上的性能
val_acc, val_loss = evaluate(valid_dataloader)
# 获取当前学习率
lr = optimizer.state_dict()['param_groups'][0]['lr']
# 学习率调度:如果验证准确率没有提升,则降低学习率
if total_accu is not None and total_accu > val_acc:
scheduler.step()
else:
total_accu = val_acc
# 打印分隔线
print('-' * 69)
# 打印epoch总结
print('| epoch {:1d} | time: {:4.2f}s | '
'valid_acc {:4.3f} valid_loss {:4.3f} | lr {:4.6f}'.format(epoch,
time.time() - epoch_start_time,
val_acc, val_loss, lr))
# 打印分隔线
print('-' * 69)
# 在验证集上评估最终模型性能
test_acc, test_loss = evaluate(valid_dataloader)
# 打印最终模型准确率
print('模型准确率为:{:5.4f}'.format(test_acc))
# 定义预测函数,用于对新文本进行分类
def predict(text, text_pipeline):
# 不计算梯度
with torch.no_grad():
# 将文本转换为词汇表索引,并转换为张量
text = torch.tensor(text_pipeline(text))
# 通过模型获取预测
# torch.tensor([0]) 作为偏移量,因为只有一个样本
output = model(text, torch.tensor([0]))
# 获取预测类别索引
return output.argmax(1).item()
# 示例文本1(注释掉)
# ex_text_str = "随便播放一首专辑阁楼里的佛里的歌"
# 示例文本2:查询汽车票
ex_text_str = "还有双鸭山到淮阴的汽车票吗13号的"
# 将模型移动到CPU(可能为了预测时的兼容性)
model = model.to("cpu")
# 对示例文本进行预测
# predict(ex_text_str, text_pipeline) 获取预测类别索引
# label_name[...] 将索引转换为类别名称
print("该文本的类别是:%s" % label_name[predict(ex_text_str, text_pipeline)])