- 了解新闻主题分类任务及相关数据。
- 掌握使用浅层网络构建新闻主题分类器的实现过程。
以新闻报道文本为输入,判断其所属主题(互斥类型),属于典型的文本分类问题。
本任务假设类别之间是互斥的,即每篇新闻只能属于一个主题。
AG_NEWS
是 Torchtext 内置的一个新闻分类数据集,包含 4 类新闻(世界、体育、商业、科技),广泛用于文本分类任务。
- 来源: AG News Corpus (来自新闻网站)
- 类别: 4 类 (
World
, Sports
, Business
, Sci/Tech
)
- 样本数: 训练集 120,000 条,测试集 7,600 条
- 字段:
label
: 类别编号 (1-4)
text
: 新闻文本
数据文件结构:
data/
├── ag_news_csv.tar.gz
└── ag_news_csv/
├── classes.txt # 标签含义:World、Sports、Business、Sci/Tech
├── readme.txt # 数据集英文说明
├── test.csv # 验证数据(7600条)
└── train.csv # 训练数据(12万条)
(1) 直接加载原始数据
下面是依赖的工具包安装命令:(由于存在兼容问题,所以指定版本)
pip install torch==2.0.1 torchtext==0.15.2 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu118
pip install portalocker>=2.0.0
portalocker
是一个用于文件锁定的 Python 包,torchtext
在加载数据集时需要它来确保数据文件的完整性。
import torch
from torchtext.datasets import AG_NEWS
import os
# 定义数据下载路径, 当前文件夹下的data文件夹
load_data_path = "./data"
if not os.path.isdir(load_data_path):
os.mkdir(load_data_path)
# 加载训练集和测试集
train_dataset, test_dataset = AG_NEWS(root=load_data_path, split=('train', 'test'))
# 查看前5条训练数据
for i, (label, text) in enumerate(train_dataset):
if i >= 5:
break
print(f"Label: {label}, Text: {text[:50]}...")
打印结果:
Label: 3, Text: Wall St. Bears Claw Back Into the Black (Reuters) ...
Label: 3, Text: Carlyle Looks Toward Commercial Aerospace (Reuters...
Label: 3, Text: Oil and Economy Cloud Stocks' Outlook (Reuters) Re...
Label: 3, Text: Iraq Halts Oil Exports from Main Southern Pipeline...
Label: 3, Text: Oil prices soar to all-time record, posing new men...
词表大小: 95811
(2) 文本数值化
# 将文本转换为词索引
text_pipeline = lambda x: vocab(tokenizer(x))
label_pipeline = lambda x: int(x) - 1 # 标签转为0-3
# 示例
text = "Apple launches new iPhone"
indices = text_pipeline(text)
print(f"文本: {text} → 词索引: {indices}")
文本: Apple launches new iPhone → 词索引: [295, 1003, 23, 0]
(3)封装为 DataLoader
from torch.utils.data import DataLoader
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
def collate_batch(batch):
label_list, text_list = [], []
for (_label, _text) in batch:
label_list.append(label_pipeline(_label))
processed_text = torch.tensor(text_pipeline(_text), dtype=torch.int64)
text_list.append(processed_text)
return torch.tensor(label_list, dtype=torch.int64).to(device), torch.stack(text_list).to(device)
# 创建 DataLoader
train_loader = DataLoader(train_data, batch_size=8, shuffle=True, collate_fn=collate_batch)
- 模型结构:
- Embedding层:将词汇索引映射为稠密向量
- 平均池化层:将变长文本转换为固定长度表示
- 全连接层:输出类别预测
import torch
import torch.nn as nn
import torch.nn.functional as F
# 配置参数
BATCH_SIZE = 16
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class TextSentiment(nn.Module):
"""文本分类模型"""
def __init__(self, vocab_size, embed_dim, num_class):
super().__init__()
# 嵌入层(sparse=True表示稀疏更新梯度)
self.embedding = nn.Embedding(vocab_size, embed_dim, sparse=True)
# 线性层
self.fc = nn.Linear(embed_dim, num_class)
# 初始化权重
self.init_weights()
def init_weights(self):
"""初始化权重(均匀分布)"""
initrange = 0.5
self.embedding.weight.data.uniform_(-initrange, initrange)
self.fc.weight.data.uniform_(-initrange, initrange)
self.fc.bias.data.zero_()
def forward(self, text):
"""前向传播过程"""
# 词嵌入
embedded = self.embedding(text)
# 调整维度以适配batch处理
c = embedded.size(0) // BATCH_SIZE
embedded = embedded[:BATCH_SIZE * c]
# 平均池化(将每个batch的词向量平均为一个向量)
embedded = embedded.transpose(1, 0).unsqueeze(0)
embedded = F.avg_pool1d(embedded, kernel_size=c)
# 输出分类结果
return self.fc(embedded[0].transpose(1, 0))
# 实例化模型
VOCAB_SIZE = len(train_dataset.get_vocab()) # 词汇总数
EMBED_DIM = 32 # 词嵌入维度
NUN_CLASS = len(train_dataset.get_labels()) # 类别总数
model = TextSentiment(VOCAB_SIZE, EMBED_DIM, NUN_CLASS).to(device)
- 实现
generate_batch
函数:
- 输入:batch大小的样本列表(每个样本是文本和标签的元组)
- 输出:拼接后的文本张量和标签张量
def generate_batch(batch):
"""将batch数据转换为样本张量和标签张量"""
# 提取标签并转换为张量
label = torch.tensor([entry[1] for entry in batch])
# 提取样本并拼接为张量
text = [entry[0] for entry in batch]
text = torch.cat(text)
return text, label
# 调用示例
batch = [(torch.tensor([3, 23, 2, 8]), 1), (torch.tensor([3, 45, 21, 6]), 0)]
print(generate_batch(batch))
# 输出:(tensor([ 3, 23, 2, 8, 3, 45, 21, 6]), tensor([1, 0]))
- 训练函数
train()
:
- 使用DataLoader加载数据
- 前向传播、计算损失、反向传播、参数更新
- 计算训练损失和准确率
- 验证函数
valid()
:
from torch.utils.data import DataLoader
def train(train_data):
"""模型训练函数"""
train_loss = 0
train_acc = 0
# 数据加载器(按batch处理)
data = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, collate_fn=generate_batch)
for i, (text, cls) in enumerate(data):
optimizer.zero_grad() # 梯度清零
output = model(text) # 模型输出
loss = criterion(output, cls) # 计算损失
train_loss += loss.item()
loss.backward() # 反向传播
optimizer.step() # 参数更新
train_acc += (output.argmax(1) == cls).sum().item() # 计算准确率
scheduler.step() # 调整学习率
# 返回平均损失和准确率
return train_loss / len(train_data), train_acc / len(train_data)
def valid(valid_data):
"""模型验证函数"""
loss = 0
acc = 0
data = DataLoader(valid_data, batch_size=BATCH_SIZE, collate_fn=generate_batch)
with torch.no_grad(): # 关闭梯度计算
for text, cls in data:
output = model(text)
loss += criterion(output, cls).item()
acc += (output.argmax(1) == cls).sum().item()
return loss / len(valid_data), acc / len(valid_data)
- 关键设置:
- 训练轮数:10
- 损失函数:交叉熵损失
- 优化器:SGD(初始学习率4.0)
- 学习率调度:StepLR(每步衰减0.9)
- 数据划分:
- 训练集:95%原始训练数据
- 验证集:5%原始训练数据
import time
from torch.utils.data.dataset import random_split
# 训练配置
N_EPOCHS = 10
min_valid_loss = float('inf')
criterion = torch.nn.CrossEntropyLoss().to(device) # 交叉熵损失
optimizer = torch.optim.SGD(model.parameters(), lr=4.0) # SGD优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9) # 学习率衰减
# 划分训练集和验证集(95%训练,5%验证)
train_len = int(len(train_dataset) * 0.95)
sub_train_, sub_valid_ = random_split(train_dataset, [train_len, len(train_dataset) - train_len])
# 训练循环
for epoch in range(N_EPOCHS):
start_time = time.time()
train_loss, train_acc = train(sub_train_)
valid_loss, valid_acc = valid(sub_valid_)
# 计算耗时
secs = int(time.time() - start_time)
mins = secs // 60
secs %= 60
# 打印训练信息
print(f'Epoch: {epoch + 1} | time in {mins} minutes, {secs} seconds')
print(f'\tLoss: {train_loss:.4f} (train) \t/ Acc: {train_acc * 100:.1f}% (train)')
print(f'\tLoss: {valid_loss:.4f} (valid) \t/ Acc: {valid_acc * 100:.1f}% (valid)')
训练过程输出示例:
Epoch:1 | time in 0 minutes,36 seconds
Loss:0.0592(train) | Acc:63.9%(train)
Loss:0.0005(valid) | Acc:69.2%(valid)
- 通过模型state_dict获取Embedding层的权重矩阵
# 打印嵌入层权重(词向量矩阵)
print(model.state_dict()['embedding.weight'])
# 输出示例:
# tensor([[ 0.4401, -0.4177, -0.4161, ..., 0.2497, -0.4657, -0.1861],
# [-0.2574, -0.1952, 0.1443, ..., -0.4687, -0.0742, 0.2606],
# ...])
- 新闻主题分类任务:判断新闻文本所属主题(互斥类别),属于文本分类问题。
- 数据:使用
AG_NEWs
数据集,包含 4 个主题,训练集 12 万条、验证集 7600 条。
- 实现步骤:
- 构建含 Embedding 层的分类模型(嵌入层 + 线性层)。
- 定义
generate_batch
函数处理 batch 数据。
- 实现训练与验证函数,计算损失和准确率。
- 划分数据集并进行多轮训练,动态调整学习率。
- 查看嵌入层的词向量矩阵,分析词汇嵌入结果。
关键知识点
- 文本分类流程:数据准备→模型构建→训练验证→评估
- Embedding层:将离散词汇索引映射为连续向量表示
- 批处理技术:将变长文本统一处理为固定维度的批数据
- 平均池化:处理变长文本的常用方法
- 学习率调度:StepLR实现学习率衰减
扩展思考
- 可以尝试不同的池化方法(如最大池化)
- 可以实验更复杂的模型结构(如加入RNN/CNN)
- 可以调整超参数(如Embedding维度、批大小等)观察效果变化
这个案例完整展示了使用PyTorch实现文本分类任务的全流程,特别适合初学者理解NLP任务的基本处理方法和深度学习模型的构建方式。