最近开始着手毕设了,本来计划是先读懂一篇论文,复现(其实是跑通并理解)其代码,用作demo,后续在此基础上进行改进。我读的论文是 DialogueRNN An Attentive RNN for Emotion Detection in Conversations,在paper with code上有官方高赞代码。本来看起来应该很不错的一个计划,直到我打开了代码,发现自己很多地方并不理解,加之也没有注释,意识到这样的代码不适合入门。在知乎看到一个回答下,对于读论文的流程讲得挺有道理的,读一篇论文,首先读懂公式,然后是文章的重点,之后跑通代码,并理解其亮点部分。读论文的目的是找idea,而不是让论文叫你手把手入门。遂转向github 情感分析入门教程:pytorch-sentiment-analysis。话不多说,今天在这里分享一下Lesson 1 Simple Sentiment Analysis的内容。

  任务是使用电影评论数据集IMDb dataset,分析句子的情感是positive or negative的二分类问题。这里只是用简单的方法来了解的流程,效果的提升优化会在之后的章节讨论。

Introduction

  我们使用通常用于分析时序数据的 recurrent neural network(RNN)。RNN一次输入一组词X={

snownlp情感 训练 情感教程_自然语言处理

},并且对于每个词都会输出一个隐藏状态h。我们通过输入当前单词

snownlp情感 训练 情感教程_python_02

和之前单词的隐藏状态

snownlp情感 训练 情感教程_人工智能_03

,循环使用RNN,来产生下一个隐藏状态

snownlp情感 训练 情感教程_python_04

。我们把最后的隐藏状态

snownlp情感 训练 情感教程_深度学习_05

输入线性层 f,来得到我们预测的情感

snownlp情感 训练 情感教程_python_06


如下所示是一个例句,RNN的预测值为0,意味着negetive情绪。请注意,我们对于每个单词使用的是相同的RNN,意味着有相同的参数。

snownlp情感 训练 情感教程_python_07

 Preparing Data

1.Field:数据应该被怎样处理

Text field:review应该被怎样处理;Label field : sentiment 应该被怎样处理

2.使用"en_core_web_sm"model前,需要先下载python -m spacy download en_core_web_sm

3.dtype=torch.float :TorchText默认设置tensors的类型为LongTensor,但是我们的criterion函数期望输入为FloatTensor。

import torch
from torchtext.legacy import data

#set the random seeds for reproducibility
SEED = 1234

torch.manual_seed(SEED)
#每次返回的卷积算法是固定的,即默认值
torch.backends.cudnn.deterministic = True

#使用spacy分词
#tokenizer_language指定为spacy model

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

划分训练集和测试集

from torchtext.legacy import datasets

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

#print(f'Number of training examples: {len(train_data)}')
#print(f'Number of testing examples: {len(test_data)}')
#check an example
#print(vars(train_data.examples[0]))

划分训练集和验证集

#random_sate = random.seed(SEED): 确保每次能得到相同的训练集和验证集的划分

import random

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

#print(f'Number of training examples: {len(train_data)}')
#print(f'Number of validation examples: {len(valid_data)}')
#print(f'Number of testing examples: {len(test_data)}')

构建词典:数据集中每一个单词的索引表。我们把每个单词构建为一个独热编码(one-hot vector)。独热编码的维度是词典中总的单词的个数。我们训练集中的单词超过100,000个,意味着我们的独热编码超过了100,000维。

有两种方式可以有效减小我们的词典大小,我们可以取出top n个最常出现的单词,也可以忽视出现次数少于m次的单词。我们这里采用前者,只保留top 25,000个单词。对于那些出现在样本中,但是已经被从词典删去的单词,我们用 unkown or sepecial token 代替。

MAX_VOCAB_SIZE = 25_000

TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE)
LABEL.build_vocab(train_data)

#print(f"Unique tokens in TEXT vocabulary: {len(TEXT.vocab)}")
#print(f"Unique tokens in LABEL vocabulary: {len(LABEL.vocab)}")

我们这里只用训练集构造词典。Because when testing any machine learning system you do not want to look at the test set in any way. We do not include the validation set as we want it to reflect the test case as mush as possible.

数据准备的第一步是构造iterators。每次迭代会返回一个batch的数据(indexed and converted into tensors)。

因为一个batch中的所有句子都需要是相同长度,所以短于最长长度的句子会被padded。这里我们使用了BucketIterator。他返回的每个example的长度相似,是每个example的填充最小化。

batch的作用:批量梯度下降(BGD),计算所有样本损失的累加和,更新一次梯度;随机梯度下降(SGD),随机选取一个样本,计算损失,更新一次梯度;小批量梯度下降,选择一批样本,计算损失和,更新一次梯度。

BATCH_SIZE = 64
#使用GPU
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,
    device = device)

Build the Model

我们在__init__方法中定义我们module的layers。我们的三个layers是:an embedding layer,our RNN, and a linear layer。嵌入层用于将稀疏的独热向量(稀疏是因为大多数元素都是0)转换为密集的嵌入向量(密集是因为维度小得多,所有元素都是实数)。除了降低RNN输入的维度外,还有一种理论认为,在这个密集的向量空间中,对review的情绪有相似影响的词距离很近( are mapped close together)。

snownlp情感 训练 情感教程_python_08

 最后,将最终的隐藏状态

snownlp情感 训练 情感教程_深度学习_05

输入全连接层。

text :[sentence length, batch size].是一批句子,句子中的每个词都被转换为了one-hot vector。PyTorch方便地将一个单热向量存储为它的索引值。即表示一个句子的张量只是该句子中每个标记的索引张量。

The input batch通过the embedding layer得到embedded,这为我们提供了句子的密集向量表示。

embedded 输入RNN ,返回两个tensor。output 是来自每个time step的 the hidden state 的拼接, 而hidden只是the final hidden state.我们使用assert语句来验证这一点。assert检验断言是否为真,若断言为假,则返回断言错误并停止运行程序。

最后,我们将最后的隐藏状态hidden 输入线性层

snownlp情感 训练 情感教程_人工智能_10

,来生成预测。

import torch.nn as nn

class RNN(nn.Module):
    def __init__(self, input_dim, embedding_dim, hidden_dim, output_dim):
        
        super().__init__()
        #输入为input_dim,输出位embedding_dim
        self.embedding = nn.Embedding(input_dim, embedding_dim)
        
        self.rnn = nn.RNN(embedding_dim, hidden_dim)
        
        self.fc = nn.Linear(hidden_dim, output_dim)
        
    def forward(self, text):

        #text = [sent len, batch size]
        
        embedded = self.embedding(text)
        
        #embedded = [sent len, batch size, emb dim]
        
        output, hidden = self.rnn(embedded)
        
        #output = [sent len, batch size, hid dim]
        #hidden = [1, batch size, hid dim]
        
        assert torch.equal(output[-1,:,:], hidden.squeeze(0))
        
        return self.fc(hidden.squeeze(0))

RNN实例化

INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1

model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM)

Train the Model

The BCEWithLogitsLoss criterion carries out both the sigmoid and the binary cross entropy steps.

#create an optimizer
import torch.optim as optim

optimizer = optim.SGD(model.parameters(), lr=1e-3)

criterion = nn.BCEWithLogitsLoss()
#place the model and the criterion on the GPU
model = model.to(device)
criterion = criterion.to(device)

我们用binary_accuracy函数计算准确率。首先使用sigmoid函数将predictions的值挤压到0~1,之后对其进行四舍五入。这个四舍五入的值会将0.5~1转化为1(positive)

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:
        
        #zero the gradients
        optimizer.zero_grad()
        
        #predictions=[batch size,1]
        predictions = model(batch.text).squeeze(1)

        #the loss being averaged over all examples in the batch.
        loss = criterion(predictions, batch.label)
        
        acc = binary_accuracy(predictions, batch.label)
        
        #calculate the gradient of each parameter
        loss.backward()
        
        #update the parameters
        optimizer.step()
        
        #.item() method is used to extract a scalar from a tensor
        epoch_loss += loss.item()
        epoch_acc += acc.item()
        
    return epoch_loss / len(iterator), epoch_acc / len(iterator)

evaluate和train相似,唯一的区别是不需要更新参数。PyTorch在with no_grad()块内的不会计算梯度。evaluate函数剩下的部分和train相同, 只是去除了 optimizer.zero_grad()loss.backward() and optimizer.step(), ,因为我们在计算时不更新模型的参数。

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

            predictions = model(batch.text).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

在每个epoch,如果valid loss是我们迄今为止看到的最好的,我们将保存模型的参数,然后在训练结束后,我们将在测试集中使用该模型。

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

test loss and acc

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

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

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