15. 自然语言处理:应用
15.1. 情感分析及数据集
本章使用斯坦福大学的大型电影评论数据集(large movie review dataset)进行情感分析,其共包含25000个电影评论,训练集和一个测试集中的“积极”和“消极”标签的数量相同。
# 读取数据集
#@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):
"""每个样本都是一个评论及其标签,1表示积极,0表示消极"""
data, labels = [], []
for label in ('pos', 'neg'):
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)
将每个单词作为一个词元,过滤掉出现不到5次的单词,我们从训练数据集中创建一个词表。
train_tokens = d2l.tokenize(train_data[0], token='word')
vocab = d2l.Vocab(train_tokens, min_freq=5, reserved_tokens=['<pad>'])
所有评论语句长度均不同,为了每次处理一小批量的评论,通过截断和填充将每个评论的长度设置为500。这类似于 9.5节中对机器翻译数据集的预处理步骤。
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)# torch.Size([25000, 500])
# 创建数据迭代器了。在每次迭代中,都会返回一小批量样本。
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))
"""
X: torch.Size([64, 500]) , y: torch.Size([64])
⼩批量数⽬: 391
"""
将上述步骤封装到load_data_imdb
函数中。它返回训练和测试数据迭代器以及IMDb评论数据集的词表。
#@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
15.2. 情感分析:使用循环神经网络
由于 15.1节中的IMDb评论数据集不是很大,使用在大规模语料库上预训练的文本表示可以减少模型的过拟合。我们将使用预训练的GloVe模型来表示每个词元,并将这些词元表示送入多层双向循环神经网络以获得文本序列表示,该文本序列表示将被转换为情感分析输出 [Maas et al., 2011]。
batch_size = 64
train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)
15.2.1. 使用循环神经网络表示单个文本
在文本分类任务(如情感分析)中,可变长度的文本序列将被转换为固定长度的类别。在下面的BiRNN
类中,虽然文本序列的每个词元经由嵌入层(self.embedding
)获得其预训练GloVe表示,双向长短期记忆网络在初始和最终时间步的隐状态被连结起来作为文本序列的表示。然后,通过一个具有两个输出(“积极”和“消极”)的全连接层(self.decoder
),将此单一文本表示转换为输出类别。
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);
15.2.2. 加载预训练的词向量
下面为词表中的单词加载预训练的100维(与embed_size
一致)的GloVe嵌入。
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
embeds.shape # torch.Size([49346, 100])
使用这些预训练的词向量来表示评论中的词元,并且在训练期间不要更新这些向量。
net.embedding.weight.data.copy_(embeds)
net.embedding.weight.requires_grad = False
15.2.3. 训练和评估模
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)
"""
loss 0.271, train acc 0.887, test acc 0.850
772.0 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
"""
#@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
15.6. 针对序列级和词元级应用程序微调BERT
通常 , 我们会为特定应用设计特定的模型,例如基于循环神经网络、卷积神经网络、注意力和多层感知机。这些模型在有空间或时间限制的情况下是有帮助的,但是,为每个自然语言处理任务精心设计一个特定的模型实际上是不可行的。BERT预训练模型可以对广泛的自然语言处理任务进行最少的架构更改。
下面将自然语言处理应用的子集概括为序列级和词元级。在序列层次上,介绍了在单文本分类任务和文本对分类(或回归)任务中,如何将文本输入的BERT表示转换为输出标签。在词元级别,将简要介绍新的应用,如文本标注和问答,并说明BERT如何表示它们的输入并转换为输出标签。在微调期间,不同应用之间的BERT所需的“最小架构更改”是额外的全连接层。在下游应用的监督学习期间,额外层的参数是从零开始学习的,而预训练BERT模型中的所有参数都是微调的。
15.6.1. 单文本分类
单文本分类将单个文本序列作为输入,并输出其分类结果。 除了情感分析之外, 还可用于判断语法是否合理等其他问题。
BERT输入序列明确地表示单个文本和文本对。如图所示,在单文本分类应用中,特殊分类标记[cls]的BERT表示对整个输入文本序列的信息进行编码。作为输入单个文本的表示,它将被送入到由全连接(稠密)层组成的多层感知机中,以输出所有离散标签值的分布。
15.6.2. 文本对分类或回归
自然语言推断属于文本对分类。以一对文本作为输入但输出连续值,语义文本相似度是一个流行的“文本对回归”任务。 例如,在语义文本相似度基准数据集(Semantic Textual Similarity Benchmark)中,句子对的相似度得分是从0(无语义重叠)到5(语义等价)的分数区间 [Cer et al., 2017]。我们的目标是预测这些分数。来自语义文本相似性基准数据集的样本包括(句子1,句子2,相似性得分):
- “A plane is taking off.”(“一架飞机正在起飞。”),“An air plane is taking off.”(“一架飞机正在起飞。”),5.000分;
- “A woman is eating something.”(“一个女人在吃东西。”),“A woman is eating meat.”(“一个女人在吃肉。”),3.000分;
- “A woman is dancing.”(一个女人在跳舞。),“A man is talking.”(“一个人在说话。”),0.000分。
与15.6.1中的单文本分类相比,上图中的文本对分类的BERT微调在输入表示上有所不同。对于文本对回归任务(如语义文本相似性),可以应用细微的更改,例如输出连续的标签值和使用均方损失:它们在回归中很常见。
15.6.3. 文本标注
词元级任务,比如文本标注(text tagging)。在文本标注任务中,词性标注为每个单词分配词性标记(例如,形容词和限定词)。 如,在Penn树库II标注集中,句子“John Smith‘s car is new”应该被标记为“NNP(名词,专有单数)NNP POS(所有格结尾)NN(名词,单数或质量)VB(动词,基本形式)JJ(形容词)”。
与15.6.1相比,唯一的区别在于,在文本标注中,输入文本的每个词元的BERT表示被送到相同的额外全连接层中,以输出词元的标签,例如词性标签。
15.6.4. 问答
作为另一个词元级应用,问答反映阅读理解能力。 例如,斯坦福问答数据集(Stanford Question Answering Dataset,SQuAD v1.1)由阅读段落和问题组成,其中每个问题的答案只是段落中的一段文本(文本片段) [Rajpurkar et al., 2016]。举个例子,考虑一段话:“Some experts report that a mask’s efficacy is inconclusive.However,mask makers insist that their products,such as N95 respirator masks,can guard against the virus.”(“一些专家报告说面罩的功效是不确定的。然而,口罩制造商坚持他们的产品,如N95口罩,可以预防病毒。”)还有一个问题“Who say that N95 respirator masks can guard against the virus?”(“谁说N95口罩可以预防病毒?”)。答案应该是文章中的文本片段“mask makers”(“口罩制造商”)。因此,SQuAD v1.1的目标是在给定问题和段落的情况下预测段落中文本片段的开始和结束。
为了预测文本片段开始的位置,相同的额外的全连接层将把来自位置的任何词元的BERT表示转换成标量分数
。文章中所有词元的分数还通过softmax转换成概率分布,从而为文章中的每个词元位置
分配作为文本片段开始的概率
。预测文本片段的结束与上面相同,只是其额外的全连接层中的参数与用于预测开始位置的参数无关。当预测结束时,位置
的词元由相同的全连接层变换成标量分数
。
对于问答,监督学习的训练目标就像最大化真实值的开始和结束位置的对数似然一样简单。当预测片段时,可以计算从位置到位置
的有效片段的分数
,并输出分数最高的跨度。