0 序言
15.1 情感分析及数据集
- 评论数据 用于 支持决策过程
- 应用场景:
- 产品评论、博客评论 和 论坛评论
- 政治:公众对政策的情绪分析
- 金融:市场情绪分析
- 营销:产品研究 和 品牌管理
- 情感被分为 离散的极性或尺度(如消极的和积极的),本质为文本分类任务。
- 本章数据集:斯坦福大学 的 大型电影评论数据集,包含IMDb下载的25000个电影评论。训练集合测试集中“积极”和“消极”的数量相同,表示不同的情感极性。
import os, torch
from torch import nn
from d2l import torch as d2l
15.1.1 读取数据集
d2l.DATA_HUB['aclImdb'] = (
'http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz',
'01ada507287d82875905620988597833ad4e0903')
def read_imdb(data_dir, is_train):
"""读取imdb评论数据集 文本序列和标签"""
data, labels = [], []
for label in ('pos', 'neg'):
cate = 'train' if is_train else 'test'
folder_name = 'aclImdb/' + cate + '/' + label + '/'
for file in os.listdir(folder_name):
with open(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(1, 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])
15.1.2 预处理数据集
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)
15.1.3 创建数据迭代器
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))
15.1.4 整合代码
def load_data_imdb(batch_size, num_steps=500):
"""返回 数据迭代器 和 IMDB评论数据集的词表"""
data_dir = 1
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
- 小结
- 情感分析任务 本质上是 文本分类问题
- 预处理后,可使用 词表 将IMDb评论数据集 加载到 数据迭代器 中
- 另外数据集:Amazon reviews https://snap.stanford.edu/data/web-Amazon.html
15.2 情感分析:使用RNN
- 将 预先训练的词向量 应用于情感分析
- 整个模型的架构:
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 64
train_iter, test_iter, vocab = load_data_imdb(batch_size)
15.2.1 使用RNN表示 单个文本
- 可变长度的文本序列 转换为 固定长度的类别
- biLSTM网络 在初始和最终时间步的隐状态(在最后一层) 被连接起来作为 文本序列的表示
- 最后通过 一个具有两个输出(消极和积极)的FC层,将此单一文本表示转换为输出类别
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)
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):
embeddings = self.embedding(inputs.T)
self.encoder.flatten_parameters()
outputs, _ = self.encoder(embeddings)
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_normal_(m._parameters[param])
net.apply(init_weights)
15.2.2 加载预训练的词向量
class TokenEmbedding:
"""Token Embedding."""
def __init__(self, embedding_name):
"""Defined in :numref:`sec_synonyms`"""
self.idx_to_token, self.idx_to_vec = self._load_embedding(
embedding_name)
self.unknown_idx = 0
self.token_to_idx = {token: idx for idx, token in
enumerate(self.idx_to_token)}
def _load_embedding(self, embedding_name):
idx_to_token, idx_to_vec = ['<unk>'], []
with open('./glove.6B.100d.txt', 'r', encoding='utf-8') as f:
for line in f:
elems = line.rstrip().split(' ')
token, elems = elems[0], [float(elem) for elem in elems[1:]]
if len(elems) > 1:
idx_to_token.append(token)
idx_to_vec.append(elems)
idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec
return idx_to_token, d2l.tensor(idx_to_vec)
def __getitem__(self, tokens):
indices = [self.token_to_idx.get(token, self.unknown_idx)
for token in tokens]
vecs = self.idx_to_vec[d2l.tensor(indices)]
return vecs
def __len__(self):
return len(self.idx_to_token)
glove_embedding = TokenEmbedding('./glove.6B.100d')
embeds = glove_embedding[vocab.idx_to_token]
embeds.shape
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)
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', net(sequence.reshape(1, -1))
s = 'this movie is so great !'
predict_sentiment(net, vocab, s)
s = 'this movie is so bad'
predict_sentiment(net, vocab, s)
- 小结
- 也可以使用spacy词元化来提高分类进度,短语标记的不同形式(如Glove的“new-york”和spacy的“new york”)
- 预训练模型表示各个词元
- bi-rnn表示文本序列
15.3 情感分析:使用CNN
- CNN可以用于NLP,将文本序列想象成一维图像即可。
- 一维CNN 可以处理 文本的局部特征,如n元语法。
- 本节使用 textCNN模型 进行情感分析
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 64
train_iter, test_iter, vocab = load_data_imdb(batch_size)
15.3.1 一维卷积
- 一维卷积的运作方式(这只是基于互相关运算的二维卷积的特例):
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)
- 具有 3个输入通道的一维互相关运算
def corr1d_multi_in(X, K):
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)
- 多输入通道的一维互相关 等价于 单输入通道的二维互相关(注意:看待问题的角度不同,前者是多个一维向量,后者是看成一个二维向量)
15.3.2 最大时间汇聚层
- 使用汇聚层——pooling从序列表示中提取最大值,作为跨时间步的最重要的特征
- textCNN中使用的 最大时间汇聚层的工作原理 ≈ 一维全局汇聚
- 对于每个通道 在不同时间步 存储 值的多通道输入,每个通道的输出 是 该通道的最大值
- 注:最大时间汇聚 允许 在不同通道上使用不同数量的时间步。
15.3.3 textCNN模型
-
textCNN模型 将 单个预训练的词元表示 作为输入,然后使用 一维卷积和最大时间汇聚 -
textCNN模型的输入:张量的宽度、高度和通道数分别为
n
,
1
,
d
n,1,d
n,1,d。 -
textCNN模型步骤为: -
textCNN模型的架构为:
定义模型
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels, **kwargs):
super(TextCNN, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.constant_embedding = nn.Embedding(vocab_size, embed_size)
self.dropout = nn.Dropout(0.5)
self.decoder = nn.Linear(sum(num_channels), 2)
self.pool = nn.AdaptiveAvgPool1d(1)
self.relu = nn.ReLU()
self.convs = nn.ModuleList()
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)
embeddings = embeddings.permute(0, 2, 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()
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)
加载预训练词向量
- 加载 预训练的100维GloVe嵌入 作为 初始化的词元表示
- 这些词元(嵌入权重)在embedding中被训练,在constant_embedding中被固定
glove_embedding = TokenEmbedding('glove.6B.100d.txt')
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, 10
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.predict_sentiment(net, vocab, 'this movie is so great')
d2l.predict_sentiment(net, vocab, 'this movie is so bad')
- 小结
- 一维CNN 可以处理 文本中的局部特征,例如
n
n
n元语法
- 多输入通道的一维互相关 等价于 单输入通道的二维互相关
- 最大时间汇聚层(pooling) 允许在 不同通道上 使用 不同数量的时间步长
- textCNN模型 使用 一维卷积层和最大时间汇聚层 将 单个词元表示 转换为 下游应用输出。
- CNN更快,更精确
15.4 自然语言推断 与 数据集
- 回顾 情感分析 目的:将 单个文本序列 分类到 预定义的类别 中
- 还需要对 文本序列 进行推断:确定 一个句子是否可以从另一个句子推断出来;或者需要通过识别语义等价的句子来消除句子间冗余。
15.4.1 自然语言推断(又:识别文本蕴涵任务)
15.4.2 斯坦福自然语言推断(SNLI)数据集
- 斯坦福?然语?推断语料库(Stanford Natural Language Inference,SNLI)是由 500000多个带标签的英语句?对 组成的集合。
import os, re, torch
from torch import nn
from d2l import torch as d2l
d2l.DATA_HUB['SNLI'] = (
'https://nlp.stanford.edu/projects/snli/snli_1.0.zip',
'9fcde07509c7e87ec61c640c1b2753d9041758e4')
读取数据集
- 原始的SNLI数据集 更加 丰富
- 以下函数 提取 数据集的一部分,然后返回 前提、假设 及其 标签的列表
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
- 现在让我们打印前3对 前提和假设,以及 它们的标签(“0”、“1”和“2”分别对应于“蕴涵”、“?盾”和“中
性”)。
data_dir = './snli_1.0/'
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)
- 训练集约有550000对,测试集约有10000对。下?显?了训练集和测试集中的三个标签“蕴涵”、“?盾”和“中性”是平衡的。
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)])
定义 用于加载数据集的类
- 下?我们来定义?个?于加载SNLI数据集的类。
- 类变量num_steps指定?本序列的?度,使得每个?批量序列将具有相同的形状。换句话说,在较?序列中的前num_steps个标记之后的标记被截断,?特殊标记
<pad> 将被附加到较短的序列后,直到它们的?度变为num_steps。 - 通过实现__getitem__功能,可以任意访问带有索引idx的前提、假设和标签。
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):
return (self.premises[idx], self.hypotheses[idx]), self.labels[idx]
def __len__(self):
return len(self.premises)
整合代码
- 现在可调?read_snli函数和SNLIDataset类来 下载SNLI数据集
- 并返回训练集和测试集的DataLoader实例,以及训练集的词表。
- 注: 必须使? 从训练集构造的词表 作为 测试集的词表。因此,在训练集中训练的模型将不知道来?测试集的任何新词元。
def load_data_snli(batch_size, num_steps=50):
"""下载SNLI数据集并返回 数据迭代器和词表"""
num_workers = d2l.get_dataloader_workers()
num_workers = 0
data_dir = './snli_1.0/'
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)
len(vocab)
for X, Y in train_iter:
print(X[0].shape)
print(X[1].shape)
print(Y.shape)
break
- 小结
- ?然语?推断研究“假设”是否可以从“前提”推断出来,其中两者都是?本序列。
- 在?然语?推断中,前提和假设之间的关系包括蕴涵关系、?盾关系和中性关系。
- 斯坦福?然语?推断(SNLI)语料库是?个?较流?的?然语?推断基准数据集。
- 可以尝试使用 自然语言推断 来评估 机器翻译结果。
- 减小词表的方法:
- 调大min_freq参数
- 调小num_steps参数
15.5 自然语言推断:使用注意力
- 可分解att模型:Parikh等人提出用 att机制 解决 自然语言推断问题
- 模型中 没有 卷积层或循环层:在SNLI数据集上以 更少的参数 实现了 当时的最佳结果
- 本节中,描述并使用这种 基于att的自然语言推断方法(使用MLP)
- 模型架构图:
15.5.1 模型
- 与保留 前提和假设中词元的顺序 相?,我们可以将 ?个?本序列中的词元 与 另?个?本序列中的每个词元 对?,然后 ?较和聚合 这些信息,以预测 前提和假设之间的逻辑关系。
- 与机器翻译中 源句和?标句之间的词元对? 类似,前提和假设之间的词元对? 可以通过注意?机制灵活地完成。
- 图示:
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
注意(Attending)
-
使用 加权平均 的“软”对齐:两个文本序列的词元对齐 -
理想情况下:较大的权重 与 要对齐的词元 相关联 -
软对齐中吗,att权重的计算方式为: -
这一分解技巧导致
f
f
f只有
m
+
n
m+n
m+n次计算(线性复杂度,只需计算出
m
+
n
m+n
m+n个不同的
f
f
f值),而不是
m
n
mn
mn次计算(二次复杂度,是将它们?对放在?起作为输?) -
-
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):
f_A = self.f(A)
f_B = self.f(B)
e = torch.bmm(f_A, f_B.permute(0, 2, 1))
beta = torch.bmm(F.softmax(e, dim=-1), B)
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
整合代码
- 定义 可分解att模型来联合训练这三个步骤(注意、比较和聚合)
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)
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
15.5.2 训练和评估模型
- 在 SNLI数据集 上对 定义好的可分解att模型 进行 训练和评估
读取数据集
- 下载并读取 SNLI数据集,批量大小为256,序列长度为50
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = load_data_snli(batch_size, num_steps)
创建模型
- 使用 预训练好的100维GloVe 来表示 输入词元
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
glove_embedding = TokenEmbedding('glove.6B.100d.txt')
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)
使用模型
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'
a = 'he is good .'
b = 'he is bad .'
predict_snli(net, vocab, a.split(), b.split())
-
小结
- 可分解注意模型包括三个步骤来预测前提和假设之间的逻辑关系:注意、?较和聚合。
- 通过注意?机制,我们可以将?个?本序列中的词元与另?个?本序列中的每个词元对?,反之亦然。这种对?是使?加权平均的软对?,其中理想情况下较?的权重与要对?的词元相关联。
- 在计算注意?权重时,分解技巧会带来??次复杂度更理想的线性复杂度。
- 我们可以使?预训练好的词向量作为下游?然语?处理任务(如?然语?推断)的输?表?。
-
缺点:
15.6 针对 序列级 和 词元级 应用程序 微调BERT
- 在空间和时间限制的情况下,为NLP应用设计了不同的模型,例如RNN,CNN,att和MLP
- 但为每个NLP任务 精心设计 一个特定的模型 是不可行的。
- BERT预训练模型:
- 对 广泛的NLP任务 进行最小的架构更改
- 提出时,改进了各种NLP任务的技术水平
- 原始BERT的两个版本参数量为1.1亿和3.4亿个参数,因此当有足够的计算资源时,我们可以考虑为下游NLP应用微调BERT
- NLP应用的子集——序列级 和 词元级
- 在序列层次上,有 单文本分类任务和文本对分类(或回归)任务,介绍了如何将输入的BERT表示 转换为 输出标签
- 在词元级别,新的应用——文本标注和问答,并说明bert如何表示 它们的输入 并转换为 输出标签
- 在微调期间,不同应用之间的bert所需的“最小架构更改”是额外的FC层。在下游应用的监督学习期间,额外层的参数是从零开始学习的,而预训练bert模型中的所有参数都是微调的。
15.6.1 单文本分类
-
概述:输入单个文本,输出其分类结果。如COLA数据集,判定给定的句子在语法上是否可以接受。 -
模型架构: -
BERT输?序列明确地表?单个?本和?本对,特殊分类标记<cls> ?于序列分类,?特殊分类标记<sep> 则标记单个?本的结束或分隔成对?本。 -
在单?本分类应?中,特殊分类标记<cls> 的BERT表? 对 整个输??本序列的信息 进?编码。作为 输?单个?本的表?,它将被送?到由 FC(Dense)层组成的?MLP 中,以输出 所有离散标签值的分布。
15.6.2 文本对分类或回归
15.6.3 文本标注
-
词元级任务:每个词元都被分配了一个标签。 -
在?本标注任务中,词性标注为 每个单词 分配词性标记(例如,形容词和限定词)。 -
根据单词在句?中的作?。如,在Penn树库II标注集中,句?“John Smith‘s car is new”应该被标记为“NNP(名词,专有单数)NNP POS(所有格结尾)NN(名词,单数或质量)VB(动词,基本形式)JJ(形容词)”。 -
模型架构: -
?本标记应?的BERT微调。与单文本分类应用相比,唯?的区别在于,在?本标注中,输??本的每个词元的BERT表? 被送到 相同的额外FC层中,以输出词元的标签,例如词性标签。
15.6.4 问答
-
词元级应用:问答 反应 阅读理解能力 -
例如,斯坦福问答数据集(Stanford Question Answering Dataset,SQuAD v1.1)由阅读段落和问题组成,其中每个问题的答案只是段落中的?段?本(?本?段)[Rajpurkar et al., 2016]。 -
SQuAD v1.1的?标 是在 给定问题和段落的情况下 预测 段落中?本?段的开始和结束。 -
模型架构: -
为了微调BERT进?问答,在BERT的输?中,将问题和段落分别作为第?个和第?个?本序列。 -
为了预测?本?段开始的位置,相同的额外的FC层将把 来?位置
i
i
i的任何词元的BERT表? 转换成 标量分数
s
i
s_i
si?。 -
?章中所有词元的分数 还通过softmax转换成 概率分布,从?为?章中的每个词元位置
i
i
i分配作为?本?段开始的概率
p
i
p_i
pi?。 -
预测 ?本?段的结束 与上?相同,只是 其额外的FC层中的参数 与 ?于预测开始位置的参数 ?关。当预测结束时,位置
i
i
i的词元由 相同的FC层 变换成 标量分数
e
i
e_i
ei?。 -
对于问答,监督学习的训练?标 就像 最?化真实值的开始和结束位置的对数似然 ?样简单。当预测?段时,我们可以计算 从位置
i
i
i到位置
j
j
j的有效?段的分数
s
i
+
e
j
(
i
≤
j
)
s_i + e_j(i ≤ j)
si?+ej?(i≤j),并输出分数最高的跨度。 -
小结
- 对于序列级和词元级NLP应?,BERT只需要最小的架构改变(额外的FC层),如单个?本分类(例如,情感分析和测试语?可接受性)、?本对分类或回归(例如,?然语?推断和语义?本相似性)、?本标记(例如,词性标记)和问答。
- 在下游应?的监督学习期间,额外层的参数是从零开始学习的,?预训练BERT模型中的所有参数都是微调的。
- 让我们为新闻?章设计?个搜索引擎算法。当系统接收到查询(例如,“冠状病毒爆发期间的?油?业”)时,它应该返回与该查询最相关的新闻?章的排序列表。假设我们有?个巨?的新闻?章池和?量的查询。为了简化问题,假设为每个查询标记了最相关的?章。如何在算法设计中应?负采样和BERT?
- BERT如何训练 LM 或 MT ?
15.7 自然语言推断:微调BERT
-
之前,我们为 SNLI数据集上的?然语?推断任务 设计了?个基于att的结构。 -
现在,通过 微调BERT 来重新审视这项任务。正如在 上一节中讨论的那样,?然语?推断 是?个序列级别的?本对分类问题,?微调BERT只需要?个额外的基于MLP的架构,如下图: -
本节将下载 ?个预训练好的小版本的BERT,然后对其进?微调,以便 在SNLI数据集上 进? ?然语?推断。
import json, multiprocessing, os, torch, re
from torch import nn
from d2l import torch as d2l
15.7.1 加载预训练的BERT
- 在此之前,我们已经在WikiText-2数据集上预训练BERT(原始的BERT模型是在更?的语料库上预训练的)。原始的BERT模型有数以亿计的参数。
- 在下?,我们提供了两个版本的预训练的BERT:“bert.base”与原始的BERT基础模型?样?,需要?量的计算资源才能进?微调,?“bert.small”是?个?版本,以便于演?。
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')
- 两 个 预 训 练 好 的BERT模 型 都 包 含
- ? 个 定 义 词 表 的 “vocab.json” ? 件
- ? 个 预 训 练 参 数 的“pretrained.params”?件。
- 实现了 以下load_pretrained_model函数 来加载 预先训练好的BERT参数:
def load_pretrained_model(pretrained_model, num_hiddens, ffn_num_hiddens,
num_heads, num_layers, dropout, max_len, devices):
vocab = d2l.Vocab()
vocab.idx_to_token = json.load(open('./'+ pretrained_model + '.torch/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.load_state_dict(torch.load('./' + pretrained_model + '.torch/pretrained.params'))
return bert, vocab
- 为了便于在?多数机器上演?,我们将在本节中加载和微调 经过预训练BERT的?版本(“bert.mall”)。
- 在练习中,我们将展?如何微调?得多的“bert.base”以显著提高测试精度。
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)
15.7.2 微调BERT的数据集
-
对于SNLI数据集的下游任务?然语?推断,我们定义了?个定制的数据集类SNLIBERTDataset。 -
在每个样本中,前提和假设形成?对?本序列,并被打包成 ?个BERT输?序列,如下图所?: -
片段索引?于区分BERT输?序列中的前提和假设。利? 预定义的BERT输?序列的最??度(max_len),持续移除输??本对中较??本的最后?个标记,直到满?max_len。 -
为了加速 ?成?于微调BERT的SNLI数据集,我们使?4个?作进程并行?成训练或测试样本。
class SNLIBERTDataset(torch.utils.data.Dataset):
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(1)
out = pool.map(self._mp_worder, 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_worder(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.vacab[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):
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)
if __name__ == "__main__":
batch_size, max_len, num_workers = 128, 128, d2l.get_dataloader_workers()
num_workers = 0
data_dir = './snli_1.0/'
train_set = SNLIBERTDataset(read_snli(data_dir, True), max_len, vocab)
test_set = SNLIBERTDataset(read_snli(data_dir, False), max_len, 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, num_workers=num_workers)
15.7.3 微调BERT
- 如下图所?,?于?然语?推断的微调BERT只需要?个额外的MLP,该MLP由两个FC层组成(请参?下?BERTClassifier类中的self.hidden和self.output)。
- 这个MLP将特殊的
<cls> 词元的BERT表?进?了转换,该词元同时编码 前提和假设的信息 为?然语?推断的三个输出:蕴涵、?盾和中性。
class BERTClassifier(nn.Module):
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, :]))
- 在下?中,预训练的BERT模型bert被送到?于下游应?的BERTClassifier实例net中。
- 在BERT微调的常?实现中,只有 额外的多层感知机(net.output)的输出层的参数 将从零开始学习。
- 预训练BERT编码器(net.encoder)和额外的多层感知机的隐藏层(net.hidden)的所有参数 都将进?微调。
net = BERTClassifier(bert)
- 回想?下,在 14.8节中,MaskLM类和NextSentencePred类 在其使?的MLP中都有?些参数。这些参数是 预训练BERT模型bert中参数的?部分,因此是net中的参数的?部分。然?,这些参数仅?于计算预训练过程中的masked-LM损失和NSP损失。
- 这两个损失函数与微调下游应?无关,因此当BERT微调时,MaskLM和NSP中采?的MLP的参数不会更新(陈旧的,staled)。
- 为了允许具有陈旧梯度的参数,标志ignore_stale_grad=True在step函数d2l.train_batch_ch13中被设置。我们通过该函数使?SNLI的训练集(train_iter)和测试集(test_iter)对net模型进?训练和评估。
- 由于计算资源有限,训练和测试精度 可以进?步提?:我们把对它的讨论留在练习中。
lr, num_epochs = 1e-4, 5
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
d2l.train_batch_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
- 小结
- 我们可以针对下游应?对 预训练的BERT模型 进?微调,例如在SNLI数据集上进??然语?推断。
- 在微调过程中,BERT模型成为下游应?模型的?部分。仅与训练前损失相关的参数在微调期间不会更新。
- 如果您的计算资源允许,请微调?个更?的预训练BERT模型,该模型与原始的BERT基础模型?样?。修改load_pretrained_model函数中的参数设置:将“bert.mall”替换为“bert.base” ,将num_hiddens=256、ffn_num_hiddens=512、num_heads=4和num_layers=2的值分别增加到768、3072、12和12。通过增加微调迭代轮数(可能还会调优其他超参数),你可以获得?于0.86的测试精度吗?
- 如何根据?对序列的?度?值截断它们?将此对截断?法与SNLIBERTDataset类中使?的?法进??较。它们的利弊是什么?计算复杂度?对短文本不利?
|