情感分析学习笔记-Task02

代码在下边已经附上,每句代码的具体意思,也写在上边,没有注释的可能是在task01里写过了,这里就不再重复赘述。因为我自己电脑配置的原因,我将原先的迭代次数5次,改为了1次,原先的hidden_dim=256改成了10,这样跑起来就快多了,如果要调试的话,你可以先试试能不能跑得通,再修改参数。这里我把最后一个predict_sentiment(model, “This film is terrible”)这句代码理解写出来,理解不到位的地方,还请指正。
我们传入IMDB数据集训练好的模型,和我们要预测情感极性的距离,首先程序会到def predict_sentiment(model, sentence):这里,这里就是将,句子里的每一各单词拿出来,对应建立好的字典中,返回的是这个单词对应字典里的下标索引,tensor.unsqueeze(1)这句话会添加一个batch维度,返回size(4,1)的tensor,然后将这个tensor和length_tensor传入我们模型里,模型就是先embedding,把4个次分别进行编码,一个词用100维的向量来表示。接着把编码后的向量输入到RNN里,因为我们只需要最后的向前和向后传播的hidden states,我们只要最后2个hidden layers就行,所以,我们拼接torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1)。最后传入线性层,加个softmax.
调试截图

# -*- codeing = utf-8 -*-
# @Time : 9/15/2021 3:36 PM
# @Author: 陈卓
# @File: task02.py
# @Software:PyCharm
import torch
from torchtext.legacy import data
# 首先设置seed,并将其分类训练、测试验证集。
# 在准备数据的时候需要注意到,由于RNN只能处理序列中的非padded元素(即非数据),
# 对于任何padded元素输出都是O。所以注意到钱们在准备数蹈的时候将include_length设置为True,以获得句子的实际长度,后续需要使用。
SEED = 1234
torch.manual_seed(SEED)
torch.backends.deterministic = True
TEXT = data.Field(tokenize = 'spacy',
                  tokenizer_language = 'en_core_web_sm',
                  include_lengths = True)
LABEL = data.LabelField(dtype = torch.float)

# 加载 IMDb数据集
from torchtext.legacy import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)

# vars() 函数返回对象object的属性和属性值的字典对象。
# print(vars(train_data.examples[0]))

# 从训练集中选取部分做验证集
import random
train_data, valid_data = train_data.split(random_state = random.seed(SEED))

# 词向量

# 接下来,使用预训练词向量进行初始化操作,其中获取这些词向量是通过指定参数传递给 build_vocab 得到的。
# 在这里,我们选取GloVe词向量,GloVe的全称是:Global Vectors for Word Representation。
# 这里我们使用的是 "glove.6B.100d" ,其中,6B表示词向量是在60亿规模的tokens上训练得到的,
# 100d表示词向量是100维的(注意,这个词向量有800多兆)
# 理论上,这些预训练词向量在词嵌入向量空间中的距离在一定程度上表征了词之间的语义关系,
# 例如,“terrible”、“awful”、“dreadful” ,它们的词嵌入向量空间的距离会非常近。
MAX_VOCAB_SIZE = 25000
# 表示从预训练的词向量中,将当前训练数据中的词汇的词向量抽取出来,构成当前训练集的 Vocab(词汇表)。
# 对于当前词向量语料库中没有出现的单词(记为UNK,unknown),通过高斯分布随机初始化(unk_init = torch.Tensor.normal_)。
TEXT.build_vocab(train_data,
                 max_size = MAX_VOCAB_SIZE,
                 vectors = "glove.6B.100d",
                 unk_init = torch.Tensor.normal_)
LABEL.build_vocab(train_data)

# 创建迭代器+选取GPU
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,
    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().__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]

        # 在将embeddings(词向量)输入RNN前,我们需要借助nn.utils.rnm.packed_padded_sequence将它们'打包',
        # 以此来保证RNN只会处理不是pad的token。
        # 我们得到的输出包括packed_output(a packed sequence)以及hidden sate和cell state。
        # 如果没有进行打包操作,那么输出的hidden state和cell state大概率是来自的子的pad token。
        # 如果使用packed padded sentences,输出的就会是最后一个非padded元素的hidden state和cell state。

        # pack sequence
        # lengths need to be on CPU!
        packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths.to('cpu'))

        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)

# 实例化模型+传入参数
# 为了保证pre_trained词向量可以加载到模型中,EMBEDDING_DIM必须等于预训练的GloVe词向量的大小。
INPUT_DIM = len(TEXT.vocab) # 250002: 之前设置的只取25000个最频繁的词,加上pad_token和unknown token
EMBEDDING_DIM = 100
HIDDEN_DIM = 10
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')

# 接下来,把前面加载好的预训练词向量复制进我们模型中的embedding嵌入层,
# 用预训练的embeddings词向量替换掉原来模型初始化的权重参数。
# 我们从字段的vocab中检索嵌入,并检查它们的大小是否正确,[vocab size, embedding dim]
pretrained_embeddings = TEXT.vocab.vectors
# 检查词向量形状 [vocab size, embedding dim]
print(pretrained_embeddings.shape)

# 用预训练的embedding词向量替换原始模型初始化的权重参数
model.embedding.weight.data.copy_(pretrained_embeddings)



# 因为我们的<unk>和<pad> token不在预训练词表里,它们已经在构建我们自己的词表时使用unk_init (an N(O, 1) distrlution)初始化了。
# 所以,最好显式地告诉模型,将它们初始化变为0,它们与情感无关。
# 我们是通过手动设置他们的词向量权重为0的。
# 注意:与初始化嵌入一样,这应该在"weight.data"而不是"weight"上完成!

#将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)
# 我们现在可以看到嵌入权重矩阵的前两行值都是0,需要注意的是,
# pad token的词向量在模型训练过程中始终不会被学习。而unkonown tolen的铜向量是会被学习的,


# 训练模型
import torch.optim as optim
# 我们是将随机梯度下降优化器从SGD'更改为Adam'。SGD使用我们定的学习率同步更新所有参数,而Adam会调整每个参茹的学习率,
# 给出更新频率更高的参数,以及更新频率更低的参数和更新频率不高的参数。有关"Adam”(和其他优化器)的更多信息。
# 要将SGD'更改为'Adam',我们只需将optim.SGD'更改为optim.Adam',还要注意,我们不提供Adam初始学习率,因为PyTorch提供了默认的贻学习率。

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

# 设置损失函数和GPU
criterion = nn.BCEWithLogitsLoss() # 损失函数. criterion 在本task中时损失函数的意思
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

# 定义一个训练函数,用来训练模型
# 正如我们设置的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 ()将model处于 "training模式",也会打开dropout和 batch normalization.
    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)

# 定义一个测试函数
# 注意:因为现在使用的是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  # 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

# 正式训练模型
N_EPOCHS = 1

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

# 模型验证
# 我们现在可以使用这个模型来预测我们给出的任何句子的情感了,注意需要提供的句子是电影评论方面的。
# 当使用模型进行实际预测时,模型要始终在evaluation mode评估模式.

import spacy
nlp = spacy.load('en_core_web_sm')
# "predict_sentiment"函数的作用如下:
#     将模型切换为evaluate模式·对句子进行分词操作
#     将分词后的每个词,对应着词汇表,转换成对应的index索引,获取句子的长度
#     将indexes,从list转化成tensor
#     通过unsqueezing添加一个batch维度将length转化成张量tensor
#     用sigmoid函数将预测值压缩到0-1之间
#     用item ()方法,将只有一个值的张量tensor转化成整数负面评论返回接近0的值,正面评论返回接近1的值。
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")
# predict_sentiment(model, "This film is great")

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值