情感分析学习笔记-Task01

Task01,由于我们要弄清楚情感分析的大概流程,所以我们用最简单的模型来理解,我们使用IMDB数据集(IMDb数据集包含50000条电影评论,每条评论都标记为正面或负面评论)下面就边上代码边理解。

这里面en_core_web_sm是一个NLP的语言模型,spaCy使用的语言模型是预先训练的统计模型,能够预测语言特征,对于英语,共有en_core_web_sm、en_core_web_md和en_core_web_lg三种语言模型,sm/md/lg为描述大小的缩写:small(小)、medium(中)、large(大),具体可以参考:https://www.cnblogs.com/ljhdo/archive/2019/05/13/10762035.html

import torch
from torchtext.legacy import data
# 设置随机种子数,该数可以保证随机数是可重复的
SEED = 1234
# 设置种子
torch.manual_seed(SEED)
torch.backends.deterministic = True
# 读取数据和标签
TEXT = data.Field(tokenize = 'spacy', tokenizer_language = 'en_core_web_sm')
LABEL = data.LabelField(dtype = torch.float)
from torchtext.legacy import datasets
# 以下代码自动下载IMDb数据集并将其拆分为规范的训练集和测击集,
# 作为torchtext.datasets对象。它使用我们之前定义的Frields处理数据。IMDb数据集包含50000条电影评论,每条评论都标记为正面或负面评论。
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)}')
# Number of training examples: 25000
# Number of testing examples: 25000
# 查看一下示例数据
print(vars(train_data.examples[0]))
# {'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'}
import random
# IMDb数据集划分了训练集和测试集,这里我们还需要创建一个验证集。可以使用.split()方法来做。
# 默认情况下,数据将按照BO%的比例划分为训陈集和验证集,可以通过设置split_ratio参数来设置训陈集和验证集的比例,即split_ratio为0.8意味着80%的示例构成训练集,20%构成验证集。
# 这里还需要将我们之前设置的随机种子SEED传递给random state参数,确保我们每次获得相同的训练集和验证集。
train_data, valid_data = train_data.split(split_ratio=0.8 , 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)}')
# Number of training examples: 20000
# Number of validation examples: 5000
# Number of testing examples: 25000

接下来,我们必须构建一个词汇表。这是一个查找表,其中数据集中的每个单词都有唯一对应的index(整数)。
我们这样做是因为我们的模型不能对疗符申进行操作,只能对数字进行操作。每个index用于为每个词构造一个one-hot向量。
one-hot

# 我们训练集中不同的单词数超过100000,这意味着我们的one-hot向量超过100,000维,这将大大延长训练时间,甚至不适合在本地运行。
# 有两种方法可以优化我们的one-hot向量,一种是可以只取前n个出现次数最多的单词作为one-hot的基,另一种是忽略出现次数小于m的单词。
# 在本例中,我们使用第一种杭法:使用25,000个最常见的单词作为one-hot编码。
# 这就会出现一个问题:有些单词在数据集中出现了,但却无法直接进行one ho编码。这里我们使用一个特别的<unl>来编码它们。
# 举个例子,如果我们的句子是"This fim isgreat and l love it"”,但是单词"love"不在词汇表中,那么我们将这句话转换成:
# “This film is great and I<unk> it "。下面我们构建词汇表,只保留最常见的max_size标记。
MAX_VOCAB_SIZE = 25000
# 为什么只在训练集上建立词汇表?因为在测试模型时,都不能以任何方式影响测试集。 当然也不包括验证集,因为希望验证集尽可能地反映测试集。
TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE)
LABEL.build_vocab(train_data)
# 我们还可以查看词汇表中最常见的单词及其他们在数据集中出现的次数。
print(TEXT.vocab.freqs.most_common(20))
# 也可以使用stoi(string to int) or itos (int to string)方法,以下输出text-vocab的前10个词汇.
print(TEXT.vocab.itos[:10])

当我们将句子输入我们的模型时,我们一次输入一个_batch_,并且批次中的所有句子都需要具有相同的长度。因此,得设置一个malength,为了确保批次中的每个句子的大小相同,填充任何短于maxlength的句子,填充得部分设置为0,大于maxlength的部分直接截取。

# 准备数据的最后一步是创建迭代器.需要创建验证集,测试集,以及训练集的迭代器,每一次的迭代都会返回一个batch的数据。
# 我们将使用一个"Bucketlterator",它是一种特殊类型的迭代器,它将返回一批示例,其中每个样本的长度差不多,
# 从而最小业化每个样本的pading数如果有gpu的话,当然可以将迭代器返回的张量放在GPU上.可以用torch.device,可以将张量放到gpu或者cpu上。
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)

准备工作已经做好了,接下来我们就开始建立我们的模型RNN,在PyTorch中使用RNN的模型的时候,创建RNN的时候不是使用RNN类,而是nn.module的子类。
在_init_我们定义的模型的层数三层模型分别是嵌入层,RNN层,最后还有全连接层。embedding层将稀疏的one-hot转换为密集嵌入到空间的向量,减少了数据的运算的计算量这里有一种理论是,对评论的情绪有相似影响的词在向量空间中被紧密地映射在一块。RNN层接受之前的状态h(t-1)和当前输入对应的密集嵌入向量这两部分用来计算下一层的隐藏层状态h(t)。https://blog.csdn.net/weixin_44305115/article/details/101283477最终,最后一层线性层就会得到RNN输出的最后一层融藏层状态。这层隐藏层状态包含了之前所有的信息,将RNN最后一层的味藏层状态输入到全连接层,得到f(HT)最终转换成batch_size*num_classes。
model

import torch.nn as nn
# forward方法是当我们将训练数据, 验证数据集,测试数据集输入到模型时,数据就会传到forward方法,得到模型输出得结果。
# 在每一个batch中, text是一个大小为_[sentence length, batch size]_的tensor.这都是每一个句子对应的one-hot向量转换得到的。
# 而每个句子的one-hot向量都是由对应词典生成索引,然后根据索引值就可以得到每个句子的one-hot向量表示方式。
# 每一个输入的batch经过embedding层都会被embedded,
# 得到每一个句子的密集向量表示embedded后的向量size为[sentence length, batch size, embedding dim]。
# 在某些框架中,使用RNN需要初始化,但在pytorch中不用,默认为全0。
# 使用RNN会返回2个tensors, output和hiddien。output的size为[sentence length,batch size,hiden dim] and hiden的size为[1, batch size,hiden dim].
# output为每一层的隐藏层状态,而hidden是最后一层的隐藏层状态.
# 其实我们一般用hidden就行了,不用管output。最终,通过线性层fc,产生最终的预测。

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))
        # squeeze方法可以消除维度为1的维度。
        return self.fc(hidden.squeeze(0))
# 下面,我们可以做建立一个RNN的例子
# 输入维度就是对应one-hot向量的维度,也等同于词典的维度.
# embedding维度是可以设置的超参数通常设置为50-250维度某种程度上也和词典大小有关.
# 隐藏层维度就是最后一层隐藏层的大小通常可以设置为100-500维,这个也会词典大小,任务的复杂程度都有关系输出的维度就是要分类的类别的数目。
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM)

然后是训练模型,损失函数BCEWithLogitsLoss,在注释里给了详解的链接,不懂的可以看看

# 在模型训练前,先要设置优化器,这里我们选择的是SGD,随机梯度下降计算,
# model.parameters()表示需要更新的参数,lr为学习率
import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=1e-3)
# 接下来定义损失函数,BCEWithLogitsLoss一般用来做二分类。https://blog.csdn.net/qq_22210253/article/details/85222093
criterion = nn.BCEWithLogitsLoss()
model = model.to(device)
criterion = criterion.to(device)


# 损失函数用来计算损失值,还需要计算准确率的函数。
# 将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
# train函数迭代所有的样本,每次都是一个batch。
# 模型的每一个参数都有一个grad属性,存储着损失
# 只需要调用模型即可
# 损失值和准确率在整个epoch 中累积
# 当然在计算的时候,要记得将LongTensor转化为torch.float。这是因为TorchText默认将张量设置内LongTensor。
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:
        # 在每一次的batch,先将梯度清
        # 函数计算的梯度值. PyTorch 不会自动删除(或"归零”)从上次梯度计算中计算出的梯度,因此必须手动将其归零。
        optimizer.zero_grad()
        # 每次输入, batch.text,到模型中.
        predictions = model(batch.text).squeeze(1)

        loss = criterion(predictions, batch.label)

        acc = binary_accuracy(predictions, batch.label)

        # 用loss.backward ()计算梯度
        loss.backward()
        # 更新参数使用的是optimizer.step()。
        optimizer.step()
		# item()抽取张量中只含有一个值的张量中的值。
        epoch_loss += loss.item()
        epoch_acc += acc.item()

    # 最后,我们返回损失和准确率,在整个epoch 中取平均值. len可以得到epoch中的batch数
    return epoch_loss / len(iterator), epoch_acc / len(iterator)

# 其他函数在train中类似在evaluate中移除了optimizer.zero_grad(),
# loss.backward() and optimizer.step(),因为不再需要更新参数了
def evaluate(model, iterator, criterion):
    epoch_loss = 0
    epoch_acc = 0
    # model.eval()将模型置于"evaluation模式",这会关掉dropout和batch normalization.
    model.eval()
    # 在with no grad()下,不会进行梯度计算.这会导致使用更少的内存并加快计算速度.
    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)

# 接下来创建计算每个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
# 接下来创建计算每个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

# 然后,我们通过多个epoch来训练模型,每一个epoch是对训练和验证集中所有样本的完整传递。
# 在每个epoch,如果在验证集上的损失值是迄今为止我们所见过的最好的,我们将保存模型的参数,然后在训练完成后我们将在测试集上使用该模型。
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(), '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}%')

# 如上所示,损失并没有真正减少多少,而且准确性很差。这是由于这是baseline,我们将在下一个notbook中改进的模型的几个问题。
# 最后,要得到真正关心的指标,测试集上的损失和准确性,参数将从已经训练好的模型中获得,这些参数为我们提供了最好的验证集上的损失。

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}%')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值