当前位置:   article > 正文

自然语言处理---情感分析(1)【baseline 从RNN开始】_自然语言情感处理

自然语言情感处理

情感分析 baseline

  • 数据集 – 电影评论数据集:IMDb数据集 ----- 数据由评论的原始字符串和情感组成,“pos”表示积极情绪,“neg”表示消极情绪。
  • 模型:RNN(默认有基础)[0时,表示预测为负面情绪, 类似分类问题]

h t = RNN ( x t , h t − 1 ) h_t = \text{RNN}(x_t, h_{t-1}) ht=RNN(xt,ht1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKlgGZVV-1631697478974)(./assets/sentiment1.png)]

TorchText 的主要概念之一是Field,这些定义了应如何处理数据, TEXT 【tokenize=‘spacy’】字段来定义应该如何处理评论,并使用 LABEL 字段来处理情绪

如果没有设置 tokenize 参数,默认值是使用空格拆分字符串。我们还需要指定一个 tokenizer_language 来告诉 torchtext 使用哪个 spaCy 模型。我们使用 en_core_web_sm 模型。

下载en_core_web_sm 模型的方法: python -m spacy download en_core_web_sm

LABELLabelField 定义,它是专门用于处理标签的 Field 类的特殊子集。

TorchText 的另一个方便的功能是它支持自然语言处理 (NLP) 中使用的常见数据集。

数据处理 — 机器可以理解训练

import torch
from torchtext.legacy import data

# 设置随机种子数,该数可以保证随机数是可重复的
SEED = 1234

# 设置种子
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

# 读取数据和标签
TEXT = data.Field(tokenize = 'spacy', tokenizer_language = 'en_core_web_sm')
LABEL = data.LabelField(dtype = torch.float)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
# 自动下载 IMDb 数据集并将其拆分为规范的训练集和测试集
from torchtext.legacy import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
  • 1
  • 2
  • 3
# view
print(f'Number of training examples: {len(train_data)}')
print(f'Number of testing examples: {len(test_data)}')
print(vars(train_data.examples[0]))
  • 1
  • 2
  • 3
  • 4
Number of training examples: 25000
Number of testing examples: 25000
  • 1
  • 2
{'text': ['elvira', 'mistress', 'of', 'the', 'dark', 'is', 'one', 'of', 'my', 'fav', 'movies', ',', 'it', 'has', 'every', 'thing', 'you', 'would', 'want', 'in', 'a', 'film', ',', 'like', 'great', 'one', 'liners', ',', 'sexy', 'star', 'and', 'a', 'Outrageous', 'story', '!', 'if', 'you', 'have', 'not', 'seen', 'it', ',', 'you', 'are', 'missing', 'out', 'on', 'one', 'of', 'the', 'greatest', 'films', 'made', '.', 'i', 'ca', "n't", 'wait', 'till', 'her', 'new', 'movie', 'comes', 'out', '!'], 'label': 'pos'}
  • 1
import random
train_data, valid_data = train_data.split(split_ratio=0.8 , random_state = random.seed(SEED))
  • 1
  • 2

须构建一个 词汇表。 这是一个查找表,其中数据集中的每个单词都有唯一对应的 index(整数)

每个 index 用于为每个词构造一个 one-hot 向量,通常用 VV 表示。

优化one-hot向量

取前n个出现次数最多的单词作为one-hot的基

忽略出现次数小于m个的单词

<unk>来编码未编码单词

MAX_VOCAB_SIZE = 25000

TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE)
LABEL.build_vocab(train_data)
  • 1
  • 2
  • 3
  • 4
print(f"Unique tokens in TEXT vocabulary: {len(TEXT.vocab)}")
print(f"Unique tokens in LABEL vocabulary: {len(LABEL.vocab)}")
# 额外token ---- <unk>和<pad>
  • 1
  • 2
  • 3

我们一次输入一个batch,并且批次中的所有句子都需要具有相同的长度

print(TEXT.vocab.freqs.most_common(20))
print(TEXT.vocab.itos[:10])
  • 1
  • 2
迭代器 ---- 喂batch

创建验证集,测试集,以及训练集的迭代器, 每一次的迭代都会返回一个batch的数据。

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,
    device = device)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

模型构建

创建RNN ----- nn.module的子类。

三层模型分别是嵌入层,RNN层,最后还有全连接层

  • embedding层将稀疏的one-hot转换为密集嵌入到空间的向量
  • RNN层接受之前的状态ht−1ht−1和当前输入对应的密集嵌入向量, 这两部分用来计算下一层的隐藏层状态, htht.
  • 最后一层线性层就会得到RNN输出的最后一层隐藏层状态

squeeze 方法, 可以消除维度为1的维度

import torch.nn as nn

class RNN(nn.Module):
    def __init__(self, input_dim, embedding_dim, hidden_dim, output_dim):
        
        super().__init__()
        
        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))
  • 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
# 设置
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1

model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
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 2,592,105 trainable parameters

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

训练模型

import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=1e-3)
  • 1
  • 2
# BCEWithLogitsLoss一般用来做二分类
criterion = nn.BCEWithLogitsLoss()
  • 1
  • 2
# GPU 适用
model = model.to(device)
criterion = criterion.to(device)
  • 1
  • 2
  • 3
# 将sigmoid层输出的预测结果输入到计算准确率的函数, 取整到最近的整数.大于0.5,就取1。反之取0。计算出预测的结果和label一致的值,在除以所有的值,就可以得到准确率。
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

train 函数迭代所有的样本,每次都是一个batch。

model.train() 将model处于 “training 模式”, 也会打开 dropoutbatch normalization. 在每一次的batch, 先将梯度清0. 模型的每一个参数都有一个 grad 属性, 存储着损失函数计算的梯度值. PyTorch 不会自动删除(或“归零”)从上次梯度计算中计算出的梯度,因此必须手动将其归零。

每次输入, batch.text, 到模型中. 只需要调用模型即可.

loss.backward()计算梯度,更新参数使用的是 optimizer.step()

损失值和准确率在整个 epoch 中累积, .item()抽取张量中只含有一个值的张量中的值。

最后,我们返回损失和准确率,在整个 epoch 中取平均值. len可以得到epoch中的batch数

当然在计算的时候,要记得将LongTensor转化为 torch.float。这是因为 TorchText 默认将张量设置为 LongTensor

def train(model, iterator, optimizer, criterion):
    
    epoch_loss = 0
    epoch_acc = 0
    
    model.train()
    
    for batch in iterator:
        
        optimizer.zero_grad()
                
        predictions = model(batch.text).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

evaluatetrain相似, 只要将train函数稍微进行修改即可。

model.eval() 将模型置于"evaluation 模式", 这会关掉 dropoutbatch normalization.

with no_grad() 下,不会进行梯度计算. 这会导致使用更少的内存并加快计算速度.

其他函数在train中类似,在evaluate中移除了 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
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(), '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}%')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
# 结果
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}%')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

next work

  • 压缩填充张量
  • 预训练的词嵌入
  • 不同的 RNN 架构
  • 双向 RNN
  • 多层 RNN
  • 正则化
  • 不同的优化器
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/375284
推荐阅读
相关标签
  

闽ICP备14008679号