第N6周:中文文本分类-Pytorch实现

思维导图如下:

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)])
相关推荐
嵌入式-老费1 小时前
自己动手写深度学习框架(pytorch训练第一个网络)
人工智能·pytorch·深度学习
Keep_Trying_Go2 小时前
论文Leveraging Unlabeled Data for Crowd Counting by Learning to Rank算法详解
人工智能·pytorch·深度学习·算法·人群计数
小女孩真可爱3 小时前
大模型学习记录(二)------Transform文本分类
语言模型·分类·transformer
z***y8625 小时前
Java数据挖掘开发
java·开发语言·数据挖掘
龙腾AI白云6 小时前
具身智能-普通LLM智能体与具身智能:从语言理解到自主行动
深度学习·数据挖掘
拾零吖6 小时前
CS336 Lecture_03
人工智能·pytorch·深度学习
盼小辉丶6 小时前
视觉Transformer实战 | Token-to-Token Vision Transformer(T2T-ViT)详解与实现
pytorch·深度学习·计算机视觉·transformer
二川bro8 小时前
基于PyTorch的视觉检测2025:YOLO实战与优化
pytorch·yolo·视觉检测
Learn Beyond Limits10 小时前
Correlation vs Cosine vs Euclidean Distance|相关性vs余弦相似度vs欧氏距离
人工智能·python·神经网络·机器学习·ai·数据挖掘