深度学习笔记之自然语言处理(NLP)
深度学习笔记之自然语言处理(NLP)
在行将开学之时,我将开始我的深度学习笔记的自然语言处理部分,这部分内容是在前面基础上开展学习的,且目前我的学习更加倾向于通识。自然语言处理部分将包含《动手学深度学习》这本书的第十四章,自然语言处理预训练和第十五章,自然语言处理应用。并且参考原书提供的jupyter notebook资源。
自然语言处理,预训练
自然语言处理(Natural Language Processing,简称NLP)是人工智能的一个重要分支,它涉及计算机和人类(自然)语言之间的互动。自然语言处理的主要目标是让计算机能够理解和解释人类语言的方式,以便于能够执行如自动翻译、情感分析、信息提取、文本摘要、语音识别等任务。自然语言处理简而言之就是让计算机可以“理解”人类的语言,现在大语言模型就做到了这一点。
词嵌入
词嵌入的英文名为word2vec(这里的2意思就是to,在很多函数命名都是如此来用)。词嵌入是用来捕捉单词中的语义信息的。词嵌入(Word Embedding)是自然语言处理(NLP)中的一种重要技术,它将词汇映射到高维空间的向量中,使得语义上相似的词在向量空间中的距离也相近。词嵌入能够有效地捕捉单词的语义信息,是深度学习在自然语言处理中的一个基础组成部分。
以下是一些关于词嵌入的详细介绍:
-
向量化表示:在词嵌入之前,文本通常以独热编码(One-Hot Encoding)的形式表示,这种表示方式维度高且稀疏,无法体现词与词之间的关系。而词嵌入则提供了低维、稠密的向量表示。
-
语义信息:通过词嵌入,语义相似的词在向量空间中的位置相近,这使得模型能够更好地理解和处理自然语言。
总的来说词向量的特征如下:
-
词向量是用于表示单词意义的向量,也可以看作词的特征向量。将词映射到实向量的技术称为词嵌入。
-
word2vec工具包含跳元模型和连续词袋模型。
-
跳元模型假设一个单词可用于在文本序列中,生成其周围的单词;而连续词袋模型假设基于上下文词来生成中心单词。
近似训练
本部分中介绍了负采样和分层softmax这两种用于处理跳元模型(连续词袋模型)的近似训练方法。
负采样(Negative Sampling)是深度学习中特别是在自然语言处理(NLP)领域常用的一种技术,尤其是在训练词嵌入模型(如Word2Vec)和推荐系统等场景中。负采样的主要目的是提高训练效率,减少计算量。在深度学习中,很多任务可以被视为分类问题,其中正样本是任务相关的正确示例,而负样本则是不相关的示例。例如,在词嵌入模型中,一个词的上下文(周围的词)可以看作是正样本,而其他不相关的词则可以作为负样本。
层序softmax(Hierarchical Softmax)是一种用于加速深度学习模型训练过程中softmax运算的技术,尤其是在处理大规模分类问题,如语言模型中的词汇表非常大时。层序softmax通过将分类问题转化为一系列二分类问题来减少计算量。
在标准的softmax运算中,我们需要计算每个类别对应的概率,这涉及到对所有类别的指数函数的计算和归一化,其计算复杂度与类别数量成线性关系。当类别数量非常大时(例如,词汇表中有数万或数十万个单词),这种计算变得非常耗时。
层序softmax通过构建一个二叉树(通常是哈夫曼树)来表示所有的类别,每个叶节点代表一个类别。在训练过程中,我们不再直接计算所有类别的概率,而是通过在二叉树上进行一系列的二分类决策来计算目标类别的概率。
来自Transformer的双向编码器表示(BERT)
BERT(Bidirectional Encoder Representations from Transformers)是一种预训练自然语言处理模型,由Google在2018年提出。BERT在自然语言处理(NLP)领域具有重大意义,因为它为多种NLP任务提供了强大的基础模型。
以下是BERT的一些主要特点:
-
双向性(Bidirectional):与传统语言模型不同,BERT是一个双向模型,这意味着它同时考虑输入文本的左右上下文。这种双向性使得BERT能够更准确地捕捉词汇的语义信息。
-
Transformer架构:BERT基于Transformer模型,这是一种基于自注意力机制的深度神经网络架构。Transformer能够处理长距离的依赖关系,这在NLP任务中非常重要。
-
预训练任务:
-
掩码语言模型(Masked Language Model, MLM):在输入文本中随机掩盖一些词汇,然后让模型预测这些被掩盖的词汇。这种任务迫使模型同时考虑被掩盖词汇的左右上下文。
-
下一句预测(Next Sentence Prediction, NSP):给定两个句子A和B,模型需要预测B是否是A的下一句。这个任务有助于模型理解句子之间的关系。
-
-
微调(Fine-tuning):BERT通过预训练任务学习通用的语言表示,然后在特定任务上进行微调。微调阶段,BERT可以根据具体任务调整模型参数,从而在各个NLP任务上取得很好的表现。
BERT在以下NLP任务中表现出色:
-
文本分类
-
命名实体识别
-
问答系统
-
自然语言推理
-
机器翻译
-
文本生成等
BERT的提出极大地推动了NLP领域的发展,为许多NLP任务提供了有效的解决方案。如今,BERT及其变体(如RoBERTa、ALBERT、ERNIE等)在学术界和工业界都得到了广泛应用。部分代码如下所示:
import torch
from torch import nn
from d2l import torch as d2l
#@save
def get_tokens_and_segments(tokens_a, tokens_b=None):
"""获取输入序列的词元及其片段索引"""
tokens = ['<cls>'] + tokens_a + ['<sep>']
# 0和1分别标记片段A和B
segments = [0] * (len(tokens_a) + 2)
if tokens_b is not None:
tokens += tokens_b + ['<sep>']
segments += [1] * (len(tokens_b) + 1)
return tokens, segments
#@save
class BERTEncoder(nn.Module):
"""BERT编码器"""
def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
ffn_num_hiddens, num_heads, num_layers, dropout,
max_len=1000, key_size=768, query_size=768, value_size=768,
**kwargs):
super(BERTEncoder, self).__init__(**kwargs)
self.token_embedding = nn.Embedding(vocab_size, num_hiddens)
self.segment_embedding = nn.Embedding(2, num_hiddens)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module(f"{i}", d2l.EncoderBlock(
key_size, query_size, value_size, num_hiddens, norm_shape,
ffn_num_input, ffn_num_hiddens, num_heads, dropout, True))
# 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数
self.pos_embedding = nn.Parameter(torch.randn(1, max_len,
num_hiddens))
def forward(self, tokens, segments, valid_lens):
# 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens)
X = self.token_embedding(tokens) + self.segment_embedding(segments)
X = X + self.pos_embedding.data[:, :X.shape[1], :]
for blk in self.blks:
X = blk(X, valid_lens)
return X
vocab_size, num_hiddens, ffn_num_hiddens, num_heads = 10000, 768, 1024, 4
norm_shape, ffn_num_input, num_layers, dropout = [768], 768, 2, 0.2
encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape, ffn_num_input,
ffn_num_hiddens, num_heads, num_layers, dropout)
tokens = torch.randint(0, vocab_size, (2, 8))
segments = torch.tensor([[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 0, 1, 1, 1, 1, 1]])
encoded_X = encoder(tokens, segments, None)
encoded_X.shape
#@save
class MaskLM(nn.Module):
"""BERT的掩蔽语言模型任务"""
def __init__(self, vocab_size, num_hiddens, num_inputs=768, **kwargs):
super(MaskLM, self).__init__(**kwargs)
self.mlp = nn.Sequential(nn.Linear(num_inputs, num_hiddens),
nn.ReLU(),
nn.LayerNorm(num_hiddens),
nn.Linear(num_hiddens, vocab_size))
def forward(self, X, pred_positions):
num_pred_positions = pred_positions.shape[1]
pred_positions = pred_positions.reshape(-1)
batch_size = X.shape[0]
batch_idx = torch.arange(0, batch_size)
# 假设batch_size=2,num_pred_positions=3
# 那么batch_idx是np.array([0,0,0,1,1,1])
batch_idx = torch.repeat_interleave(batch_idx, num_pred_positions)
masked_X = X[batch_idx, pred_positions]
masked_X = masked_X.reshape((batch_size, num_pred_positions, -1))
mlm_Y_hat = self.mlp(masked_X)
return mlm_Y_hat
mlm = MaskLM(vocab_size, num_hiddens)
mlm_positions = torch.tensor([[1, 5, 2], [6, 1, 5]])
mlm_Y_hat = mlm(encoded_X, mlm_positions)
mlm_Y_hat.shape
mlm_Y = torch.tensor([[7, 8, 9], [10, 20, 30]])
loss = nn.CrossEntropyLoss(reduction='none')
mlm_l = loss(mlm_Y_hat.reshape((-1, vocab_size)), mlm_Y.reshape(-1))
mlm_l.shape
#@save
class NextSentencePred(nn.Module):
"""BERT的下一句预测任务"""
def __init__(self, num_inputs, **kwargs):
super(NextSentencePred, self).__init__(**kwargs)
self.output = nn.Linear(num_inputs, 2)
def forward(self, X):
# X的形状:(batchsize,num_hiddens)
return self.output(X)
encoded_X = torch.flatten(encoded_X, start_dim=1)
# NSP的输入形状:(batchsize,num_hiddens)
nsp = NextSentencePred(encoded_X.shape[-1])
nsp_Y_hat = nsp(encoded_X)
nsp_Y_hat.shape
nsp_y = torch.tensor([0, 1])
nsp_l = loss(nsp_Y_hat, nsp_y)
nsp_l.shape
#@save
class BERTModel(nn.Module):
"""BERT模型"""
def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
ffn_num_hiddens, num_heads, num_layers, dropout,
max_len=1000, key_size=768, query_size=768, value_size=768,
hid_in_features=768, mlm_in_features=768,
nsp_in_features=768):
super(BERTModel, self).__init__()
self.encoder = BERTEncoder(vocab_size, num_hiddens, norm_shape,
ffn_num_input, ffn_num_hiddens, num_heads, num_layers,
dropout, max_len=max_len, key_size=key_size,
query_size=query_size, value_size=value_size)
self.hidden = nn.Sequential(nn.Linear(hid_in_features, num_hiddens),
nn.Tanh())
self.mlm = MaskLM(vocab_size, num_hiddens, mlm_in_features)
self.nsp = NextSentencePred(nsp_in_features)
def forward(self, tokens, segments, valid_lens=None,
pred_positions=None):
encoded_X = self.encoder(tokens, segments, valid_lens)
if pred_positions is not None:
mlm_Y_hat = self.mlm(encoded_X, pred_positions)
else:
mlm_Y_hat = None
# 用于下一句预测的多层感知机分类器的隐藏层,0是“<cls>”标记的索引
nsp_Y_hat = self.nsp(self.hidden(encoded_X[:, 0, :]))
return encoded_X, mlm_Y_hat, nsp_Y_hat
自然语言处理,应用
本章将探讨两种流行的自然语言处理任务,分别是:情感分析和自然语言推断,并在预训练和相应架构的基础上开展应用。
情感分析
情感分析与数据集
情感分析的数据集是来自于斯坦福大学的,数据集中包含上万条评论,且包含两种标签,分别是积极和消极。对数据集的读取和分析代码如下所示.
import os
import torch
from torch import nn
import d2l
#@save
d2l.DATA_HUB['aclImdb'] = (
'http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz',
'01ada507287d82875905620988597833ad4e0903')
data_dir = d2l.download_extract('aclImdb', 'aclImdb')
#@save
def read_imdb(data_dir, is_train):
"""读取IMDb评论数据集文本序列和标签"""
data, labels = [], []
for label in ('pos', 'neg'):#两种标签分别为postive 和negative
folder_name = os.path.join(data_dir, 'train' if is_train else 'test',
label)
for file in os.listdir(folder_name):
with open(os.path.join(folder_name, file), 'rb') as f:
review = f.read().decode('utf-8').replace('\n', '')
data.append(review)
labels.append(1 if label == 'pos' else 0)
return data, labels
train_data = read_imdb(data_dir, is_train=True)
print('训练集数目:', len(train_data[0]))
for x, y in zip(train_data[0][:3], train_data[1][:3]):
print('标签:', y, 'review:', x[0:60])
train_tokens = d2l.tokenize(train_data[0], token='word')
vocab = d2l.Vocab(train_tokens, min_freq=5, reserved_tokens=['<pad>'])#进行词频过滤
d2l.set_figsize()
d2l.plt.xlabel('# tokens per review')
d2l.plt.ylabel('count')
d2l.plt.hist([len(line) for line in train_tokens], bins=range(0, 1000, 50))
num_steps = 500 # 序列长度
train_features = torch.tensor([d2l.truncate_pad(
vocab[line], num_steps, vocab['<pad>']) for line in train_tokens])
print(train_features.shape)
train_iter = d2l.load_array((train_features,
torch.tensor(train_data[1])), 64)
for X, y in train_iter:
print('X:', X.shape, ', y:', y.shape)
break
print('小批量数目:', len(train_iter))
#@save
def load_data_imdb(batch_size, num_steps=500):
"""返回数据迭代器和IMDb评论数据集的词表"""
data_dir = d2l.download_extract('aclImdb', 'aclImdb')
train_data = read_imdb(data_dir, True)
test_data = read_imdb(data_dir, False)
train_tokens = d2l.tokenize(train_data[0], token='word')
test_tokens = d2l.tokenize(test_data[0], token='word')
vocab = d2l.Vocab(train_tokens, min_freq=5)
train_features = torch.tensor([d2l.truncate_pad(
vocab[line], num_steps, vocab['<pad>']) for line in train_tokens])
test_features = torch.tensor([d2l.truncate_pad(
vocab[line], num_steps, vocab['<pad>']) for line in test_tokens])
train_iter = d2l.load_array((train_features, torch.tensor(train_data[1])),
batch_size)
test_iter = d2l.load_array((test_features, torch.tensor(test_data[1])),
batch_size,
is_train=False)
return train_iter, test_iter, vocab
输出的词元长度的直方图如下:
情感分析与循环神经网络
循环神经网络(RNN)非常适合用于处理文本数据,因为它能够考虑到序列数据中的时间动态特性,这在文本中体现为词语的顺序。以下是使用循环神经网络来研究文本中情感的基本步骤:
-
数据收集与预处理
-
收集数据集:获取一个标注好的情感数据集,如IMDb电影评论数据集,其中包含了正面和负面情感的评论。
-
文本清洗:去除无关字符,如HTML标签、特殊符号等。
-
分词:将文本分割成单词或词语。
-
构建词汇表:将文本中的单词映射为唯一的整数索引。
-
填充与截断:确保所有文本序列长度一致,通过填充或截断来实现。
-
构建模型
-
选择RNN类型:可以使用标准的RNN,或者更高级的变体如LSTM(长短期记忆网络)或GRU(门控循环单元),这些变体能够更好地捕捉长距离依赖。
-
定义网络结构
-
训练模型
-
损失函数:对于分类问题,通常使用交叉熵损失函数。
-
优化器:选择一个优化器,如Adam,来调整网络权重。
-
迭代训练:通过多次迭代训练数据来优化网络权重。
-
评估模型
-
划分数据集:将数据集划分为训练集、验证集和测试集。
-
评估指标:使用准确率、召回率、F1分数等指标来评估模型性能。
-
模型调优
-
调整超参数:根据验证集的性能来调整学习率、批次大小、网络层数等超参数。
-
防止过拟合:使用正则化技术如Dropout,或者提前停止训练来防止过拟合。
-
应用模型
-
情感分析:使用训练好的模型对新的文本数据进行情感分类。
完成的代码如下所示:
import torch
from torch import nn
import d2l
batch_size = 64
train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)
class BiRNN(nn.Module):
def __init__(self, vocab_size, embed_size, num_hiddens,
num_layers, **kwargs):
super(BiRNN, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
# 将bidirectional设置为True以获取双向循环神经网络
self.encoder = nn.LSTM(embed_size, num_hiddens, num_layers=num_layers,
bidirectional=True)
self.decoder = nn.Linear(4 * num_hiddens, 2)
def forward(self, inputs):
# inputs的形状是(批量大小,时间步数)
# 因为长短期记忆网络要求其输入的第一个维度是时间维,
# 所以在获得词元表示之前,输入会被转置。
# 输出形状为(时间步数,批量大小,词向量维度)
embeddings = self.embedding(inputs.T)
self.encoder.flatten_parameters()
# 返回上一个隐藏层在不同时间步的隐状态,
# outputs的形状是(时间步数,批量大小,2*隐藏单元数)
outputs, _ = self.encoder(embeddings)
# 连结初始和最终时间步的隐状态,作为全连接层的输入,
# 其形状为(批量大小,4*隐藏单元数)
encoding = torch.cat((outputs[0], outputs[-1]), dim=1)
outs = self.decoder(encoding)
return outs
embed_size, num_hiddens, num_layers = 100, 100, 2
devices = d2l.try_all_gpus()
net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers)#构造具有两个隐藏层的双向循环神经网络
def init_weights(m):#初始化模型参数
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
if type(m) == nn.LSTM:
for param in m._flat_weights_names:
if "weight" in param:
nn.init.xavier_uniform_(m._parameters[param])
net.apply(init_weights)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')#加载预训练的Glove嵌入
embeds = glove_embedding[vocab.idx_to_token]
embeds.shape
net.embedding.weight.data.copy_(embeds)
net.embedding.weight.requires_grad = False
lr, num_epochs = 0.01, 5
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,#训练模型
devices)
#@save
def predict_sentiment(net, vocab, sequence):
"""预测文本序列的情感"""
sequence = torch.tensor(vocab[sequence.split()], device=d2l.try_gpu())
label = torch.argmax(net(sequence.reshape(1, -1)), dim=1)
return 'positive' if label == 1 else 'negative'
predict_sentiment(net, vocab, 'this movie is so great')#结果是Positive
predict_sentiment(net, vocab, 'this movie is so bad')#结果是negative
训练结果是判断相当的准确,但凡给出稍微正常的评价语句就能分成积极的和消极的。
在深度学习中,预训练(Pre-training)是一种训练策略,指的是在一个大型数据集上对模型进行初步训练,然后再在特定任务的数据集上进行微调(Fine-tuning)。以下是预训练的几个关键概念:
-
预训练的目的
-
学习通用特征:预训练通常在大规模数据集上进行,目的是让模型学习到通用的特征表示,这些特征可以在多种不同的任务中重用。
-
减少对标注数据的依赖:标注数据通常非常昂贵且耗时,预训练可以让模型在没有大量标注数据的情况下也能获得较好的性能。
-
提高模型性能:预训练可以作为一种正则化手段,帮助模型在小数据集上避免过拟合,并提高泛化能力。
-
预训练的方法
-
自监督学习:在没有标注数据的情况下,通过设计预测任务来训练模型,例如预测句子中的下一个词(语言模型预训练)或预测图像中的像素块(如MAE)。
-
监督学习:使用有标注的大数据集进行训练,如ImageNet数据集上的图像分类任务。
-
迁移学习:在源任务上预训练模型,然后将模型迁移到目标任务上进行微调。
-
预训练模型
-
BERT(Bidirectional Encoder Representations from Transformers):一种基于Transformer的双向编码器,通过预测句子中被掩盖的词来进行预训练。
-
GPT(Generative Pre-trained Transformer):一种基于Transformer的生成模型,通过语言建模任务进行预训练。
-
ResNet:一种深度卷积神经网络,通常在ImageNet数据集上进行预训练,用于图像识别任务。
情感分析与卷积神经网络
在这个例子中,我们将使用一个简单的卷积神经网络(CNN)来进行文本情感分析。以下是使用CNN进行文本情感分析的基本步骤:
-
数据预处理:
-
分词:将文本数据分割成单词或字符。
-
嵌入:使用预训练的词向量(如Word2Vec、GloVe)将单词转换为固定长度的向量。
-
填充:确保所有文本序列长度一致,通常通过在序列末尾添加特殊填充值(如0)。
-
-
构建卷积神经网络:
-
嵌入层:将单词索引映射到密集的向量表示。
-
卷积层:使用多个卷积核提取特征。
-
激活函数:通常使用ReLU函数。
-
池化层:减少特征维度,同时保留重要信息。
-
全连接层:将提取的特征映射到情感类别。
-
-
训练模型:
-
损失函数:通常使用交叉熵损失函数。
-
优化器:如Adam、SGD等。
-
-
评估模型:
-
使用验证集评估模型性能。
-
可以使用准确率、F1分数等指标来衡量模型效果。
-
示例代码如下:
import torch
from torch import nn
import d2l
batch_size = 64#定义批量大小
train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)#加载数据
def corr1d(X, K):#定义一个一维互相关函数
w = K.shape[0]
Y = torch.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]):
Y[i] = (X[i: i + w] * K).sum()
return Y
X, K = torch.tensor([0, 1, 2, 3, 4, 5, 6]), torch.tensor([1, 2])
corr1d(X, K)#测试一维互相关函数
def corr1d_multi_in(X, K):
# 首先,遍历'X'和'K'的第0维(通道维)。然后,把它们加在一起
return sum(corr1d(x, k) for x, k in zip(X, K))
X = torch.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = torch.tensor([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
class TextCNN(nn.Module):#定义一个卷积神经网络类,继承自nn.Module
def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels,
**kwargs):
super(TextCNN, self).__init__(**kwargs)
for c, k in zip(num_channels, kernel_sizes):
self.convs.append(nn.Conv1d(2 * embed_size, c, k))
def forward(self, inputs):
# 沿着向量维度将两个嵌入层连结起来,
# 每个嵌入层的输出形状都是(批量大小,词元数量,词元向量维度)连结起来
embeddings = torch.cat((
self.embedding(inputs), self.constant_embedding(inputs)), dim=2)
# 根据一维卷积层的输入格式,重新排列张量,以便通道作为第2维
embeddings = embeddings.permute(0, 2, 1)
# 每个一维卷积层在最大时间汇聚层合并后,获得的张量形状是(批量大小,通道数,1)
# 删除最后一个维度并沿通道维度连结
encoding = torch.cat([
torch.squeeze(self.relu(self.pool(conv(embeddings))), dim=-1)
for conv in self.convs], dim=1)
outputs = self.decoder(self.dropout(encoding))
return outputs
embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
devices = d2l.try_all_gpus()#采用GPU来训练
net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels)#新建一个神经网络
def init_weights(m):
if type(m) in (nn.Linear, nn.Conv1d):
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)#调用属性函数
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')#加载预训练的Glove嵌入
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.data.copy_(embeds)
net.constant_embedding.weight.data.copy_(embeds)
net.constant_embedding.weight.requires_grad = False
lr, num_epochs = 0.001, 5
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)#采用d2l包中的训练函数
d2l.predict_sentiment(net, vocab, 'this movie is so great')#结果为Positive
d2l.predict_sentiment(net, vocab, 'this movie is so bad')#结果为negative
在Textcnn模型中使用一维卷积层和最大时间汇聚层将单个词元表示转化为下游的应用输出。
自然语言推断
自然语言推断与数据集
自然语言推断(Natural Language Inference, NLI)是自然语言处理(Natural Language Processing, NLP)领域的一个重要研究方向,它研究的是如何让计算机理解和推断两个自然语言表达(通常是一个前提(premise)和一个假设(hypothesis))之间的逻辑关系。自然语言推断的主要研究内容包括以下几个方面:
-
关系分类:自然语言推断的核心任务是判断两个句子之间的逻辑关系,这通常分为三个类别:
-
蕴含(Entailment):假设是前提的必然结果。
-
矛盾(Contradiction):假设与前提完全相反,不能同时为真。
-
中性(Neutral):假设既不是前提的必然结果,也不与前提矛盾。
-
-
模型架构:研究者们探索了多种深度学习模型架构来提高自然语言推断的性能,包括:
-
卷积神经网络(CNN):用于捕捉局部特征和句子的层次结构。
-
循环神经网络(RNN):特别是长短期记忆网络(LSTM)和门控循环单元(GRU),用于处理序列数据。
-
变压器(Transformer)模型:利用自注意力机制,能够处理长距离依赖,是目前NLI研究中的主流模型。
-
预训练模型:如BERT(Bidirectional Encoder Representations from Transformers)、RoBERTa、ALBERT等,这些模型在大型语料库上预训练,然后在NLI任务上进行微调。
-
import os
import re
import torch
from torch import nn
import d2l
#@save
d2l.DATA_HUB['SNLI'] = (
'https://nlp.stanford.edu/projects/snli/snli_1.0.zip',
'9fcde07509c7e87ec61c640c1b2753d9041758e4')
data_dir = d2l.download_extract('SNLI')#下载斯坦福自然语言数据集
#@save
def read_snli(data_dir, is_train):
"""将SNLI数据集解析为前提、假设和标签"""
def extract_text(s):
# 删除我们不会使用的信息
s = re.sub('\\(', '', s)
s = re.sub('\\)', '', s)
# 用一个空格替换两个或多个连续的空格
s = re.sub('\\s{2,}', ' ', s)
return s.strip()
label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2}
file_name = os.path.join(data_dir, 'snli_1.0_train.txt'
if is_train else 'snli_1.0_test.txt')
with open(file_name, 'r') as f:
rows = [row.split('\t') for row in f.readlines()[1:]]
premises = [extract_text(row[1]) for row in rows if row[0] in label_set]
hypotheses = [extract_text(row[2]) for row in rows if row[0] \
in label_set]
labels = [label_set[row[0]] for row in rows if row[0] in label_set]
return premises, hypotheses, labels
train_data = read_snli(data_dir, is_train=True)
for x0, x1, y in zip(train_data[0][:3], train_data[1][:3], train_data[2][:3]):
print('前提:', x0)
print('假设:', x1)
print('标签:', y)
test_data = read_snli(data_dir, is_train=False)
for data in [train_data, test_data]:
print([[row for row in data[2]].count(i) for i in range(3)])
#@save
class SNLIDataset(torch.utils.data.Dataset):
"""用于加载SNLI数据集的自定义数据集"""
def __init__(self, dataset, num_steps, vocab=None):
self.num_steps = num_steps
all_premise_tokens = d2l.tokenize(dataset[0])
all_hypothesis_tokens = d2l.tokenize(dataset[1])
if vocab is None:
self.vocab = d2l.Vocab(all_premise_tokens + \
all_hypothesis_tokens, min_freq=5, reserved_tokens=['<pad>'])
else:
self.vocab = vocab
self.premises = self._pad(all_premise_tokens)
self.hypotheses = self._pad(all_hypothesis_tokens)
self.labels = torch.tensor(dataset[2])
print('read ' + str(len(self.premises)) + ' examples')
def _pad(self, lines):#填充
return torch.tensor([d2l.truncate_pad(
self.vocab[line], self.num_steps, self.vocab['<pad>'])
for line in lines])
def __getitem__(self, idx):#返回数据集中索引为idx的元素
return (self.premises[idx], self.hypotheses[idx]), self.labels[idx]
def __len__(self):#返回数据集中的元素数量
return len(self.premises)
#@save
def load_data_snli(batch_size, num_steps=50):
"""下载SNLI数据集并返回数据迭代器和词表"""
num_workers = d2l.get_dataloader_workers()
data_dir = d2l.download_extract('SNLI')
train_data = read_snli(data_dir, True)
test_data = read_snli(data_dir, False)
train_set = SNLIDataset(train_data, num_steps)
test_set = SNLIDataset(test_data, num_steps, train_set.vocab)
train_iter = torch.utils.data.DataLoader(train_set, batch_size,
shuffle=True,
num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(test_set, batch_size,
shuffle=False,
num_workers=num_workers)
return train_iter, test_iter, train_set.vocab
train_iter, test_iter, vocab = load_data_snli(128, 50)#批量大小为128,每个序列长度为50
len(vocab)#词表大小
for X, Y in train_iter:#X是一个元组,包含两个形状为(批量大小, 词数)的张量
print(X[0].shape)
print(X[1].shape)
print(Y.shape)
break
自然语言推断:使用注意力
注意力机制在自然语言推断中的应用
在自然语言推断中,注意力机制通常用于以下步骤:
-
词嵌入层
首先,将输入的前提(premise)和假设(hypothesis)中的每个单词转换为其对应的词向量表示。
-
编码层
使用循环神经网络(RNN)或变压器(Transformer)的编码器层来处理这些词向量,以获得每个单词的上下文表示。
-
注意力层
在编码层之后,引入注意力层来计算前提和假设之间的交互。以下是几种常见的注意力机制:
使用注意力机制的自然语言推断的代码如下:
import torch
from torch import nn
from torch.nn import functional as F
import d2l
def mlp(num_inputs, num_hiddens, flatten):#多层感知机
net = []
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_inputs, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_dim=1))
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_hiddens, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_dim=1))
return nn.Sequential(*net)
class Attend(nn.Module):#注意力机制
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B):
# A/B的形状:(批量大小,序列A/B的词元数,embed_size)
# f_A/f_B的形状:(批量大小,序列A/B的词元数,num_hiddens)
f_A = self.f(A)
f_B = self.f(B)
# e的形状:(批量大小,序列A的词元数,序列B的词元数)
e = torch.bmm(f_A, f_B.permute(0, 2, 1))
# beta的形状:(批量大小,序列A的词元数,embed_size),
# 意味着序列B被软对齐到序列A的每个词元(beta的第1个维度)
beta = torch.bmm(F.softmax(e, dim=-1), B)
# beta的形状:(批量大小,序列B的词元数,embed_size),
# 意味着序列A被软对齐到序列B的每个词元(alpha的第1个维度)
alpha = torch.bmm(F.softmax(e.permute(0, 2, 1), dim=-1), A)
return beta, alpha
class Compare(nn.Module):#比较
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(torch.cat([A, beta], dim=2))
V_B = self.g(torch.cat([B, alpha], dim=2))
return V_A, V_B
class Aggregate(nn.Module):#聚合
def __init__(self, num_inputs, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_inputs, num_hiddens, flatten=True)
self.linear = nn.Linear(num_hiddens, num_outputs)
def forward(self, V_A, V_B):
# 对两组比较向量分别求和
V_A = V_A.sum(dim=1)
V_B = V_B.sum(dim=1)
# 将两个求和结果的连结送到多层感知机中
Y_hat = self.linear(self.h(torch.cat([V_A, V_B], dim=1)))
return Y_hat
class DecomposableAttention(nn.Module):#分解注意力
def __init__(self, vocab, embed_size, num_hiddens, num_inputs_attend=100,
num_inputs_compare=200, num_inputs_agg=400, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_inputs_attend, num_hiddens)
self.compare = Compare(num_inputs_compare, num_hiddens)
# 有3种可能的输出:蕴涵、矛盾和中性
self.aggregate = Aggregate(num_inputs_agg, num_hiddens, num_outputs=3)
def forward(self, X):#前向传播函数
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
batch_size, num_steps = 256, 50#批量大小为256,每个序列长度为50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)#加载SNLI数据集
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()#嵌入维度为100,隐藏单元数为200
net = DecomposableAttention(vocab, embed_size, num_hiddens)#定义神经网络
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')#加载预训练的词向量
embeds = glove_embedding[vocab.idx_to_token]#获取词汇表中每个词元的词向量
net.embedding.weight.data.copy_(embeds)#用预训练的词向量初始化嵌入层
lr, num_epochs = 0.001, 4#定义学习率和迭代周期数
trainer = torch.optim.Adam(net.parameters(), lr=lr)#定义优化器
loss = nn.CrossEntropyLoss(reduction="none")#定义损失函数
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,#训练模型
devices)
#@save
def predict_snli(net, vocab, premise, hypothesis):
"""预测前提和假设之间的逻辑关系"""
net.eval()
premise = torch.tensor(vocab[premise], device=d2l.try_gpu())
hypothesis = torch.tensor(vocab[hypothesis], device=d2l.try_gpu())
label = torch.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), dim=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
自然语言推断:微调BERT
自然语言推断(Natural Language Inference, NLI)是一种判断两个句子之间逻辑关系的任务。BERT(Bidirectional Encoder Representations from Transformers)是一种预训练的语言表示模型,它在大量文本数据上进行训练,以学习通用的语言表示。微调(Fine-tuning)是在预训练模型的基础上,针对特定任务进行进一步训练的过程。以下是自然语言推断中微调BERT的原理:
BERT的预训练
在微调之前,BERT模型已经经历了两个预训练任务:
-
遮蔽语言模型(Masked Language Model, MLM):随机遮蔽输入序列中的某些 tokens,然后预测这些遮蔽的 tokens。
-
下一句预测(Next Sentence Prediction, NSP):给定两个句子,模型预测第二个句子是否是第一个句子的后续句子。
这两个任务使BERT能够学习到丰富的语言表示,这些表示捕获了单词的上下文信息。
具体的示例代码如下:
import json
import multiprocessing
import os
import torch
from torch import nn
from d2l import d2l as d2l
d2l.DATA_HUB['bert.base'] = (d2l.DATA_URL + 'bert.base.torch.zip',
'225d66f04cae318b841a13d32af3acc165f253ac')
d2l.DATA_HUB['bert.small'] = (d2l.DATA_URL + 'bert.small.torch.zip',
'c72329e68a732bef0452e4b96a1c341c8910f81f')
def load_pretrained_model(pretrained_model, num_hiddens, ffn_num_hiddens,
num_heads, num_layers, dropout, max_len, devices):
data_dir = d2l.download_extract(pretrained_model)
# 定义空词表以加载预定义词表
vocab = d2l.Vocab()
vocab.idx_to_token = json.load(open(os.path.join(data_dir,
'vocab.json')))
vocab.token_to_idx = {token: idx for idx, token in enumerate(
vocab.idx_to_token)}
bert = d2l.BERTModel(len(vocab), num_hiddens, norm_shape=[256],
ffn_num_input=256, ffn_num_hiddens=ffn_num_hiddens,
num_heads=4, num_layers=2, dropout=0.2,
max_len=max_len, key_size=256, query_size=256,
value_size=256, hid_in_features=256,
mlm_in_features=256, nsp_in_features=256)
# 加载预训练BERT参数
bert.load_state_dict(torch.load(os.path.join(data_dir,
'pretrained.params')))
return bert, vocab
devices = d2l.try_all_gpus()
bert, vocab = load_pretrained_model(
'bert.small', num_hiddens=256, ffn_num_hiddens=512, num_heads=4,
num_layers=2, dropout=0.1, max_len=512, devices=devices)
class SNLIBERTDataset(torch.utils.data.Dataset):#SNLI数据集
def __init__(self, dataset, max_len, vocab=None):
all_premise_hypothesis_tokens = [[
p_tokens, h_tokens] for p_tokens, h_tokens in zip(
*[d2l.tokenize([s.lower() for s in sentences])
for sentences in dataset[:2]])]
self.labels = torch.tensor(dataset[2])
self.vocab = vocab
self.max_len = max_len
(self.all_token_ids, self.all_segments,
self.valid_lens) = self._preprocess(all_premise_hypothesis_tokens)
print('read ' + str(len(self.all_token_ids)) + ' examples')
def _preprocess(self, all_premise_hypothesis_tokens):
pool = multiprocessing.Pool(4) # 使用4个进程
out = pool.map(self._mp_worker, all_premise_hypothesis_tokens)
all_token_ids = [
token_ids for token_ids, segments, valid_len in out]
all_segments = [segments for token_ids, segments, valid_len in out]
valid_lens = [valid_len for token_ids, segments, valid_len in out]
return (torch.tensor(all_token_ids, dtype=torch.long),
torch.tensor(all_segments, dtype=torch.long),
torch.tensor(valid_lens))
def _mp_worker(self, premise_hypothesis_tokens):
p_tokens, h_tokens = premise_hypothesis_tokens
self._truncate_pair_of_tokens(p_tokens, h_tokens)
tokens, segments = d2l.get_tokens_and_segments(p_tokens, h_tokens)
token_ids = self.vocab[tokens] + [self.vocab['<pad>']] \
* (self.max_len - len(tokens))
segments = segments + [0] * (self.max_len - len(segments))
valid_len = len(tokens)
return token_ids, segments, valid_len
def _truncate_pair_of_tokens(self, p_tokens, h_tokens):
# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置
while len(p_tokens) + len(h_tokens) > self.max_len - 3:
if len(p_tokens) > len(h_tokens):
p_tokens.pop()
else:
h_tokens.pop()
def __getitem__(self, idx):
return (self.all_token_ids[idx], self.all_segments[idx],
self.valid_lens[idx]), self.labels[idx]
def __len__(self):
return len(self.all_token_ids)
# 如果出现显存不足错误,请减少“batch_size”。在原始的BERT模型中,max_len=512
batch_size, max_len, num_workers = 256, 128, d2l.get_dataloader_workers()#批量大小为256,最大长度为128
data_dir = d2l.download_extract('SNLI')#下载SNLI数据集
train_set = SNLIBERTDataset(d2l.read_snli(data_dir, True), max_len, vocab)#加载SNLI数据集
test_set = SNLIBERTDataset(d2l.read_snli(data_dir, False), max_len, vocab)#加载SNLI数据集
train_iter = torch.utils.data.DataLoader(train_set, batch_size, shuffle=True,
num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(test_set, batch_size,
num_workers=num_workers)
class BERTClassifier(nn.Module):#BERT分类器
def __init__(self, bert):
super(BERTClassifier, self).__init__()
self.encoder = bert.encoder
self.hidden = bert.hidden
self.output = nn.Linear(256, 3)
def forward(self, inputs):
tokens_X, segments_X, valid_lens_x = inputs
encoded_X = self.encoder(tokens_X, segments_X, valid_lens_x)
return self.output(self.hidden(encoded_X[:, 0, :]))
net = BERTClassifier(bert)#定义神经网络
lr, num_epochs = 1e-4, 5
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)
python