1、词表映射

无论是深度学习还是传统的统计机器学习方法处理自然语言,都需要先将输入的语言符号(通常为标记Token),映射为大于等于0、小于词表大小的整数,该整数也被称作一个标记的索引值或下标。

vocab类实现标记和索引之间的相互映射。

from collections import defaultdict, Counter

class Vocab:
    def __init__(self, tokens=None):
        self.idx_to_token = list()
        self.token_to_idx = dict()

        if tokens is not None:
            if "<unk>" not in tokens:
                tokens = tokens + ["<unk>"]
            for token in tokens:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1
            self.unk = self.token_to_idx['<unk>']

    @classmethod
    def build(cls, text, min_freq=1, reserved_tokens=None):
        token_freqs = defaultdict(int)
        for sentence in text:
            for token in sentence:
                token_freqs[token] += 1
        uniq_tokens = ["<unk>"] + (reserved_tokens if reserved_tokens else [])
        uniq_tokens += [token for token, freq in token_freqs.items() \
                        if freq >= min_freq and token != "<unk>"]
        return cls(uniq_tokens)

    def __len__(self):
        #返回词表大小,即词表有多少个互不相同的标记
        return len(self.idx_to_token)

    def __getitem__(self, token):
        #查找对应输入标记的索引
        #不存在,返回标记<unk>的索引
        return self.token_to_idx.get(token, self.unk)

    def convert_tokens_to_ids(self, tokens):
        #查找一系列输入标记对应的索引
        return [self[token] for token in tokens]

    def convert_ids_to_tokens(self, indices):
        #查找一系列索引值对应的标记
        return [self.idx_to_token[index] for index in indices]


def save_vocab(vocab, path):
    with open(path, 'w') as writer:
        writer.write("\n".join(vocab.idx_to_token))


def read_vocab(path):
    with open(path, 'r') as f:
        tokens = f.read().split('\n')
    return Vocab(tokens)

2、词向量层

在使用深度学习进行自然语言处理时,将一个词(或者标记) 转换为一个低维、稠密、连续的词向量(也称 Embedding ) 是一种基本的词表示方法,通过 torch.nn 包提供的 Embedding 层即可实现该功能。

创建 Embedding 对象,需要两个参数

  • num_embeddings :词表的大小
  • enbeading-dim: Embedding 向量的维度

实现的功能是将输入的整数张量中每个整数(通过词表映射功能获得标记对应的整数)映射为相应维度(embedding_dim)的张量。

示例

import torch
from torch import nn
#词表大小为8,Embedding向量维度3
embedding=nn.Embedding(8,3)
#输入形状(2,4)的整数张量,相当于2个长度为4的整数序列,每个整数范围是0-7
input=torch.tensor([[0,1,2,1],[4,6,6,7]],dtype=torch.long)
#调用embedding
output=embedding(input)
print(output)
print(output.shape)

输出

张量形状为[2, 4, 3],即在原始输入最后增加一个长度为3 的维

tensor([[[-0.9536,  0.3367, -1.1639],
         [-0.4816, -0.8973, -0.7700],
         [ 1.9007,  1.3130, -1.2717],
         [-0.4816, -0.8973, -0.7700]],

        [[ 0.5755,  0.8908, -1.0384],
         [ 0.3724,  0.1216,  2.4466],
         [ 0.3724,  0.1216,  2.4466],
         [ 1.4089,  0.2458,  0.2263]]], grad_fn=<EmbeddingBackward0>)
torch.Size([2, 4, 3])

3、融入词向量层的多层感知器

基本的多层感知器,输入为固定大小的实数向量。如果输人为文本,即整数序列(假设已经利用词表映射工具将文本中每个标记映射为了相应的整数),在经过多层感知器之前,需要利用词向量层将输入的整数映射为向量。

但是,一个序列中通常含有多个词向量,那么如何将它们表示为一个多层感知器的输入向量呢?一种方法是将几个向量拼接成一个大小为

pytorch RNN模型情感分析_人工智能

的向量,其中d表示每个词向量的大小。这样做的一个问题是最终的预测结果与标记在序列中的位置过于相关。例如,如果在一个序列前面增加一个标记,则序列中的每个标记位置都变了,也就是它们对应的参数都发生了变化,那么模型预测的结果可能完全不同,这样显然不合理。

在自然语言处理中,可以使用词袋(Bag-Of-Words, Bow)模型解决该问题。词袋模型指的是在表示序列时,不考虑其中元素的顺序,而是将其简单地看成是一个集合。于是就可以采用聚合操作处理一个序列中的多个词向量,如求平均、求和或保留最大值等。融入词向量层以及词袋模型的多层感知器代码如下:

import torch
from torch import nn
from torch.nn import functional as F

class MLP(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_class):
        super(MLP, self).__init__()
        # 词嵌入层
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        # 线性变换:词嵌入层->隐含层
        self.linear1 = nn.Linear(embedding_dim, hidden_dim)
        # 使用ReLU激活函数
        self.activate = F.relu
        # 线性变换:激活层->输出层
        self.linear2 = nn.Linear(hidden_dim, num_class)

    def forward(self, inputs):
        embeddings = self.embedding(inputs)
        # 将序列中多个embedding进行聚合(此处是求平均值)
        embedding = embeddings.mean(dim=1)
        hidden = self.activate(self.linear1(embedding))
        outputs = self.linear2(hidden)
        # 获得每个序列属于某一类别概率的对数值
        probs = F.log_softmax(outputs, dim=1)
        return probs

mlp = MLP(vocab_size=8, embedding_dim=3, hidden_dim=5, num_class=2)
# 输入为两个长度为4的整数序列
inputs = torch.tensor([[0, 1, 2, 1], [4, 6, 6, 7]], dtype=torch.long)
outputs = mlp(inputs)
print(outputs)

输出:

结果为每个序列属于某一类别的概率的对数

tensor([[-1.1612, -0.3756],
        [-0.8089, -0.5894]], grad_fn=<LogSoftmaxBackward0>)

在实际的自然语言处理任务中,一个批次里输人的文本长度往往是不固定的,因此无法像上面的代码一样简单地用一个张量存储词向量并求平均值。 PyTorch 提供了一种更灵活的解决方案,即EmnbeddingBag 层。在调用 Embedding-Bag 层时,首先需要将不定长的序列拼按起来,然后使用一个偏移向量 ( Offsets ) 记录每个序列的起始位置。

4、数据处理

第一步是将待处理的数据从硬盘或者其他地方加载到程序中,此时读入的是原始文本数据,还需要经过分句、标记解析等 预处理过程转换为标记序列,然后再使用词表映射工具将每个标记映射到相应的索引值。在此,使用 NLTK 提供的句子倾向性分析数据 (sentence_polarity) 作为示例,

import torch
from vocab import Vocab

def load_sentence_polarity():
    from nltk.corpus import sentence_polarity

    #使用全部橘子集合(已经过标记解析)创建词表
    vocab = Vocab.build(sentence_polarity.sents())

    #褒贬各4000句子作为训练数据,使用创建的词表将标记映射为相应的索引
    #褒义标签为0,贬义标签为1
    #每个样例是一个有索引值列表和标签组成的元祖
    train_data = [(vocab.convert_tokens_to_ids(sentence), 0)
                  for sentence in sentence_polarity.sents(categories='pos')[:4000]] \
        + [(vocab.convert_tokens_to_ids(sentence), 1)
            for sentence in sentence_polarity.sents(categories='neg')[:4000]]

    #其余的作为测试数据
    test_data = [(vocab.convert_tokens_to_ids(sentence), 0)
                 for sentence in sentence_polarity.sents(categories='pos')[4000:]] \
        + [(vocab.convert_tokens_to_ids(sentence), 1)
            for sentence in sentence_polarity.sents(categories='neg')[4000:]]

    return train_data, test_data, vocab

def length_to_mask(lengths):
    max_len = torch.max(lengths)
    mask = torch.arange(max_len).expand(lengths.shape[0], max_len) < lengths.unsqueeze(1)
    return mask

def load_treebank():
    from nltk.corpus import treebank
    sents, postags = zip(*(zip(*sent) for sent in treebank.tagged_sents()))

    vocab = Vocab.build(sents, reserved_tokens=["<pad>"])

    tag_vocab = Vocab.build(postags)

    train_data = [(vocab.convert_tokens_to_ids(sentence), tag_vocab.convert_tokens_to_ids(tags)) for sentence, tags in zip(sents[:3000], postags[:3000])]
    test_data = [(vocab.convert_tokens_to_ids(sentence), tag_vocab.convert_tokens_to_ids(tags)) for sentence, tags in zip(sents[3000:], postags[3000:])]

    return train_data, test_data, vocab, tag_vocab

dataset 是 Dataset 类(在torch.utils.data 包中定义)的一个对象,用于存储数据,一般需要根据具体的数据存取需求创建 Dataset 类的子类。如创建 —个BowDataset 子类,其中 Bow 是词袋的意思。具体代码如下

import torch
from torch import nn, optim
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
from collections import defaultdict
from vocab import Vocab
from utils import load_sentence_polarity

class BowDataset(Dataset):
    def __init__(self, data):
        #data为原始数据
        self.data = data
    def __len__(self):
        #数据集中样例的数目
        return len(self.data)
    def __getitem__(self, i):
        #返回下标i的数据
        return self.data[i]

#用于对样本进行整理
def collate_fn(examples):
    #从独立样本集合中构建各批次的输入输出 
    #其中,BowDataset类定义了一个样本的数据结构,即输入标签和轮出标悠的元组
    # 因此,将输入inputs定义为一个张量的列表,其中每个张量为原始句子中标记序列 
    inputs = [torch.tensor(ex[0]) for ex in examples]
    #输出的目标targets 为该批次中全部样例输出结果 (0或1) 构成的张量
    targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
    # 获取一个批次中每个样例的序列长度 
    offsets = [0] + [i.shape[0] for i in inputs]
    #根据序列的长度,转换为每个序列起始位置的偏移量 
    offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
    #将inputs列表中的张量拼接成一个大的张量 
    inputs = torch.cat(inputs)
    return inputs, offsets, targets

5、多层感知机模型的训练与测试

import torch
from torch import nn, optim
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
from collections import defaultdict
from vocab import Vocab
from utils import load_sentence_polarity


# tqdm是一个Python模块,能以进度条的方式显示迭代的进度
from tqdm.auto import tqdm

# 超参数设置
embedding_dim = 128
hidden_dim = 256
num_class = 2
batch_size = 32
num_epoch = 5

# 加载数据
train_data, test_data, vocab = load_sentence_polarity()
train_dataset = BowDataset(train_data)
test_dataset = BowDataset(test_data)
train_data_loader = DataLoader(train_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=True)
test_data_loader = DataLoader(test_dataset, batch_size=1, collate_fn=collate_fn, shuffle=False)

# 加载模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = MLP(len(vocab), embedding_dim, hidden_dim, num_class)
model.to(device) # 将模型加载到CPU或GPU设备

#训练过程
nll_loss = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001) # 使用Adam优化器

model.train()
for epoch in range(num_epoch):
    total_loss = 0
    for batch in tqdm(train_data_loader, desc=f"Training Epoch {epoch}"):
        inputs, offsets, targets = [x.to(device) for x in batch]
        log_probs = model(inputs, offsets)
        loss = nll_loss(log_probs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Loss: {total_loss:.2f}")

# 测试过程
acc = 0
for batch in tqdm(test_data_loader, desc=f"Testing"):
    inputs, offsets, targets = [x.to(device) for x in batch]
    with torch.no_grad():
        output = model(inputs, offsets)
        acc += (output.argmax(dim=1) == targets).sum().item()

# 输出在测试集上的准确率
print(f"Acc: {acc / len(test_data_loader):.2f}")

训练结果


pytorch RNN模型情感分析_深度学习_02


6、基于卷积神经网络的情感分类

第3 节的词袋模型表示文本,只考虑了文本中词语的信息,而忽视了词组信息,如句子

我不喜欢这部电影

词袋模型看到文本中有“喜欢”一词,则很可能将其识别为褒义。而卷积神经网络可以提取词组信息,如将卷积核的大小设置为 2,则可以提取特征“不 喜欢” 等,显然这对于最终情感极性的判断至关重要。卷积神经网络的大部分代码与多层感知器的实现一致

其中的不同之处:

  • 模型不同,需要从 nn. Module 类派生一个 CMN 子类
  • 在调用卷积神经网络时,还需要设置两个额外的超参数,分别为filter_size =3(卷积核的大小)和num_tilter =100(卷积核的个数)
  • 数据整理函数(collate_fn),pad_seguence 两数实现补齐 (Padding)功能,使得一个批次中 全部宇列长度相同(同最大长度序列),不足的默认使用0补齐。

除了以上不同,其他代码与多层感知器的实现几乎一致。如要实现一个基于新模型的情感分类任务,只需要定义一个nn.Module类的子类,并修改数据整理函数(collate_fn)即可

import torch
from torch import nn, optim
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pad_sequence
from collections import defaultdict
from vocab import Vocab
from utils import load_sentence_polarity

class CnnDataset(Dataset):
    def __init__(self, data):
        self.data = data
    def __len__(self):
        return len(self.data)
    def __getitem__(self, i):
        return self.data[i]

#另外,数据整理函数也需要进行一些修改。  
# pad_seguence 两数实现补齐 (Padding)功能,使得一个批次中 全部序列长度相同(同最大长度序列),不足的默认使用0补齐。 
def collate_fn(examples):
    inputs = [torch.tensor(ex[0]) for ex in examples]
    targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
    # 对batch内的样本进行padding,使其具有相同长度
    inputs = pad_sequence(inputs, batch_first=True)
    return inputs, targets

#模型不同,需要从nn.Module类派生一个 CNN 子类
class CNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, filter_size, num_filter, num_class):
        super(CNN, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        #padding=1表示在卷积操作之前,将序列的前后各补充1个输入
        self.conv1d = nn.Conv1d(embedding_dim, num_filter, filter_size, padding=1)
        self.activate = F.relu
        self.linear = nn.Linear(num_filter, num_class)
    def forward(self, inputs):
        embedding = self.embedding(inputs)
        convolution = self.activate(self.conv1d(embedding.permute(0, 2, 1)))
        pooling = F.max_pool1d(convolution, kernel_size=convolution.shape[2])
        outputs = self.linear(pooling.squeeze(dim=2))
        log_probs = F.log_softmax(outputs, dim=1)
        return log_probs

#tqdm是一个Pyth模块,能以进度条的方式显示迭代的进度
from tqdm.auto import tqdm

#超参数设置
embedding_dim = 128
hidden_dim = 256
num_class = 2
batch_size = 32
num_epoch = 5
#在调用卷积神经网络时,还需要设置两个额外的超参数,分别为filter_size =3(卷积核的大小)和num_tilter =100(卷积核的个数)。
filter_size = 3
num_filter = 100

#加载数据
train_data, test_data, vocab = load_sentence_polarity()
train_dataset = CnnDataset(train_data)
test_dataset = CnnDataset(test_data)
train_data_loader = DataLoader(train_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=True)
test_data_loader = DataLoader(test_dataset, batch_size=1, collate_fn=collate_fn, shuffle=False)

#加载模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = CNN(len(vocab), embedding_dim, filter_size, num_filter, num_class)
model.to(device) #将模型加载到CPU或GPU设备

#训练过程
nll_loss = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001) #使用Adam优化器

model.train()
for epoch in range(num_epoch):
    total_loss = 0
    for batch in tqdm(train_data_loader, desc=f"Training Epoch {epoch}"):
        inputs, targets = [x.to(device) for x in batch]
        log_probs = model(inputs)
        loss = nll_loss(log_probs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Loss: {total_loss:.2f}")

#测试过程
acc = 0
for batch in tqdm(test_data_loader, desc=f"Testing"):
    inputs, targets = [x.to(device) for x in batch]
    with torch.no_grad():
        output = model(inputs)
        acc += (output.argmax(dim=1) == targets).sum().item()

#输出在测试集上的准确率
print(f"Acc: {acc / len(test_data_loader):.2f}")

训练结果


pytorch RNN模型情感分析_Powered by 金山文档_03


7、基于循环神经网络的情感分类

第3 节的词袋模型还忽路了文本中词的顺序信息,因此对于两个句子 “张三行李四”和“李四行张三”,它们的表示是完全相同的,但显然这并不合理。循环神经网络模型能更好地对序列数据进行表示。

以长短时记忆(LSTM) 网络为例。

其中,大部分代码与前面的实现一致,不同之处:

  • 需要从nn.Module派生一个LSTM子类
  • forward中使用 pack_padded_sequence将变长序列打包,功能是将之前补齐过的一个小批次序列打包成一个序列,其中每个原始序列的长度存储在lengths中,能被self.lstm直接调用
import torch
from torch import nn, optim
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence
from collections import defaultdict
from vocab import Vocab
from utils import load_sentence_polarity

#tqdm是一个Python模块,能以进度条的方式显式迭代的进度
from tqdm.auto import tqdm

class LstmDataset(Dataset):
    def __init__(self, data):
        self.data = data
    def __len__(self):
        return len(self.data)
    def __getitem__(self, i):
        return self.data[i]

#整理函数
def collate_fn(examples):
    #获得每个序列的长度
    lengths = torch.tensor([len(ex[0]) for ex in examples])
    inputs = [torch.tensor(ex[0]) for ex in examples]
    targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
    # 对batch内的样本进行padding,使其具有相同长度
    inputs = pad_sequence(inputs, batch_first=True)
    return inputs, lengths, targets

#需要从nn.Module派生一个LSTM子类
class LSTM(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_class):
        super(LSTM, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        self.output = nn.Linear(hidden_dim, num_class)

    def forward(self, inputs, lengths):
        embeddings = self.embeddings(inputs)
        #使用 pack_padded_sequence将变长序列打包
        x_pack = pack_padded_sequence(embeddings, lengths, batch_first=True, enforce_sorted=False)
        hidden, (hn, cn) = self.lstm(x_pack)
        outputs = self.output(hn[-1])
        log_probs = F.log_softmax(outputs, dim=-1)
        return log_probs

embedding_dim = 128
hidden_dim = 256
num_class = 2
batch_size = 32
num_epoch = 5

#加载数据
train_data, test_data, vocab = load_sentence_polarity()
train_dataset = LstmDataset(train_data)
test_dataset = LstmDataset(test_data)
train_data_loader = DataLoader(train_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=True)
test_data_loader = DataLoader(test_dataset, batch_size=1, collate_fn=collate_fn, shuffle=False)

#加载模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = LSTM(len(vocab), embedding_dim, hidden_dim, num_class)
model.to(device) #将模型加载到GPU中(如果已经正确安装)

#训练过程
nll_loss = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001) #使用Adam优化器

model.train()
for epoch in range(num_epoch):
    total_loss = 0
    for batch in tqdm(train_data_loader, desc=f"Training Epoch {epoch}"):
        inputs, lengths, targets = [x.to(device) for x in batch]
        log_probs = model(inputs, lengths)
        loss = nll_loss(log_probs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Loss: {total_loss:.2f}")

#测试过程
acc = 0
for batch in tqdm(test_data_loader, desc=f"Testing"):
    inputs, lengths, targets = [x.to(device) for x in batch]
    with torch.no_grad():
        output = model(inputs, lengths)
        acc += (output.argmax(dim=1) == targets).sum().item()

#输出在测试集上的准确率
print(f"Acc: {acc / len(test_data_loader):.2f}")

训练结果


pytorch RNN模型情感分析_深度学习_04


8、基于 Transformer 的情感分类

基于 Transformer 实现情感分类与使用 LSTM 也非常相似,主要有一处不同, 即需要定义 Transformer模型。

# Defined in Section 4.6.8

import math
import torch
from torch import nn, optim
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence
from collections import defaultdict
from vocab import Vocab
from utils import load_sentence_polarity, length_to_mask

# tqdm是一个Pyth模块,能以进度条的方式显式迭代的进度
from tqdm.auto import tqdm

class TransformerDataset(Dataset):
    def __init__(self, data):
        self.data = data
    def __len__(self):
        return len(self.data)
    def __getitem__(self, i):
        return self.data[i]

def collate_fn(examples):
    lengths = torch.tensor([len(ex[0]) for ex in examples])
    inputs = [torch.tensor(ex[0]) for ex in examples]
    targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
    # 对batch内的样本进行padding,使其具有相同长度
    inputs = pad_sequence(inputs, batch_first=True)
    return inputs, lengths, targets

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=512):
        super(PositionalEncoding, self).__init__()

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + self.pe[:x.size(0), :]
        return x

class Transformer(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_class,
                 dim_feedforward=512, num_head=2, num_layers=2, dropout=0.1, max_len=128, activation: str = "relu"):
        super(Transformer, self).__init__()
        # 词嵌入层
        self.embedding_dim = embedding_dim
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.position_embedding = PositionalEncoding(embedding_dim, dropout, max_len)
        # 编码层:使用Transformer
        encoder_layer = nn.TransformerEncoderLayer(hidden_dim, num_head, dim_feedforward, dropout, activation)
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
        # 输出层
        self.output = nn.Linear(hidden_dim, num_class)


    def forward(self, inputs, lengths):
        inputs = torch.transpose(inputs, 0, 1)
        #与LSTM处理情况相同,输入的数据的第一维是批次,需要转换为TransformerEncoder
        #所需要的第1维是长度,第二维是批次的形状
        hidden_states = self.embeddings(inputs)
        hidden_states = self.position_embedding(hidden_states)
        #length_to_mask作用是根据批次中每个序列的长度生成mask矩阵,以便处理长度不一致的序列,忽略掉比较短的序列的无效部分
        #src_key_padding_mask的参数正好与length_to_mask的结果相反(无自注意力的部分为true)
        attention_mask = length_to_mask(lengths.to('cpu')) == False
        #根据批次中的每个序列长度生成mask矩阵
        hidden_states = self.transformer(hidden_states, src_key_padding_mask=attention_mask.to('cuda'))
        hidden_states = hidden_states[0, :, :]
        #取第一个标记的输出结果作为分类层的输入
        output = self.output(hidden_states)
        log_probs = F.log_softmax(output, dim=1)
        return log_probs

embedding_dim = 128
hidden_dim = 128
num_class = 2
batch_size = 32
num_epoch = 5

# 加载数据
train_data, test_data, vocab = load_sentence_polarity()
train_dataset = TransformerDataset(train_data)
test_dataset = TransformerDataset(test_data)
train_data_loader = DataLoader(train_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=True)
test_data_loader = DataLoader(test_dataset, batch_size=1, collate_fn=collate_fn, shuffle=False)

# 加载模型
device = torch.device('cuda')
model = Transformer(len(vocab), embedding_dim, hidden_dim, num_class)
model.to(device) # 将模型加载到GPU中(如果已经正确安装)

# 训练过程
nll_loss = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001) # 使用Adam优化器

model.train()
for epoch in range(num_epoch):
    total_loss = 0
    for batch in tqdm(train_data_loader, desc=f"Training Epoch {epoch}"):
        inputs, lengths, targets = [x.to(device) for x in batch]
        log_probs = model(inputs, lengths)
        loss = nll_loss(log_probs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Loss: {total_loss:.2f}")

# 测试过程
acc = 0
for batch in tqdm(test_data_loader, desc=f"Testing"):
    inputs, lengths, targets = [x.to(device) for x in batch]
    with torch.no_grad():
        output = model(inputs, lengths)
        acc += (output.argmax(dim=1) == targets).sum().item()

# 输出在测试集上的准确率
print(f"Acc: {acc / len(test_data_loader):.2f}")

训练结果


pytorch RNN模型情感分析_pytorch RNN模型情感分析_05