PyTorch 深度学习实战(7):循环神经网络(RNN)与文本分类(基于本地头条数据集)

在上一篇文章中,我们探讨了生成对抗网络(GAN)及其在图像生成中的应用。本文将转向自然语言处理(NLP)领域,介绍循环神经网络(RNN)的基本概念,并使用 PyTorch 构建一个 RNN 模型来解决文本分类问题。我们将基于本地的头条新闻数据集进行实战演练。


一、循环神经网络(RNN)基础

循环神经网络(RNN)是一种专门用于处理序列数据的神经网络结构。与传统的全连接神经网络不同,RNN 通过引入“记忆”机制,能够捕捉序列数据中的时间依赖关系,因此在 NLP、时间序列分析等领域有着广泛的应用。

1. RNN 的结构

RNN 的核心思想是将前一时刻的隐藏状态(Hidden State)传递到当前时刻,从而实现对序列数据的建模。其基本结构包括:

  • 输入层:接收序列中的每个时间步的输入数据。

  • 隐藏层:存储当前时间步的隐藏状态,并传递到下一个时间步。

  • 输出层:输出当前时间步的预测结果。

RNN 的计算公式如下:

2. RNN 的变体

尽管 RNN 在处理短序列时表现良好,但在处理长序列时容易出现梯度消失或梯度爆炸问题。为了解决这些问题,研究者提出了多种 RNN 的变体,例如:

  • LSTM(长短期记忆网络):通过引入门控机制,能够更好地捕捉长距离依赖关系。

  • GRU(门控循环单元):LSTM 的简化版本,计算效率更高。

3. 文本分类任务

文本分类是 NLP 中的经典任务之一,目标是将一段文本分配到预定义的类别中。例如,新闻分类、情感分析等都属于文本分类任务。


二、文本分类实战

我们将使用本地的头条新闻数据集来训练一个 RNN 模型,实现新闻标题的分类任务。

1. 问题描述

头条新闻数据集包含若干条新闻标题及其对应的类别标签。我们的目标是构建一个 RNN 模型,能够根据新闻标题预测其所属类别。

数据规模:共382688条,分布于15个分类中。每行为一条数据,以!分割的个字段,从前往后分别是 新闻ID,分类code(见下文),分类名称(见下文),新闻字符串(仅含标题),新闻关键词。原始数据集格式如下:6551700932705387022!101!news_culture!京城最值得你来场文化之旅的博物馆!保利集团,马未都,中国科学技术馆,博物馆,新中国

2. 实现步骤
  1. 加载和预处理数据。

  2. 构建词汇表并将文本转换为序列。

  3. 定义 RNN 模型。

  4. 定义损失函数和优化器。

  5. 训练模型。

  6. 测试模型并评估性能。

3. 代码实现
import pandas as pd
import numpy as np
import torch
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
import jieba  # 中文分词工具
import matplotlib.pyplot as plt
from collections import Counter
from sklearn.model_selection import train_test_split
​
# 设置 Matplotlib 支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # 设置字体为 SimHei(黑体)
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
​
# 数据预处理函数
def label_text(data):
    items = data.split('_!_')
    code = items[2]
    title = items[3]
    keyword = items[4]
    label = code
    text = title + keyword
    return label, text
​
# 加载数据
df = pd.DataFrame(columns=['label', 'text'])
data = []
with open('toutiao_cat_data.txt', 'r', encoding='utf-8') as file:
    for line in file:
        label, text = label_text(line)
        data.append({'label': label, 'text': text})
​
df = pd.DataFrame(data)
​
# 将标签映射为整数
label_to_idx = {label: idx for idx, label in enumerate(set(df['label']))}
df['label'] = df['label'].map(label_to_idx)
​
# 划分训练集和测试集
df_train, df_test = train_test_split(df, test_size=0.2, random_state=42, stratify=df['label'])
​
# 中文分词函数
def chinese_tokenizer(text):
    return list(jieba.cut(text))  # 使用结巴分词
​
# 构建词汇表
def build_vocab(texts, tokenizer, max_vocab_size=10000):
    word_freq = Counter()
    for text in texts:
        tokens = tokenizer(text)
        word_freq.update(tokens)
​
    # 按频率排序并截断
    sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
    vocab = {word: idx + 2 for idx, (word, _) in enumerate(sorted_words[:max_vocab_size])}
​
    # 添加特殊标记
    vocab['<PAD>'] = 0
    vocab['<UNK>'] = 1
    return vocab
​
# 生成词汇表
vocab = build_vocab(df_train['text'], chinese_tokenizer)
​
# 文本转索引序列
def text_to_indices(text, vocab, tokenizer):
    tokens = tokenizer(text)
    return [vocab.get(token, vocab['<UNK>']) for token in tokens]
​
# 自定义数据集类
class TextDataset(Dataset):
    def __init__(self, texts, labels, vocab, tokenizer, max_length=200):
        self.texts = texts.reset_index(drop=True)  # 确保索引连续
        self.labels = labels.reset_index(drop=True)  # 确保索引连续
        self.vocab = vocab
        self.tokenizer = tokenizer
        self.max_length = max_length
​
    def __len__(self):
        return len(self.texts)
​
    def __getitem__(self, idx):
        if idx >= len(self.texts):
            raise IndexError(f"Index {idx} is out of bounds for length {len(self.texts)}")
​
        text = self.texts[idx]
        indices = text_to_indices(text, self.vocab, self.tokenizer)
​
        # 填充/截断
        if len(indices) >= self.max_length:
            indices = indices[:self.max_length]
        else:
            indices = indices + [self.vocab['<PAD>']] * (self.max_length - len(indices))
​
        return {
            'text': torch.LongTensor(indices),
            'label': torch.LongTensor([self.labels[idx]]),
            'length': min(len(indices), self.max_length)
        }
​
# 创建Dataset
train_dataset = TextDataset(df_train['text'], df_train['label'], vocab, chinese_tokenizer)
test_dataset = TextDataset(df_test['text'], df_test['label'], vocab, chinese_tokenizer)
​
# 创建DataLoader
BATCH_SIZE = 64
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE)
​
# 定义RNN模型
class RNNClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.rnn = nn.GRU(embed_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, num_classes)
​
    def forward(self, text, lengths):
        # text shape: (batch_size, seq_len)
        embedded = self.embedding(text)  # (batch_size, seq_len, embed_dim)
        # 确保 lengths 是 CPU 上的张量
        lengths = lengths.to('cpu')
​
        # 处理变长序列
        packed = nn.utils.rnn.pack_padded_sequence(
            embedded, lengths, batch_first=True, enforce_sorted=False
        )
        _, hidden = self.rnn(packed)
​
        # hidden shape: (1, batch_size, hidden_dim)
        output = self.fc(hidden.squeeze(0))
        return output
​
# 模型参数
VOCAB_SIZE = len(vocab)
EMBED_DIM = 128
HIDDEN_DIM = 256
NUM_CLASSES = len(set(df_train['label']))  # 类别数量
​
# 设备选择
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = RNNClassifier(VOCAB_SIZE, EMBED_DIM, HIDDEN_DIM, NUM_CLASSES).to(device)
​
# 训练函数
def train(model, dataloader, optimizer, criterion):
    model.train()
    total_loss = 0
    correct = 0
    total = 0
​
    for batch in dataloader:
        texts = batch['text'].to(device)
        labels = batch['label'].squeeze().to(device)
        lengths = batch['length']
​
        optimizer.zero_grad()
        outputs = model(texts, lengths)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
​
        total_loss += loss.item()
        _, predicted = torch.max(outputs, 1)
        correct += (predicted == labels).sum().item()
        total += labels.size(0)
​
    return total_loss / len(dataloader), correct / total
​
# 评估函数
def evaluate(model, dataloader, criterion):
    model.eval()
    total_loss = 0
    correct = 0
    total = 0
​
    with torch.no_grad():
        for batch in dataloader:
            texts = batch['text'].to(device)
            labels = batch['label'].squeeze().to(device)
            lengths = batch['length'].to(device)
​
            outputs = model(texts, lengths)
            loss = criterion(outputs, labels)
​
            total_loss += loss.item()
            _, predicted = torch.max(outputs, 1)
            correct += (predicted == labels).sum().item()
            total += labels.size(0)
​
    return total_loss / len(dataloader), correct / total
​
# 初始化优化器和损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
​
# 训练参数
NUM_EPOCHS = 10
train_losses = []
val_losses = []
accuracies = []
​
# 学习率调度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
​
# 训练循环
for epoch in range(NUM_EPOCHS):
    train_loss, train_acc = train(model, train_loader, optimizer, criterion)
    val_loss, val_acc = evaluate(model, test_loader, criterion)
​
    scheduler.step()  # 更新学习率
​
    train_losses.append(train_loss)
    val_losses.append(val_loss)
    accuracies.append(val_acc)
​
    print(f'Epoch {epoch + 1}/{NUM_EPOCHS}')
    print(f'Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}')
    print(f'Val Accuracy: {val_acc:.4f}')
​
# 可视化结果
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_losses, label='Train Loss')
plt.plot(val_losses, label='Val Loss')
plt.legend()
​
plt.subplot(1, 2, 2)
plt.plot(accuracies, label='Val Accuracy')
plt.legend()
plt.show()
​
model.eval()  # 设置为评估模式
​
# 输入文本
input_text = '突发!乌克兰遭大规模袭击,乌军:俄军动用67枚导弹,已击落34枚!泽连斯基呼吁部分停火'
​
# 分词
tokens = chinese_tokenizer(input_text)
​
# 转换为索引序列
indices = text_to_indices(input_text, vocab, chinese_tokenizer)
​
# 填充/截断
max_length = 200
if len(indices) >= max_length:
    indices = indices[:max_length]
else:
    indices = indices + [vocab['<PAD>']] * (max_length - len(indices))
​
# 转换为张量
input_tensor = torch.LongTensor(indices).unsqueeze(0).to(device)  # 添加 batch 维度
length_tensor = torch.LongTensor([len(indices)]).to(device)  # 序列长度
​
# 预测
with torch.no_grad():
    output = model(input_tensor, length_tensor)
    _, predicted = torch.max(output, 1)
​
# 获取类别
predicted_label = predicted.item()
​
# 将预测的类别索引映射回原始标签
idx_to_label = {idx: label for label, idx in label_to_idx.items()}
predicted_class = idx_to_label[predicted_label]
​
print(f"预测的类别是: {predicted_class}")

三、代码解析

  1. 数据加载与预处理

    • 使用 jieba 进行中文分词。

    • 构建词汇表并将文本转换为索引序列。

    • 使用 TextDataset 类封装数据,支持填充和截断。

  2. RNN 模型

    • 使用 nn.Embedding 将单词索引转换为词向量。

    • 使用 nn.GRU 处理序列数据,并通过全连接层输出分类结果。

  3. 训练过程

    • 使用交叉熵损失函数和 Adam 优化器。

    • 训练 10 个 epoch,并记录损失值和准确率。

  4. 测试过程

    • 在测试集上评估模型性能,计算准确率。

  5. 可视化

    • 绘制训练损失、验证损失和验证准确率曲线。


四、运行结果

运行上述代码后,你将看到以下输出:

  • 训练过程中每 epoch 打印一次损失值和准确率。

  • 测试集准确率(通常在 80% 以上)。

  • 训练损失和验证损失曲线图。


五、总结

本文介绍了循环神经网络(RNN)的基本概念,并使用 PyTorch 实现了一个简单的中文文本分类模型。通过这个例子,我们学习了如何处理中文文本数据、构建 RNN 模型以及进行训练和评估。

在下一篇文章中,我们将探讨 Transformer 模型及其在机器翻译中的应用。敬请期待!


代码实例说明

  • 本文代码可以直接在 Jupyter Notebook 或 Python 脚本中运行。

  • 如果你有 GPU,可以将模型和数据移动到 GPU 上运行,例如:model = model.to('cuda')texts = texts.to('cuda')

希望这篇文章能帮助你更好地理解 RNN 及其在文本分类中的应用!如果有任何问题,欢迎在评论区留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值