2.GitHub pytorch sentiment analysis(进阶版)

本文介绍了在PyTorch中进行更高级的情感分析,包括使用预训练词向量、LSTM、双向RNN、多层RNN以及正则化等技术。通过训练模型,最终实现对电影评论的情感分类,并提供了一个预测函数。
摘要由CSDN通过智能技术生成

Updated Sentiment Analysis

在前一章,我们做了基础的情感分析,在这一章,我们会得到一个更好的分类结果

我们会使用

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

1.准备数据

我们会使用 packed padded sequences,这会使得我们的RNN只会处理那些非padded的元素,对于那些padded(截长)的部分,都会变成0.为了将每个样本截长补短,需要设置include_lengths = True

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', include_lengths = True) # include_lengths = True,为了之后对每个句子设置固定长度
LABEL = data.LabelField(dtype = torch.float)

加载数据

from torchtext import datasets

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

从训练集中分一部分出来做验证集

import random

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

2.词向量

我们使用预先训练好的词向量,我们获得这些词向量是通过指定将参数传进build_vocab
TorchText会自动下载,并将词与词向量联系起来

这里我们使用的是"glove.6B.100d",glove是个算法,用来计算词向量的.6B意味着,词向量是在60亿tokens上训练出来的.100d意味着,词向量是100维的
还有其他的:

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)

charngram.100d
fasttext.en.300d
fasttext.simple.300d
glove.42B.300d
glove.840B.300d
glove.twitter.27B.25d
glove.twitter.27B.50d
glove.twitter.27B.100d
glove.twitter.27B.200d
glove.6B.50d
glove.6B.100d
glove.6B.200d
glove.6B.300d

训练好的词向量,语义相近的词在向量空间里靠的近.比如terrible和awful,dreadful就很近
这样就能给我们的嵌入层embedding layer一个好的初始化条件.就不需要从零开始学这些关系了
(注意,这个词向量有800多兆)

3.创建迭代器+使用GPU


BATCH_SIZE = 64

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建迭代器,将数据集一个batch一个batch往模型中送
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)

4. 构建模型

LSTM: 这里介绍另一种RNN结构,LSTM. 因为标准的RNN有梯度消失的问题,LSTM克服了这一点.通过加入一个recurrent state叫做cell state,存储的是远期记忆,hidden state存储的是近期记忆
在这里插入图片描述
Bidirectional RNN: 双向RNN:单向的RNN好理解,就是将分词后的词向量一个一个的往模型里面输入,而双向就是多加了一个从右到左的一条输入
在这里插入图片描述
Multi-layer RNN:多层RNN可以被视为深度RNN,就是在最开始的标准RNN上多加几层RNN,
在这里插入图片描述
正则化Regularization
前几个改进模型能提升分类器的性能.但是加了许多要训练的参数,避免过度你和.要使用正则化,惩罚项.
或是放弃某些节点,dropout.:前向传播的时候,随机删除一层里的部分神经元

实施细节
1.对于每个样本里补短后加上的pad token,模型是不应该对其进行训练的.我们要显式的告诉模型,padding token跟句子的情感是无关的.这就意味着pad token的嵌入层(词向量)会一直保持初始化的样子,都是0. 如何做到?是通过往nn.Embedding 层传入pad token 的index索引,作为padding_idx 参数

2.使用LSTM而不是RNN,我们用nn.LSTM,要注意的是,LST吗返回的是output还有(final hidden state,final cell state)元组
cell state 就是LSTM特有的保留上下文记忆(长期记忆)用的

3.因为LSTM的final hidden state包含了前向和后向传播的部分,并且拼接在一起了.那么,下一层nn.Linear 层的输入的形状就是隐藏层维度形状的两倍.

4.在将embeddings(词向量)传入RNN前,我们需要将它们打包nn.utils.rnn.packed_padded_sequence这样,我们的RNN就只会处理那些不是pad的token.然后RNN就会输出packed_output (一个被打包的句子)以及hidden sate 和 cell state. 如果没有packed padded sentences, 输出的hidden state和cell state是句子最后一个元素的,那样大概率就会使pad token,如果使用packed padded sentences,输出的就会是最后一个非padded元素的hidden state 和 cell state

5.然后我们将输出的句子解压,还是用的nn.utils.rnn.pad_packed_sequence 来讲其转换成一个tensor张量,padding tokens的输出就只会是元素是零的张量,通常我们会对输出解压只在模型之后需要它的时候.尽管,在我们这个案例中,不需要,我们任然会unpack句子来展示他的步骤

6.final hidden sate,也就是hidden,形状是 [num layers * num directions, batch size, hid dim].以为隐含层里的神经元参数的顺序是这样的 : 前向,后向,前向,后向…

[forward_layer_0, backward_layer_0, forward_layer_1, backward_layer 1, …, forward_layer_n, backward_layer n].

因为我们只要最后的前向和后向传播的hidden states,我们只要最后2个hidden layers就行hidden[-2,:,:]hidden[-1,:,:]
然后将他们合并在一起,再传入线性连接层linear layer

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__()
        # embedding,嵌入层,(词向量)
         self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx)
            
        # RNN的改进型LSTM
        self.rnn = nn.LSTM(embedding_dim, # input_size
                          hidden_dim, #output_size
                          num_layers=n_layers, # 几层
                          bidirectional=bidirectional, #是否双向
                          dropout=dropout) #随机去除神经元
        # 线性连接层
        self.fc = nn.Linear(hidden_dim * 2, output_dim) # 因为前向传播+后向传播有两个hidden sate,且合并在一起,所以乘以2
        
        # 随机去除神经元
        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中的 padding tokens是数值为0的张量
        
        #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)

5. 实例化模型+传入参数

为了保证pre-trained 词向量可以加载到模型中,EMBEDDING_DIM 必须等于预训练的GloVe词向量的大小

INPUT_DIM = len(TEXT.vocab) # 250002: 之前设置的只取25000个最频繁的词,加上pad_token和unknown token
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] #指定参数,就这样定义pad_token的index索引值,才能让模型不管pad token

model = RNN(INPUT_DIM, 
            EMBEDDING_DIM, 
            HIDDEN_DIM, 
            OUTPUT_DIM, 
            N_LAYERS, 
            BIDIRECTIONAL, 
            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')

The model has 4,810,857 trainable parameters

接下来就是将前面加载好的预训练词向量复制进我们模型中的embedding嵌入层,用预训练的embeddings词向量替换掉原来模型初始化的权重参数


pretrained_embeddings = TEXT.vocab.vectors
print(pretrained_embeddings.shape) #检查下词向量的形状 [vocab size, embedding dim]
# 用预训练的embeddings词向量替换掉原来模型初始化的权重参数
model.embedding.weight.data.copy_(pretrained_embeddings)

因为我们的<unk> 和<pad>token不在预训练词表里,他们已经在构建我们自己的词表时,初始化了.所以最好显式的告诉模型,将他们变为0,它们与情感无关.

我们是通过手动设置他们的词向量权重为0的

#将unknown 和padding token设置为0
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)

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]])

可以看到头两行,值都是0, pad token的词向量在模型训练过程中始终不会被学习,而unknown token的词向量是会被学习的

6.训练模型

我们将随机梯度下降SGD优化器改成Adam优化器.SGD优化器对所有的训练参数一视同仁,都采用我们设定好的学习率进行同步更新.
而Adam会为每个训练参数调整学习率,从而使更新频率搞得参数有较低的学习率,更新频率较低的参数有较高的学习率

6.1 设置优化器


import torch.optim as optim

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

6.2 设置损失函数,和GPU


criterion = nn.BCEWithLogitsLoss() # 损失函数. criterion 在本例中时损失函数的意思

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

6.3 计算精确度

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

6.3 定义一个训练函数,用来训练模型

因为用到了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    #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)

6.4 定义一个测试函数

因为现在模型中用了dropouput,所以在评估模型时要用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  #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)

再创建一个函数告诉我们epochs训练多长时间

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

6.5 正式训练模型

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}%')

Epoch: 01 | Epoch Time: 0m 28s
Train Loss: 0.439 | Train Acc: 80.32%
Val. Loss: 0.368 | Val. Acc: 84.31%
Epoch: 02 | Epoch Time: 0m 27s
Train Loss: 0.335 | Train Acc: 86.26%
Val. Loss: 0.417 | Val. Acc: 81.54%
Epoch: 03 | Epoch Time: 0m 27s
Train Loss: 0.266 | Train Acc: 89.55%
Val. Loss: 0.366 | Val. Acc: 86.37%
Epoch: 04 | Epoch Time: 0m 27s
Train Loss: 0.229 | Train Acc: 91.12%
Val. Loss: 0.289 | Val. Acc: 88.57%
Epoch: 05 | Epoch Time: 0m 27s
Train Loss: 0.200 | Train Acc: 92.59%
Val. Loss: 0.261 | Val. Acc: 89.76%

6.6 最终测试结果

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}%')

Test Loss: 0.274 | Test Acc: 88.78%

7. 使用训练好的模型来预测

现在可以这个模型来给一些真实的电影评论来做分类了
当使用一个模型用来实际预测时,模型要始终在evaluation mode评估模式

7.1 创建predict_sentiment函数

这个函数做了如下些事:

  • 设置模型到evaluation mode
  • 将句子分词
  • 将分词后的每个词,对应着词汇表,转换成对应的index索引,
  • 获取句子的长度
  • 将indexes,从list转化成tensor
  • 通过unsqueezing 添加一个batch维度
  • 将length转化成张量tensor
  • 用sigmoid函数将预测值压缩到0-1之间
  • 用item()方法,将只有一个值的张量tensor转化成整数

负面评论接近0,正面评论接近1

import spacy
nlp = spacy.load('en')

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()
predict_sentiment(model, "This film is terrible")

0.005683214403688908

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值