基于Python的自然语言处理系列(9):使用TorchText与预训练词嵌入进行新闻分类

        在前一篇文章中,我们展示了如何使用TorchText和RNN进行新闻分类。在这篇文章中,我们将改进之前的模型,通过使用预训练词嵌入、优化器的更改、正交初始化以及打包填充序列的技巧,提升模型的学习效率和效果。

1. 改进方向

提高模型学习效果:

  • 使用预训练词嵌入:使用FastText嵌入预训练的词向量,预计准确率提升约20%。
  • 更换优化器为Adam:提升学习速度。
  • 正交初始化:用于RNN/LSTM或CNN的初始化方法,尽管改善有限,但是一种优秀的实践。

提高模型计算效率:

  • 打包填充序列:通过打包填充序列,RNN将忽略填充部分,节省计算量,大幅提高模型的准确率。
  • Embedding层的padding_idx:让Embedding层忽略填充标记,减少无用的计算,提升效率。

2. 数据加载与预处理

        首先,我们使用TorchText加载AG_NEWS数据集,并进行预处理。我们使用spacy进行文本的标记化,并创建词汇表,将文本转化为对应的整数表示。

from torchtext.datasets import AG_NEWS
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator

# 加载数据集
train, test = AG_NEWS()

# 使用spacy进行标记化
tokenizer = get_tokenizer('spacy', language='en_core_web_sm')

# 构建词汇表
def yield_tokens(data_iter):
    for _, text in data_iter:
        yield tokenizer(text)

vocab = build_vocab_from_iterator(yield_tokens(train), specials=['<unk>', '<pad>'])
vocab.set_default_index(vocab["<unk>"])

        为了节省计算资源,我们对数据集进行了随机拆分,仅保留20%的训练数据和10%的验证数据:

train_size = len(list(iter(train)))
too_much, train, valid = train.random_split(total_length=train_size, weights = {"too_much": 0.7, "train": 0.2, "valid": 0.1}, seed=999)

train_size = len(list(iter(train)))
val_size = len(list(iter(valid)))
print(f"训练集大小: {train_size}, 验证集大小: {val_size}")

3. 使用FastText嵌入

        接下来,我们将使用FastText的预训练词嵌入,将其应用到我们的词汇表中。FastText能够处理OOV(Out Of Vocabulary,超出词汇表)的词汇,并考虑到词汇的子词信息,因此在许多应用中具有较好的表现。

from torchtext.vocab import FastText

# 下载FastText预训练向量
fast_vectors = FastText(language='simple')

# 获取FastText嵌入矩阵
fast_embedding = fast_vectors.get_vecs_by_tokens(vocab.get_itos()).to(device)

4. 打包填充序列与模型设计

        为了提高RNN的计算效率,我们将使用打包填充序列(Packed Padded Sequences),让RNN忽略填充部分,节省不必要的计算。

from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence

# 定义collate_fn函数,创建批次数据
def collate_batch(batch):
    label_list, text_list, length_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)
        length_list.append(processed_text.size(0))
    return torch.tensor(label_list, dtype=torch.int64), pad_sequence(text_list, padding_value=vocab['<pad>'], batch_first=True), torch.tensor(length_list, dtype=torch.int64)

        我们将在RNN模型中应用打包填充序列,以提高计算效率:

class PaddedRNN(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, output_dim):
        super().__init__()
        # 通过设置padding_idx让Embedding层忽略填充
        self.embedding = nn.Embedding(input_dim, emb_dim, padding_idx=vocab['<pad>'])
        self.rnn = nn.RNN(emb_dim, hid_dim, batch_first=True)
        self.fc = nn.Linear(hid_dim, output_dim)
        
    def forward(self, text, text_lengths):
        # 嵌入
        embedded = self.embedding(text)
        
        # 打包嵌入
        packed_embedded = pack_padded_sequence(embedded, text_lengths.to('cpu'), batch_first=True, enforce_sorted=False)
        
        # 通过RNN层
        packed_output, hn = self.rnn(packed_embedded)
        
        # 解包
        output, _ = pad_packed_sequence(packed_output, batch_first=True)
        
        return self.fc(hn.squeeze(0))

5. 模型训练与优化

        为了加速训练,我们使用Adam优化器,并在RNN权重上应用正交初始化,以应对梯度消失和爆炸问题。

import torch.optim as optim

# 定义优化器和损失函数
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()

# 正交初始化
def initialize_weights(m):
    if isinstance(m, nn.Linear):
        nn.init.xavier_normal_(m.weight)
    elif isinstance(m, nn.RNN):
        for name, param in m.named_parameters():
            if 'weight' in name:
                nn.init.orthogonal_(param)

6. 模型训练与评估

        接下来,我们定义模型的训练和评估函数。训练函数将模型设置为训练模式,评估函数则将模型设置为评估模式,并避免梯度计算。

def accuracy(preds, y):
    predicted = torch.max(preds.data, 1)[1]
    batch_corr = (predicted == y).sum()
    acc = batch_corr / len(y)
    return acc

def train(model, loader, optimizer, criterion, loader_length):
    epoch_loss = 0
    epoch_acc = 0
    model.train()
    
    for i, (label, text, text_length) in enumerate(loader): 
        label = label.to(device)
        text = text.to(device)
        
        predictions = model(text, text_length).squeeze(1)
        
        # 计算损失和准确率
        loss = criterion(predictions, label)
        acc  = accuracy(predictions, label)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        epoch_loss += loss.item()
        epoch_acc += acc.item()
                        
    return epoch_loss / loader_length, epoch_acc / loader_length

def evaluate(model, loader, criterion, loader_length):
    epoch_loss = 0
    epoch_acc = 0
    model.eval()
    
    with torch.no_grad():
        for i, (label, text, text_length) in enumerate(loader): 
            label = label.to(device)
            text = text.to(device)

            predictions = model(text, text_length).squeeze(1)
            
            loss = criterion(predictions, label)
            acc  = accuracy(predictions, label)

            epoch_loss += loss.item()
            epoch_acc += acc.item()
        
    return epoch_loss / loader_length, epoch_acc / loader_length

        在每个epoch结束时,我们会保存模型的最佳验证结果,并绘制损失和准确率的变化图:

import matplotlib.pyplot as plt

train_losses, train_accs, valid_losses, valid_accs = [], [], [], []
best_valid_loss = float('inf')
num_epochs = 5

for epoch in range(num_epochs):
    train_loss, train_acc = train(model, train_loader, optimizer, criterion, len(train_loader))
    valid_loss, valid_acc = evaluate(model, valid_loader, criterion, len(valid_loader))
    
    train_losses.append(train_loss)
    train_accs.append(train_acc)
    valid_losses.append(valid_loss)
    valid_accs.append(valid_acc)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'best-model.pt')
    
    print(f'Epoch: {epoch+1} | Train Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
    print(f'Val Loss: {valid_loss:.3f} | Val Acc: {valid_acc*100:.2f}%')

# 可视化损失和准确率
plt.figure(figsize=(10,6))
plt.plot(train_losses, label='Train Loss')
plt.plot(valid_losses, label='Valid Loss')
plt.legend()
plt.show()

plt.figure(figsize=(10,6))
plt.plot(train_accs, label='Train Acc')
plt.plot(valid_accs, label='Valid Acc')
plt.legend()
plt.show()

7. 测试与预测

        我们可以使用训练好的模型对新的文本进行预测。以下是如何测试随机新闻文本:

def predict(text, text_length):
    with torch.no_grad():
        output = model(text, text_length).squeeze(1)
        predicted = torch.max(output.data, 1)[1]
        return predicted

test_str = "Google is facing major challenges in its business."
text = torch.tensor(text_pipeline(test_str)).unsqueeze(0).to(device)
text_length = torch.tensor([text.size(1)]).to(device)

prediction = predict(text, text_length)
print(f'预测结果: {prediction.item()}')

结语

        通过本文的探索,我们学习了如何通过引入预训练的FastText词嵌入正交初始化打包填充序列来提升文本分类模型的性能。这些改进不仅增强了模型的学习效果,还提高了计算效率。此外,我们还使用了Adam优化器,进一步加速了模型的训练过程。

        这些技巧在实际应用中非常有用,尤其是处理大规模文本数据时,它们能够显著减少计算资源的消耗,同时提升模型的预测准确性。在接下来的文章中,我们将继续探索更复杂的网络架构,例如LSTM,以进一步优化文本分类任务的表现。

        通过不断引入新技术和优化策略,相信读者将能够从中学到更多深层次的自然语言处理(NLP)知识,并逐步应用到实际项目中。

如果你觉得这篇博文对你有帮助,请点赞、收藏、关注我,并且可以打赏支持我!

欢迎关注我的后续博文,我将分享更多关于人工智能、自然语言处理和计算机视觉的精彩内容。

谢谢大家的支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的Anthony

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值