当前位置:   article > 正文

(二)升级版RNN实现文本情感分析(Pytorch)_multi layer rnn

multi layer rnn


在前面的笔记中,我们记下了基本面,以便进行情绪分析。在这个笔记本里,我们会得到不错的结果。
我们将使用:

  • packed padded sequences
  • pre-trained word embeddings
  • different RNN architecture
  • bidirectional RNN
  • multi-layer RNN
  • regularization
  • a different optimizer

这将使我们达到约84%的测试精度。

准备数据

与前面一样,我们将设置种子(seed)、定义字段(Fields)并获取train/valid/test的分割数据集。

我们将使用填充的序列(packed padded sequences),这将使我们的RNN只处理序列中没有填充的元素,并且对于任何填充的元素,输出张量都是零。为了使用填充的序列,我们必须告诉RNN实际的序列有多长。为此,我们为文本(TEXT)字段设置include_length = True。这将导致batch.text现在是一个元组,元组中第一个元素是我们的句子(一个被填充的数字化张量),第二个元素是句子的实际长度。

import torch
from torchtext import data
from torchtext import datasets

SEED = 1234

torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

TEXT = data.Field(tokenize='spacy', tokenizer_language='en_core_web_sm', include_lengths=True)
LABEL = data.LabelField(dtype = torch.float)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后我们加载IMDb数据集。

from torchtext import datasets

train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
  • 1
  • 2
  • 3

然后从我们的训练集中创建验证集。

import random

train_data, valid_data = train_data.split(random_state = random.seed(SEED))
  • 1
  • 2
  • 3

其次是使用预先训练的词嵌入。现在,我们不用随机初始化单词嵌入,而是用这些预先训练过的向量初始化它们。我们可以通过指定需要的向量并将其作为参数传递给build_vocab来获得这些向量。TorchText处理下载向量,并将它们与我们词汇表中的正确单词联系起来。

在这里,我们要用glove.6B.100d向量。glove是用来计算向量的算法,可在这里获取更多信息。6B表示这些向量是用6亿个标记训练的,100d表示这些向量是100维的。

你可以在这里看到其他合适的向量。

其理论是,这些预先训练的向量在向量空间中已经有了语义相近的词,例如:“terrible”, “awful”, "dreadful"相邻近。这给了我们的嵌入层一个良好的初始化,因为它不需要从头学习这些关系。

注意:这些向量大约是862MB,所以如果你有一个有限的互联网连接要注意。

默认情况下,TorchText会初始化你词汇表中的单词,但不在你预先训练的嵌入中(pre-trained embeddings)的单词会初始化为零。我们不想要这个,而是通过将unk_init设置为torch.Tensor.normal_来随机初始化它们。这将通过高斯分布(Gaussian distribution)初始化这些单词。

MAX_VOCAB_SIZE = 25_000

TEXT.build_vocab(train_data, 
                 max_size = MAX_VOCAB_SIZE, 
                 vectors = "glove.6B.100d", 
                 unk_init = torch.Tensor.normal_)

LABEL.build_vocab(train_data)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

与之前一样,我们创建迭代器,如果有可用的GPU,就将张量放置在GPU上。

对于填充遮蔽序列(packed padded sequences)的另一件事是,批中的所有张量都需要按它们的长度排序。这在迭代器中通过设置sort_within_batch = True来处理。


BATCH_SIZE = 64

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
    (train_data, valid_data, test_data), 
    batch_size = BATCH_SIZE,
    sort_within_batch = True,
    device = device)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

搭建模型

不同的RNN架构

我们将使用一种不同的RNN体系结构,称为长短期记忆(LSTM)。为什么LSTM优于标准RNN?标准的rnn存在梯度消失的问题。LSTMs通过拥有一个称为cell c c c的额外循环状态(可以认为是LSTM的“memory”)来克服这一点,并且使用多个门(gates )来控制进出记忆的信息流。要了解更多信息,请点击这里。我们可以简单地认为LSTM是 x t x_t xt h t h_t ht c t c_t ct的函数,而不是仅仅是只有 x t x_t xt h t h_t ht的函数。
在这里插入图片描述
因此,使用了LSTM的模型看起来像这样(省略了嵌入层):
在这里插入图片描述
初始cell状态 c 0 c_0 c0,就像初始隐藏状态 h 0 h_0 h0一样,被初始化为一个全为0的张量。但是,我们仍然只使用最终的隐藏状态,而不是最终的cell状态来进行情感预测,即 y ^ = f ( h T ) \hat{y}=f(h_T) y^=f(hT)

Bidirectional RNN

双向RNN背后的概念很简单。除了有一个RNN从第一个到最后一个处理句子中的单词(前向RNN)外,我们还有一个RNN从最后一个到第一个处理句子中的单词(后向RNN)。在时间步 t t t,正向RNN为处理单词 x t x_t xt,反向RNN为处理单词 x t − t + 1 x_{t -t+1} xtt+1

PyTorch中,由前向和后向rnn返回的隐藏状态(和cell状态)张量叠放在一个张量之上。

我们使用前向RNN(从句子的最后一个单词中获得)的最后一个隐藏状态 h T → h_T^\rightarrow hT和后向RNN(从句子的第一个单词中获得)的最后一个隐藏状态 h T ← h_T^\leftarrow hT进行情感预测,即$ \帽子{y} = f (h_T ^ \ rightarrow h_T ^ \ leftarrow)美元

下图显示了一个双向RNN,正向RNN为橙色,反向RNN为绿色,线性层为银色。
在这里插入图片描述

Multi-layer RNN

多层rnn(也称为深度rnn)是另一个简单的概念。我们的想法是在初始标准RNN的基础上添加额外的RNN,每添加一个RNN就是另一层。第一个(底部)RNN在时间步长 t t t时输出的隐藏状态将作为其在时间步长 t t t时对上面RNN的输入。然后从最终(最高)层的最终隐藏状态进行预测。

下图是一个多层单向RNN,层数以上标表示。还要注意,每一层都需要自己的初始隐藏状态 h 0 L h_0^L h0L
在这里插入图片描述

Regularization

尽管我们对模型进行了改进,但每个改进都增加了额外的参数。不要对太多细节进行过度拟合,你的模型中参数越多,你的模型进行过度拟合的概率就越高(记住训练数据,会导致较低的训练误差但也会导致较高的验证/测试误差,即对新的、看不见的例子泛化较差)。为了解决这个问题,我们使用正则化(regularization )。更具体地说,我们使用一种称为dropout的正则化方法。Dropout的工作原理是在前进过程中随机删除(设置为0)一层中的神经元。用超参数来确定每个神经元dropout的概率,对每个应用了dropout的神经元进行独立考虑。关于dropout为何有效的一个理论是,去掉参数的模型可以被视为“较弱”(参数较少)的模型。所有这些“较弱”的模型(每一次向前通过一个模型)的预测都在模型的参数中取平均值。因此,您的一个模型可以被认为是较弱模型的集合,这些模型都没有被过度参数化,因此也不应该过度拟合。

实现细节

对这个模型的另一个补充是,我们不会学习《pad》标记的嵌入。这是因为我们想明确地告诉我们的模型填充标记与确定句子的情感无关。这意味着pad标记的嵌入将保持在初始化时的位置(我们稍后将其初始化为所有的零)。为此,我们将填充标记的索引作为padding_idx参数传递给nn.Embedding层。

为了使用LSTM代替标准的RNN,我们使用nn.LSTM而不是nn.RNN。另外,请注意LSTM返回output和一个包含最终 hidden state和最终cell状态的元组,而标准RNN只返回output和最终hidden state。

由于我们的LSTM的最终隐藏状态同时有一个前向和一个后向组件,这两个组件将被串接在一起,输入到nn.Linear层的大小是隐藏维度的两倍。

通过传递num_layers的值和RNN/LSTM的bidirectional参数来实现双向性和添加额外的层。

Dropout是通过初始化nn来实现的。Dropout层(参数是每个神经元dropout的概率),并在我们想要应用Dropout到的每一层后,在forward方法中使用它。注意:永远不要在输入或输出层(本例中是text层或fc层)上使用dropout,您只希望在中间层上使用dropout。LSTM有一个dropout参数,它将dropout添加在一个层的隐藏状态到下一层的隐藏状态之间的连接上。

当我们传递的句子的长度以能够使用填充序列时,我们必须添加第二个参数text_length给forward。

在将嵌入内容传递给RNN之前,我们需要对它们进行打包,这是我们使用nn.utils.rnn.packed_padded_sequence所做的。这将导致RNN只处理序列中的非填充元素。然后RNN将返回packed_output(一个打包序列)以及隐藏状态和cell状态(两者都是张量)。如果没有打包填充序列,hidden和cell都是序列中最后一个元素的张量,这很可能是一个填充标记,但是当使用打包填充序列时,它们都来自序列中最后一个非填充元素。

然后,我们使用nn.utils.rnn.pad_packed_sequence解包输出序列,将它从一个填充序列转换为一个张量。填充标记输出的元素将是零张量(其中每个元素都为零的张量)。通常,我们只有在稍后要在模型中使用输出时才需要解包输出。尽管在本例中没有,但我们仍然解包了序列,以展示如何完成。

最终隐藏状态,hidden,形状为[num layers * num direction, batch size, hidden dim]。这些是有序的:[forward_layer_0, backward_layer_0, forward_layer__1, backward_layer_1,…, forward layer_n, backward_layer n]。因为我们想要最终的(顶部)层的向前和向后隐藏状态,我们从第一个维度获得最上面的两个隐藏层,hidden[-2,:,:]和hidden[-1,:,:],并在将它们传递到线性层(应用dropout后)之前将它们连接在一起。

import torch.nn as nn

class RNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, 
                 bidirectional, dropout, pad_idx):
        
        super().__init__()
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx)
        
        self.rnn = nn.LSTM(embedding_dim, 
                           hidden_dim, 
                           num_layers=n_layers, 
                           bidirectional=bidirectional, 
                           dropout=dropout)
        
        self.fc = nn.Linear(hidden_dim * 2, output_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, text, text_lengths):
        
        #text = [sent len, batch size]
        
        embedded = self.dropout(self.embedding(text))
        
        #embedded = [sent len, batch size, emb dim]
        
        #pack sequence
        packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths)
        
        packed_output, (hidden, cell) = self.rnn(packed_embedded)
        
        #unpack sequence
        output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)

        #output = [sent len, batch size, hid dim * num directions]
        #output over padding tokens are zero tensors
        
        #hidden = [num layers * num directions, batch size, hid dim]
        #cell = [num layers * num directions, batch size, hid dim]
        
        #concat the final forward (hidden[-2,:,:]) and backward (hidden[-1,:,:]) hidden layers
        #and apply dropout
        
        hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))
                
        #hidden = [batch size, hid dim * num directions]
            
        return self.fc(hidden)
  • 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
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

与之前一样,我们将创建RNN类的一个实例,其中包含number of layers, bidirectionality and dropout probability的新参数。

为了保证预训练的向量能够加载到模型中,embeddding_dim必须与先前加载的预训练的GloVe 向量相等。

我们从词汇表中获取pad标记索引,从字段(field)的pad_token属性(默认为《pad》)中获取pad标记的实际字符串表示。

INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]

model = RNN(INPUT_DIM, 
            EMBEDDING_DIM, 
            HIDDEN_DIM, 
            OUTPUT_DIM, 
            N_LAYERS, 
            BIDIRECTIONAL, 
            DROPOUT, 
            PAD_IDX)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我们将打印出模型中的参数数量。

请注意,我们的参数几乎是以前的两倍!

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')
  • 1
  • 2
  • 3
  • 4

The model has 4,810,857 trainable parameters

最后添加的是复制我们之前加载到模型的嵌入层的预先训练的单词嵌入。

我们从字段(fild)的词汇表中检索嵌入,并检查它们的大小是否正确,[vocab size,embedding dim]

pretrained_embeddings = TEXT.vocab.vectors

print(pretrained_embeddings.shape)
  • 1
  • 2
  • 3

torch.Size([25002, 100])

然后我们用预先训练好的嵌入层替换初始权重。

注意:这应该总是在weight.data上完成而不是weight!

model.embedding.weight.data.copy_(pretrained_embeddings)
  • 1
tensor([[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
        [ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
        [-0.0382, -0.2449,  0.7281,  ..., -0.1459,  0.8278,  0.2706],
        ...,
        [-0.0614, -0.0516, -0.6159,  ..., -0.0354,  0.0379, -0.1809],
        [ 0.1885, -0.1690,  0.1530,  ..., -0.2077,  0.5473, -0.4517],
        [-0.1182, -0.4701, -0.0600,  ...,  0.7991, -0.0194,  0.4785]])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们现在可以看到嵌入权重矩阵的前两行已经被设为0。当我们将pad标记的索引传递给嵌入层的padding_idx时,它将在整个训练过程中保持为零,然而《unk》标记嵌入将被学习。

训练模型

现在来训练模型。

我们在这里要做的唯一更改是将优化器从SGD更改为Adam。SGD用相同的学习率更新所有参数,选择这个学习率是很棘手的。Adam为每个参数调整学习率,给出更新更频繁的参数较低的学习率和更新不频繁的参数较高的学习率。关于Adam(和其他优化器)的更多信息可以在这里找到。

将SGD更改为Adam,只需更改optim.SGD为optim.Adam即可,还请注意,我们不需要为Adam提供初始学习率,因为PyTorch指定了一个默认初始学习率。

import torch.optim as optim

optimizer = optim.Adam(model.parameters())
  • 1
  • 2
  • 3

训练模型的其余步骤保持不变。

我们定义criterion ,并将模型和criterion 放在GPU上(如果有的话)…

criterion = nn.BCEWithLogitsLoss()

model = model.to(device)
criterion = criterion.to(device)
  • 1
  • 2
  • 3
  • 4

我们实现了计算准确率的函数…


def binary_accuracy(preds, y):
    """
    Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8
    """

    #round predictions to the closest integer
    rounded_preds = torch.round(torch.sigmoid(preds))
    correct = (rounded_preds == y).float() #convert into float for division 
    acc = correct.sum() / len(correct)
    return acc
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我们定义一个函数来训练我们的模型。

因为我们已经设置了include_length = True,所以我们的batch.text现在是一个元组,元组的第一个元素是数字化的张量,第二个元素是每个序列的实际长度。在将它们传递给模型之前,我们将它们分离成它们自己的变量,text和text_length。

注意:因为我们现在使用的是dropout,所以我们必须记住使用model.train()来确保在训练时dropout是“打开的”。

def train(model, iterator, optimizer, criterion):
    
    epoch_loss = 0
    epoch_acc = 0
    
    model.train()
    
    for batch in iterator:
        
        optimizer.zero_grad()
        
        text, text_lengths = batch.text
        
        predictions = model(text, text_lengths).squeeze(1)
        
        loss = criterion(predictions, batch.label)
        
        acc = binary_accuracy(predictions, batch.label)
        
        loss.backward()
        
        optimizer.step()
        
        epoch_loss += loss.item()
        epoch_acc += acc.item()
        
    return epoch_loss / len(iterator), epoch_acc / len(iterator)
  • 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

然后我们定义一个函数来测试我们的模型,再次记住分离batch.text。

注意:因为我们现在正在使用dropout,所以必须记住使用model.eval()来确保在求值时dropout被“关闭”。

def evaluate(model, iterator, criterion):
    
    epoch_loss = 0
    epoch_acc = 0
    
    model.eval()
    
    with torch.no_grad():
    
        for batch in iterator:

            text, text_lengths = batch.text
            
            predictions = model(text, text_lengths).squeeze(1)
            
            loss = criterion(predictions, batch.label)
            
            acc = binary_accuracy(predictions, batch.label)

            epoch_loss += loss.item()
            epoch_acc += acc.item()
        
    return epoch_loss / len(iterator), epoch_acc / len(iterator)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

同时创建一个好的函数来告诉我们每个epoch要花多长时间。

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后,我们训练我们的模型…


N_EPOCHS = 5

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

    start_time = time.time()
    
    train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
    valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
    
    end_time = time.time()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut2-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: {valid_loss:.3f} |  Val. Acc: {valid_acc*100:.2f}%')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
Epoch: 01 | Epoch Time: 0m 38s
	Train Loss: 0.653 | Train Acc: 60.88%
	 Val. Loss: 0.742 |  Val. Acc: 49.60%
Epoch: 02 | Epoch Time: 0m 38s
	Train Loss: 0.546 | Train Acc: 72.31%
	 Val. Loss: 0.655 |  Val. Acc: 67.46%
Epoch: 03 | Epoch Time: 0m 38s
	Train Loss: 0.646 | Train Acc: 62.02%
	 Val. Loss: 0.707 |  Val. Acc: 52.84%
Epoch: 04 | Epoch Time: 0m 37s
	Train Loss: 0.595 | Train Acc: 68.16%
	 Val. Loss: 0.562 |  Val. Acc: 69.60%
Epoch: 05 | Epoch Time: 0m 38s
	Train Loss: 0.429 | Train Acc: 80.82%
	 Val. Loss: 0.318 |  Val. Acc: 87.10%
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

…并得到我们新的和极大改进的测试准确率!

model.load_state_dict(torch.load('tut2-model.pt'))

test_loss, test_acc = evaluate(model, test_iterator, criterion)

print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
  • 1
  • 2
  • 3
  • 4
  • 5
Test Loss: 0.332 | Test Acc: 86.54%
  • 1

用户输入

我们现在可以使用我们的模型来预测我们给出的任何句子的情感。因为它已经成在影评上训练成,所以我们提供的句子也应该是影评。

当使用模型进行推理时,它应该始终处于评估模式。如果本教程是逐步遵循的,那么它应该已经处于评估模式(在测试集中进行评估),但是我们显式地设置它以避免任何风险。

我们的predict_sentiment函数做以下几件事:

  • 将模型设置为评估模式
  • 将句子标记化,即把它从一个原始字符串分割成一个标记列表
  • 通过将这些标记从我们的词汇表中转换为它们的整数表示来索引它们
  • 获取序列的长度
  • 将索引(Python列表)转换为PyTorch张量
  • 通过使用unsqueezeing来添加batch维度
  • 把长度转换成一个张量
  • 使用sigmoid函数从0到1之间的实数中压缩输出预测
  • 使用item()方法将包含单个值的张量转换为整数

我们期望负面评论返回接近0的值,正面评论返回接近1的值。

import spacy

nlp = spacy.load('en_core_web_sm')

def predict_sentiment(model, sentence):
    model.eval()
    tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
    indexed = [TEXT.vocab.stoi[t] for t in tokenized]
    length = [len(indexed)]
    tensor = torch.LongTensor(indexed).to(device)
    tensor = tensor.unsqueeze(1)
    length_tensor = torch.LongTensor(length)
    prediction = torch.sigmoid(model(tensor, length_tensor))
    return prediction.item()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

一个负面评论的例子…

res = predict_sentiment(model, "This film is terrible")
print(res)
  • 1
  • 2
0.07018284499645233
  • 1

一个正面评价的例子…

predict_sentiment(model, "This film is great")
  • 1
0.9217998385429382
  • 1

完整代码

import torch
from torchtext import data
from torchtext import datasets

SEED = 1234

torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

TEXT = data.Field(tokenize='spacy', tokenizer_language='en_core_web_sm', include_lengths=True)
LABEL = data.LabelField(dtype=torch.float)

train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)

import random

train_data, valid_data = train_data.split(random_state = random.seed(SEED))

MAX_VOCAB_SIZE = 25_000

TEXT.build_vocab(train_data, max_size=MAX_VOCAB_SIZE, vectors='glove.6B.100d', unk_init=torch.Tensor.normal_)
LABEL.build_vocab(train_data)

BATCH_SIZE = 64

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
    (train_data, valid_data, test_data),
    batch_size=BATCH_SIZE,
    sort_within_batch=True,
    device=device
)

import torch.nn as nn

class RNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout, pad_idx):

        super(RNN, self).__init__()

        self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)

        self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=n_layers, bidirectional=bidirectional, dropout=dropout)

        self.fc = nn.Linear(hidden_dim * 2, output_dim)

        self.dropout = nn.Dropout(dropout)

    def forward(self, text, text_lengths):

        # text = [sent_len, batch_size]

        embedded = self.dropout(self.embedding(text))

        # embedded = [sent_len, batch_size, emb_dim]

        # pack sequence
        packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths)

        packed_output, (hidden, cell) = self.rnn(packed_embedded)

        # unpack sequence
        output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)

        # output = [sent_len, batch_size, hid_dim * num_directions]
        # output over padding tokens are zero tensors

        # hidden = [num_layers * num_directions, batch_size, hid_dim]
        # cell = [num_layers * num_directions, batch_size, hid_dim]

        # concat the final forward (hidden[-2, :, :]) and backward (hidden[-1, :, :]) hidden layers
        # and apply dropout
        hidden = self.dropout(torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1))

        # hidden = [batch_size, hid_dim * num_directions]

        return self.fc(hidden)

INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]

model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS, True, DROPOUT, PAD_IDX)

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')

pretrained_embeddings = TEXT.vocab.vectors
print(pretrained_embeddings.shape)

model.embedding.weight.data.copy_(pretrained_embeddings)

UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]

model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

print(model.embedding.weight.data)

import torch.optim as optim

optimizer = optim.Adam(model.parameters())

criterion = nn.BCEWithLogitsLoss()

model = model.to(device)
criterion = criterion.to(device)

def binary_accuracy(preds, y):
    """
    Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8
    """

    # round predictions to the closest integer
    rounded_preds = torch.round(torch.sigmoid(preds))
    correct = (rounded_preds == y).float() # convert into float for division
    acc = correct.sum() / len(correct)
    return acc

def train(model, iterator, optimizer, criterion):

    epoch_loss = 0
    epoch_acc = 0

    model.train()

    for batch in iterator:

        optimizer.zero_grad()

        text, text_lengths = batch.text

        predictions = model(text, text_lengths).squeeze(1)

        loss = criterion(predictions, batch.label)

        acc = binary_accuracy(predictions, batch.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 batch in iterator:

            text, text_lengths = batch.text

            predictions = model(text, text_lengths).squeeze(1)

            loss = criterion(predictions, batch.label)

            acc = binary_accuracy(predictions, batch.label)

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

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

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 = 5

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

    start_time = time.time()

    train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
    valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)

    end_time = time.time()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut2-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: {valid_loss:.3f} |  Val. Acc: {valid_acc * 100:.2f}%')

model.load_state_dict(torch.load('tut2-model.pt'))

test_loss, test_acc = evaluate(model, test_iterator, criterion)

print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')

import spacy

nlp = spacy.load('en_core_web_sm')

def predict_sentiment(model, sentence):
    model.eval()
    tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
    indexed = [TEXT.vocab.stoi[t] for t in tokenized]
    length = [len(indexed)]
    tensor = torch.LongTensor(indexed).to(device)
    tensor = tensor.unsqueeze(1)
    length_tensor = torch.LongTensor(length)
    prediction = torch.sigmoid(model(tensor, length_tensor))
    return prediction.item()


res = predict_sentiment(model, "This film is terrible")
print(res)
res = predict_sentiment(model, "This film is great")
print(res)


























  • 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
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262

后续行动

我们现在已经为电影评论建立了一个像样的情感分析模型!在下一篇笔记中,我们将实现一个模型,它可以用更少的参数获得相当的精度,而且训练速度更快。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/357018
推荐阅读
相关标签
  

闽ICP备14008679号