0%

[NLP] 新手的第一个 NLP 项目:文本分类(2)

现在数据已经准备就绪,可以构建模型了。

本文的模型参考了论文 《Convolutional Neural Networks for Sentence Classification》,原文代码在此

论文里使用了两个词嵌入:随模型进行训练的词嵌入和 Google 预训练好的 Word2Vec 词嵌入。本文里为了直观,没有采用预训练的词嵌入。

构建模型

论文里使用了三个卷积核分别为 3、4、5 的二维卷积层,拼接后经过一个范围为 4 的池化层。最后经过一个全连接层,经过 sigmoid 函数处理后输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from torch import nn
from torch.nn import functional as F

class CNN(nn.Module):
def __init__(self, vocab_size, embed_size, dropout, batch_size):
super(CNN, self).__init__()
self.batch_size = batch_size

self.embedding = nn.Embedding(vocab_size, embed_size) # (BATCH_SIZE, SEQ_LEN, embed_size)

self.conv1 = nn.Conv2d(1, 1, 3)
self.conv2 = nn.Conv2d(1, 1, 4)
self.conv3 = nn.Conv2d(1, 1, 5)

self.dropout = nn.Dropout(dropout)

self.fc = nn.Linear(2232, 1)

def forward(self, x):
x = self.embedding(x)
x.unsqueeze_(1) # (BATCH_SIZE, 1, SEQ_LEN, embed_size)
output1 = self.conv1(x)
output1 = F.max_pool2d(F.relu(output1), 4)

output2 = self.conv2(x)
output2 = F.max_pool2d(F.relu(output2), 4)

output3 = self.conv3(x)
output3 = F.max_pool2d(F.relu(output3), 4)
output = torch.cat([output1, output2, output3], axis=1)
output = self.dropout(output)

return self.fc(output.view(self.batch_size, -1))
注意:因为经过词嵌入的张量维度为 (BATCH_SIZE, SEQ_LEN, embed_size),而 nn.Conv2d 的输入张量的维度要求为 (BATCH_SIZE, CHANNEL, NONE, NONE),所以我们需要使用 x.unsqueeze_(1) 为张量添加一个维度。

我们使用 Adam 为优化器,nn.BCEWithLogitsLoss() 为损失函数。

1
2
3
4
from torch import optim

optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()
注意:nn.BCEWithLogitsLoss() 是先进行了 sigmoid 运算后再求交叉熵的损失函数,无需额外的 sigmoid 运算。

然后我们再定义一个求准确率的函数:

1
2
3
4
5
def binary_accuracy(preds, y):
rounded_preds = torch.round(torch.sigmoid(preds))
correct = (rounded_preds == y).float()
acc = correct.sum() / len(correct)
return acc

紧接着我们开始定义训练和验证的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 训练函数
def train(model, iterator, optimizer, criterion):
epoch_loss = 0
epoch_acc = 0

model.train() # 训练模式

for text, label in iterator:
optimizer.zero_grad()
preds = model(text)
loss = criterion(preds.squeeze(), label.float())
acc = binary_accuracy(preds.squeeze(), label)
loss.backward()
optimizer.step()

epoch_loss += loss.item()
epoch_acc += acc.item()

return epoch_loss / len(iterator), epoch_acc / len(iterator)

# 验证函数
def evaluate(model, iterator, criterion):
epoch_loss = 0
epoch_acc = 0

model.eval() # 验证模式

with torch.no_grad():
for text, label in iterator:
preds = model(text)
loss = criterion(preds.squeeze(), label.float())
acc = binary_accuracy(preds.squeeze(), label)

epoch_loss += loss.item()
epoch_acc += acc.item()

return epoch_loss / len(iterator), epoch_acc / len(iterator)

可以看到,训练函数与验证函数大同小异,主要区别在于:

  1. 训练模式下权重更新,验证模式下权重不更新;
  2. 验证模式没有优化器。
    注意:在计算损失函数时,真实标签也要转换成 float 格式,否则会报错。
    下面就可以构建真正的训练、评估循环了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import time

    def epoch_time(start_time, end_time): # 计算每一轮花费的时间
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - elapsed_mins * 60)
    return elapsed_mins, elapsed_secs

    N_EPOCHS = 10
    best_test_loss = float('inf')

    for epoch in range(N_EPOCHS):
    start_time = time.time()

    train_loss, train_acc = train(model, train_iter, optimizer, criterion)
    test_loss, test_acc = evaluate(model, test_iter, criterion)

    end_time = time.time()
    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    if test_loss < best_test_loss:
    best_test_loss = test_loss
    torch.save(model.state_dict(), 'model.pt')

    print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
    print(f'\t Val. Loss: {test_loss:.3f} | Val. Acc: {test_acc*100:.2f}%')
    我们进行 10 轮训练,如果验证集的准确率大于最大准确率,则保存模型。最佳结果为:
    1
    2
    3
    Epoch: 10 | Epoch Time: 4m 5s
    Train Loss: 0.171 | Train Acc: 93.41%
    Val. Loss: 0.426 | Val. Acc: 82.85%
    这个结果马马虎虎,希望在后面将模型改进后,模型的表现会更好。

可以在 https://github.com/vincent507cpu/nlp\_project/blob/master/text%20classification/01%20CNN.ipynb 查看全部代码。

欢迎关注我的其它发布渠道