在前面的文章中,我们介绍了如何使用RNN和LSTM进行文本分类。在本篇文章中,我们将引入另一种强大的深度学习方法——卷积神经网络(CNN)。CNN通常用于图像处理,但它也能够在文本分类中表现出色。通过捕捉文本中的n-grams(如二元组、三元组等),CNN可以识别出有助于分类的重要特征。
1. 卷积神经网络在文本分类中的应用
CNN在图像处理中通常使用2D卷积层,但对于文本,文本数据是一维的。我们可以将文本视为一个“序列”,然后通过多个大小不同的卷积核来提取特征。每个卷积核可以理解为一种特定的模式检测器,它在文本中搜索相关的n-grams。我们将使用不同大小的卷积核,来捕捉二元组(bi-gram)、三元组(tri-gram)等不同长度的特征。
例如,一个大小为2的卷积核将覆盖连续的两个单词,模拟二元组。通过对这些n-grams进行卷积操作,并结合池化层的作用,我们可以提取出对分类任务至关重要的特征。
2. 数据加载与预处理
首先,我们使用TorchText
库加载AG_NEWS数据集,并进行预处理,使用与之前相同的流程,包括使用spacy
进行标记化、构建词汇表,并加载预训练的FastText词嵌入。
from torchtext.datasets import AG_NEWS
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import FastText
# 加载数据集
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>"])
# 引入FastText预训练词嵌入
fast_vectors = FastText(language='simple')
fast_embedding = fast_vectors.get_vecs_by_tokens(vocab.get_itos()).to(device)
3. 卷积神经网络模型设计
我们设计了一个简单的CNN模型。它由一个嵌入层、多个卷积层、池化层以及全连接层组成。卷积层将捕捉不同大小的n-grams特征,而池化层则用于提取每个特征中最显著的部分。
模型结构
- 嵌入层:将输入的文本序列转化为词嵌入矩阵。
- 卷积层:使用不同大小的卷积核来捕捉n-grams特征。
- 池化层:通过最大池化操作选取卷积输出中的最重要信息。
- 全连接层:将池化后的特征拼接起来,进行分类。
import torch.nn as nn
import torch.nn.functional as F
class CNN(nn.Module):
def __init__(self, input_dim, emb_dim, output_dim, dropout, n_filters, filter_sizes):
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim, padding_idx=pad_idx)
# 多个不同大小的卷积核
self.convs = nn.ModuleList([
nn.Conv2d(in_channels=1,
out_channels=n_filters,
kernel_size=(fs, emb_dim))
for fs in filter_sizes
])
self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, text):
embedded = self.embedding(text) # 嵌入
embedded = embedded.unsqueeze(1) # 扩展维度以适配Conv2D
# 卷积和池化操作
conved = [F.relu(conv(embedded)).squeeze(3) for conv in self.convs]
pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
# 拼接池化后的结果
cat = self.dropout(torch.cat(pooled, dim=1))
return self.fc(cat)
在这个模型中,我们使用了三种不同大小的卷积核(如3、4、5),来捕捉不同长度的n-grams。通过卷积和最大池化操作,我们提取出了这些n-grams中的关键信息,并将它们拼接在一起后通过全连接层进行分类。
4. 模型训练与评估
我们使用Adam优化器进行模型训练,并在训练过程中计算模型的损失和准确率。以下是完整的训练与评估代码:
import torch.optim as optim
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 计算准确率
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) in enumerate(loader):
label = label.to(device)
text = text.to(device)
# 前向传播
predictions = model(text).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) in enumerate(loader):
label = label.to(device)
text = text.to(device)
predictions = model(text).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
我们可以使用5个epoch训练模型,并保存最佳的模型。
num_epochs = 5
best_valid_loss = float('inf')
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))
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'best-cnn-model.pt')
print(f'Epoch {epoch+1} | Train Loss: {train_loss:.3f}, Train Acc: {train_acc*100:.2f}%')
print(f'Valid Loss: {valid_loss:.3f}, Valid Acc: {valid_acc*100:.2f}%')
5. 测试与预测
训练完成后,我们可以使用模型对新的文本进行预测。
def predict(text):
with torch.no_grad():
output = model(text).squeeze(1)
predicted = torch.max(output.data, 1)[1]
return predicted
test_str = "Google is facing financial challenges."
text = torch.tensor(text_pipeline(test_str)).unsqueeze(0).to(device)
prediction = predict(text)
print(f'预测结果: {prediction.item()}')
结语
在本文中,我们通过引入卷积神经网络(CNN),展示了如何高效地进行文本分类。CNN通过不同大小的卷积核捕捉文本中的n-grams特征,结合最大池化层提取最重要的信息,从而实现了高效的特征提取和分类。然而,虽然CNN在处理短文本任务中表现出色,它在处理长序列或依赖较长上下文的任务时可能有所不足。
为了更好地处理这些复杂的上下文和长依赖关系问题,我们可以引入序列到序列(seq2seq)模型。seq2seq模型通过编码器和解码器结构,能够将一个序列映射为另一个序列,这对于处理翻译、摘要生成等任务非常有效。在seq2seq模型中,编码器将输入序列转换为一个固定长度的上下文向量,解码器根据该向量生成输出序列。这种结构不仅适用于自然语言生成任务,也能够帮助处理长文本的分类问题。
敬请期待下一篇文章,届时我们将详细介绍seq2seq模型的工作原理及其在自然语言处理中的应用!
如果你觉得这篇博文对你有帮助,请点赞、收藏、关注我,并且可以打赏支持我!
欢迎关注我的后续博文,我将分享更多关于人工智能、自然语言处理和计算机视觉的精彩内容。
谢谢大家的支持!