⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议。
Skip to content

第27章预训练语言模型

习题27.1

  设计基于双向LSTM的预训练语言模型,假设下游任务是文本分类。

解答:

解答思路:

  1. 设计基于双向LSTM的预训练模型ELMo,并加载模型权重
  2. 设计基于ELMo的文本分类模型
  3. 自编程实现基于ELMo的文本分类

解答步骤:

第1步:设计基于双向LSTM的预训练模型ELMo,并加载模型权重

  ELMo是一个双向LSTM模型,它首先使用大规模文本语料库进行预训练,当用于下游任务时,其输出表示通常作为上下文相关的词向量。(论文参考链接:https://arxiv.org/abs/1802.05365)

  ELMo有多个不同模型大小的变体,这里我们以small版本为例,详见:https://allenai.org/allennlp/software/elmo.

python
import wget
import os
from allennlp.modules.elmo import Elmo
python
elmo_options_file = './data/elmo_2x1024_128_2048cnn_1xhighway_options.json'
elmo_weight_file = './data/elmo_2x1024_128_2048cnn_1xhighway_weights.hdf5'
url = "https://s3-us-west-2.amazonaws.com/allennlp/models/elmo/2x1024_128_2048cnn_1xhighway/elmo_2x1024_128_2048cnn_1xhighway_options.json"
if(not os.path.exists(elmo_options_file)):
    wget.download(url, elmo_options_file)
url = "https://s3-us-west-2.amazonaws.com/allennlp/models/elmo/2x1024_128_2048cnn_1xhighway/elmo_2x1024_128_2048cnn_1xhighway_weights.hdf5"
if(not os.path.exists(elmo_weight_file)):
    wget.download(url, elmo_weight_file)
python
# 加载模型
elmo = Elmo(elmo_options_file, elmo_weight_file, 1)
python
elmo
Elmo(
  (_elmo_lstm): _ElmoBiLm(
    (_token_embedder): _ElmoCharacterEncoder(
      (char_conv_0): Conv1d(16, 32, kernel_size=(1,), stride=(1,))
      (char_conv_1): Conv1d(16, 32, kernel_size=(2,), stride=(1,))
      (char_conv_2): Conv1d(16, 64, kernel_size=(3,), stride=(1,))
      (char_conv_3): Conv1d(16, 128, kernel_size=(4,), stride=(1,))
      (char_conv_4): Conv1d(16, 256, kernel_size=(5,), stride=(1,))
      (char_conv_5): Conv1d(16, 512, kernel_size=(6,), stride=(1,))
      (char_conv_6): Conv1d(16, 1024, kernel_size=(7,), stride=(1,))
      (_highways): Highway(
        (_layers): ModuleList(
          (0): Linear(in_features=2048, out_features=4096, bias=True)
        )
      )
      (_projection): Linear(in_features=2048, out_features=128, bias=True)
    )
    (_elmo_lstm): ElmoLstm(
      (forward_layer_0): LstmCellWithProjection(
        (input_linearity): Linear(in_features=128, out_features=4096, bias=False)
        (state_linearity): Linear(in_features=128, out_features=4096, bias=True)
        (state_projection): Linear(in_features=1024, out_features=128, bias=False)
      )
      (backward_layer_0): LstmCellWithProjection(
        (input_linearity): Linear(in_features=128, out_features=4096, bias=False)
        (state_linearity): Linear(in_features=128, out_features=4096, bias=True)
        (state_projection): Linear(in_features=1024, out_features=128, bias=False)
      )
      (forward_layer_1): LstmCellWithProjection(
        (input_linearity): Linear(in_features=128, out_features=4096, bias=False)
        (state_linearity): Linear(in_features=128, out_features=4096, bias=True)
        (state_projection): Linear(in_features=1024, out_features=128, bias=False)
      )
      (backward_layer_1): LstmCellWithProjection(
        (input_linearity): Linear(in_features=128, out_features=4096, bias=False)
        (state_linearity): Linear(in_features=128, out_features=4096, bias=True)
        (state_projection): Linear(in_features=1024, out_features=128, bias=False)
      )
    )
  )
  (_dropout): Dropout(p=0.5, inplace=False)
  (scalar_mix_0): ScalarMix(
    (scalar_parameters): ParameterList(
        (0): Parameter containing: [torch.FloatTensor of size 1]
        (1): Parameter containing: [torch.FloatTensor of size 1]
        (2): Parameter containing: [torch.FloatTensor of size 1]
    )
  )
)

  从模型结构可以看到,ELMo主要由一个token编码器、一个双向双层LSTM和输出层构成。其中token编码器为输入的token计算输入向量表示,然后双向双层LSTM计算隐层(状态),最后,输出层将输入向量表示以及两层LSTM的隐层(状态)进行混合,得到最后的输出表示。

第2步:设计基于ELMo的文本分类模型

  文本分类模型主要由预训练模型ELMo、基于双向LSTM的编码器、全连接层分类器三部分组成。

  假设输入单词序列是x={x1,x2,,xl},输出是类别 y,文本分类任务旨在计算条件概率 P(y|x)=P(y|x1,x2,,xl)

  首先,将单词序列 x 输入到预训练模型ELMo,获得每个token的输入表示x={x1,x2,,xl }。

  结合习题25.4,基于双向LSTM的编码器如下所示:

  前向的LSTM的隐层(状态):

htf=LSTMf(xt,ht1f)

  后向的LSTM的隐层(状态):

htb=LSTMb(xt,ht+1b)

其中,LSTMfLSTMb 分别表示前向LSTM和后向LSTM,ht1fht+1b 分别表示前向LSTM和后向LSTM的上一个位置的隐藏状态。

这里,将两者的拼接表示为:

ht=[hif;hib]

其中,;表示两个向量的拼接。

  然后,将所有token的表示进行Mean-Pooling,得到输入单词序列x的表示h:

h=Mean-Pool(h1,h2,,hl)

  最后,将这一表示喂入分类器,获得每个类别的概率:

p=softmax(fc(h))

  在训练时,通常将预训练模型ELMo的参数进行固定,然后使用交叉熵作为损失函数,优化基于双向LSTM的编码器和全连接层分类器。

第3步:自编程实现双向LSTM的预训练语言模型

python
import time

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.data.dataset import random_split
from torchtext.data.functional import to_map_style_dataset
from torchtext.datasets import AG_NEWS
from allennlp.modules.elmo import batch_to_ids
python
label_pipeline = lambda x : int(x) - 1
python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def collate_batch(batch):
    label_list, text_list = [], []
    for (_label, _text) in batch:
        label_list.append(label_pipeline(_label))
        text_list.append(_text.split())
    label_list = torch.tensor(label_list, dtype=torch.int64)
    return label_list.to(device), text_list
python
# 加载AG_NEWS数据集
train_iter, test_iter = AG_NEWS(root='./data')
train_dataset = to_map_style_dataset(train_iter)
test_dataset = to_map_style_dataset(test_iter)
num_train = int(len(train_dataset) * 0.95)
split_train_, split_valid_ = \
    random_split(train_dataset, [num_train, len(train_dataset) - num_train])


BATCH_SIZE = 128
train_dataloader = DataLoader(split_train_, batch_size=BATCH_SIZE,
                              shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid_, batch_size=BATCH_SIZE,
                              shuffle=False, collate_fn=collate_batch)
test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE,
                             shuffle=False, collate_fn=collate_batch)
python
class TextClassifier(nn.Module):
    def __init__(self, embedding_dim, hidden_dim, num_classes):
        super().__init__()
        # 使用预训练的ELMO
        self.elmo = elmo

        # 使用双向LSTM
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, bidirectional=True, batch_first=True)

        # 使用线性函数进行文本分类任务
        self.fc = nn.Linear(hidden_dim * 2, num_classes)

        self.dropout = nn.Dropout(0.5)
        self.init_weights()

    def init_weights(self):
        initrange = 0.1
        self.fc.weight.data.uniform_(-initrange, initrange)
        self.fc.bias.data.uniform_(-initrange, initrange)
    
    def forward(self, sentence_lists):
        character_ids = batch_to_ids(sentence_lists)
        character_ids = character_ids.to(device)

        embeddings = self.elmo(character_ids)
        embedded = embeddings['elmo_representations'][0]

        x, _ = self.lstm(embedded)
        x = x.mean(1)
        x = self.dropout(x)
        x = self.fc(x)
        return x
python
EMBED_DIM = 256
HIDDEN_DIM = 64
NUM_CLASSES = 4
LEARNING_RATE = 1e-2
# 由于单个epochs运行时间达到5分钟,
# 有条件的小伙伴可以设置更多的epoch,达到更好的训练效果
NUM_EPOCHS = 1
python
model = TextClassifier(EMBED_DIM, HIDDEN_DIM, NUM_CLASSES).to(device)
python
def train(dataloader):
    model.train()

    for idx, (label, text) in enumerate(dataloader):
        optimizer.zero_grad()
        predicted_label = model(text)
        loss = criterion(predicted_label, label)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
python
def evaluate(dataloader):
    model.eval()
    total_acc, total_count = 0, 0

    with torch.no_grad():
        for idx, (label, text) in enumerate(dataloader):
            predicted_label = model(text)
            total_acc += (predicted_label.argmax(1) == label).sum().item()
            total_count += label.size(0)

    return total_acc/total_count
python
# 使用交叉熵损失函数
criterion = nn.CrossEntropyLoss().to(device)
# 设置优化器
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
python
for epoch in range(1, NUM_EPOCHS + 1):
    epoch_start_time = time.time()
    train(train_dataloader)
    accu_val = evaluate(valid_dataloader)
    print('-' * 59)
    print('| end of epoch {:3d} | time: {:5.2f}s | '
          'valid accuracy {:8.1f}% '.format(epoch,
                                           time.time() - epoch_start_time,
                                           accu_val * 100))
    print('-' * 59)
-----------------------------------------------------------
| end of epoch   1 | time: 321.49s | valid accuracy     90.4% 
-----------------------------------------------------------
python
ag_news_label = {1: "World", 2: "Sports", 3: "Business", 4: "Sci/Tec"}
python
def predict(text):
    with torch.no_grad():
        output = model([text])
        return output.argmax(1).item() + 1
python
ex_text_str = """
Our younger Fox Cubs (Y2-Y4) also had a great second experience 
of swimming competition in February when they travelled over to 
NIS at the end of February to compete in the SSL Development 
Series R2 event. For students aged 9 and under these SSL 
Development Series events are a great introduction to 
competitive swimming, focussed on fun and participation whilst 
also building basic skills and confidence as students build up 
to joining the full SSL team in Year 5 and beyond.
"""

print("This is a %s news" %ag_news_label[predict(ex_text_str)])
This is a Business news

习题27.2

  假设GPT微调的下游任务是两个文本的匹配,写出学习的目标函数。

解答:

解答思路:

  1. 给出GPT模型的描述
  2. 给出文本匹配任务的描述
  3. 写出GPT微调的下游任务是两个文本匹配时的学习的目标函数

解答步骤:

第1步:给出GPT模型的描述

  根据书中第27章的本章概要的GPT模型的描述:

  GPT模型的输入是单词序列,可以是一个句子或一段文章。首先经过输入层,产生初始的单词表示向量的序列。之后经过 L 个Transformer解码层,得到单词表示向量的序列,GPT模型的输出是在单词序列各个位置上的条件概率。
  GPT预训练时,通过极大似然估计学习模型的参数。

Ltrain=i=1nlogPθ(xi|x1,x2,,xi1)

  GPT微调时,通过优化下游任务的目标函数,进一步调节模型的参数。

第2步:给出文本匹配任务的描述

  根据书中第27.2.2节的文本匹配任务的描述:

  如果下游任务是文本匹配,如判断两句话是否形成一问一答。输入单词序列是 x0,x1,,xl,输出是类别 y,仍然计算条件概率 P(y|x0,x1,,xl)。类别有两类,表示匹配或不匹配。这时单词序列 x0,x1,,xl 是两个单词序列合成的序列,如一个问句和一个答句合并而成。以特殊字符<cls>开始,中间以特殊字符<sep>间隔,最后以特殊字符<sep>结束。

第3步:写出GPT微调的下游任务是两个文本匹配时的学习的目标函数

  假设分别有两个文本,矩阵X1=(x1(1),x2(1),,xm1(1))和矩阵X2=(xm+1(2),xm+2(2),,xm+n1(2))分别表示两个文本的词嵌入的序列,矩阵E1=(e1(1),e2(1),,em1(1))和矩阵E2=(em+1(2),em+2(2),,em+n1(2))分别表示两个文本的位置嵌入序列。

  经过输入层,产生初始的单词表示向量的序列,分别表示为

H10=X1+E1H20=X2+E2

  将两个句子分别作为前句或后句,构造两个完整的文本:

  • 文本1:<Start>, x1(1),x2(1),,xm1(1), <Delim>, xm+1(2),xm+2(2),,xm+n1(2), <Extract>
  • 文本2:<Start>, xm+1(2),xm+2(2),,xm+n1(2), <Delim>, x1(1),x2(1),,xm1(1), <Extract>

分别进行GPT预训练模型:

H1(L)=transformer_decoder([H1(0);H2(0)])H2(L)=transformer_decoder([H2(0);H1(0)])

将得到单词表示向量的序列进行融合,使用线性层得到GPT模型的输出:

H(L)=H1(L)H2(L)=(h1(L),h2(L),hm+n(L))Pθ(xi|x1,x2,,xi1)=softmax(WxThi(L))=exp(wxiThi(L))xiexp(wxiThi(L))

在预训练阶段,目标函数是

LPT=i=1m+nlogPθ(xi|x1,x2,,xi1)

  微调时,下游任务是文本匹配,输入的单词序列是两个文本合的序列x1,x2,,xm+n,输出是类别y,以特殊字符<Start>开始,中间以特殊字符<Delim>间隔,最后以特殊字符<Extract>结束。

可得到微调的目标函数为

LLM=1m+ni=1m+nlogPθ(y|x1,x2,,xi1)

习题27.3

  设计一个2层卷积神经网络编码器和2层卷积神经网络解码器组成的自动编码器(使用第28章介绍的转置卷积)。

解答:

解答思路:

  1. 给出自动编码器的原理
  2. 给出转置卷积的定义
  3. 自编程实现2层卷积自动编码器

解答步骤:

第1步:自动编码器的原理

  根据书中第27.2.2节的自动编码器的描述:

  自动编码器(auto encoder)是用于数据表示的无监督学习的一种神经网络。自动编码器由编码器网络和解码器网络组成。 最基本的情况下,编码器和解码器分别都是一层神经网络

z=F(x)=a(WEx+bE)y=G(z)=a(WDx+bD)

其中,WE,WD是权重矩阵,bE,bD是偏置向量,a()是激活函数。
  学习时,目标函数是

L=1Ni=1NL(xi,yi)=1Ni=1NL(xi,G(F(xi)))

第2步: 转置卷积的定义

  根据书中第28.2.1节的转置卷积的定义:

  转置卷积(transposed convolution)也称为微步卷积(fractionally strided convolution)或反卷积(deconvolution),在图像生成网络、图像自动编码器等模型中广泛使用。卷积可以用于图像数据尺寸的缩小,而转置卷积可以用于图像数据尺寸的放大,又分别称为下采样或上采样。

  根据书中第28章的本章概要的转置卷积的运算:

对于任意一个卷积运算,存在对应的线性变换的矩阵 C。针对转置矩阵 CT,引入新的卷积运算,称为转置卷积。原始卷积和转置卷积是相互对应、互为反向的运算。原始卷积的卷积核是 W 时,转置卷积的卷积核是 rot180(W)。卷积核和转置卷积核之间有 rot180(rot180(W))=W 成立。

第3步:自编程实现2层卷积自动编码器

python
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import tqdm
from matplotlib import pyplot as plt
from torch import optim
from torch.utils.data import DataLoader
from torchvision.datasets import mnist
from torchviz import make_dot
python
class AutoEncoder(nn.Module):
    def __init__(self):
        super(AutoEncoder, self).__init__()
        # 2层卷积神经网络编码器
        self.encoder = nn.Sequential(
            nn.Conv2d(1, 16, 3, 2, 1),
            nn.ReLU(),
            nn.Conv2d(16, 32, 3, 2, 1),
            nn.ReLU()
        )
        # 2层卷积神经网络解码器
        self.decoder = nn.Sequential(
            # 使用转置卷积
            nn.ConvTranspose2d(32, 16, 3, 2, 1, output_padding=1),
            nn.ReLU(),

            nn.ConvTranspose2d(16, 1, 3, 2, 1, output_padding=1),
            nn.Sigmoid()
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x
python
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 使用MNIST数据集
train_set = mnist.MNIST('./data', transform=transforms.ToTensor(), train=True, download=True)
test_set = mnist.MNIST('./data', transform=transforms.ToTensor(), train=False, download=True)
train_dataloader = DataLoader(train_set, batch_size=32, shuffle=True)
test_dataloader = DataLoader(test_set, batch_size=8, shuffle=False)
python
model = AutoEncoder().to(device)

# 设置损失函数
criterion = nn.MSELoss()
# 设置优化器
optimizer = optim.Adam(model.parameters(), lr=1e-2)
python
# 显示模型结构
model
AutoEncoder(
  (encoder): Sequential(
    (0): Conv2d(1, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1))
    (3): ReLU()
  )
  (decoder): Sequential(
    (0): ConvTranspose2d(32, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), output_padding=(1, 1))
    (1): ReLU()
    (2): ConvTranspose2d(16, 1, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), output_padding=(1, 1))
    (3): Sigmoid()
  )
)
python
# 模型训练
EPOCHES = 10
for epoch in range(EPOCHES):
    for img, _ in tqdm.tqdm(train_dataloader, ):
        optimizer.zero_grad()

        img = img.to(device)
        out = model(img)
        loss = criterion(out, img)
        loss.backward()

        optimizer.step()
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:10<00:00, 180.98it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:09<00:00, 188.63it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:09<00:00, 189.27it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:10<00:00, 178.80it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:10<00:00, 185.14it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:09<00:00, 187.51it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:10<00:00, 179.53it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:10<00:00, 186.49it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:09<00:00, 189.39it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1875/1875 [00:09<00:00, 189.29it/s]
python
# 将生成图片和原始图片进行对比
for i, data in enumerate(test_dataloader):
    img, _ = data
    img = img.to(device)
    model = model.to(device)
    img_new = model(img).detach().cpu().numpy()
    img = img.cpu().numpy()
    plt.figure(figsize=(8, 2))
    for j in range(8):
        plt.subplot(2, 8, j + 1)
        plt.axis('off')
        plt.imshow(img_new[j].squeeze())
        plt.subplot(2, 8, 8 + j + 1)
        plt.axis('off')
        plt.imshow(img[j].squeeze())
    if i >= 2:
        break


png

png

png

python
def save_model_structure(model, device):
    x = torch.randn(1, 1, 28, 28).requires_grad_(True).to(device)
    y = model(x)
    vise = make_dot(y, params=dict(list(model.named_parameters()) + [('x', x)]))
    vise.format = "png"
    vise.directory = "./data"
    vise.view("2-Layer-CNN-AutoEncoder-Struction", cleanup=True)
python
# 保存模型架构图
save_model_structure(model, device)

习题27.4

  证明当编码器和解码器都是线性函数时,主成分分析可以作为自动编码器学习的方法。

解答:

解答思路:

  1. 给出主成分分析的定义
  2. 给出自动编码器学习过程
  3. 证明当编码器和解码器都是线性函数时,主成分分析可以作为自动编码器学习的方法

解答步骤:

第1步:主成分分析的定义

  根据书中第16章的主成分分析的基本思想:

  主成分分析中,首先对给定数据进行规范化,使得数据每一变量的平均值为0,方差为1,之后对数据进行正交变换,原来由线性相关变量表示的数据通过正交变换变成由若干个线性无关的新变量表示的数据。新变量是可能的正交变换中变量的方差的和(信息保存)最大的,方差表示在新变量上信息的大小。将新变量依次称为第一主成分、第二主成分等。这就是主成分分析的基本思想。

  根据书中第16.2.3节的算法16.1主成分分析算法:

算法16.1(主成分分析算法)
输入:m×n样本矩阵X,其每一行元素的均值为零;
输出:k×n样本主成分矩阵Y
参数:主成分个数k
(1)构造新的n×m矩阵

X=1n1XT

X每一列的均值为零。
(2)对矩阵X进行截断奇异值分解,得到

X=UΣVT

k个奇异值、奇异向量。矩阵V的前k列构成k个样本主成分。
(3)求k×n样本主成分矩阵

Y=VTX

第2步:自动编码器学习过程

  根据书中第27章的本章概要的自动编码器的描述:

  自动编码器是用于数据表示的无监督学习的一种神经网络。自动编码器由编码器网络和解码器网络组成。学习时编码器将输入向量转换为中间表示向量,解码器再将中间表示向量转换为输出向量。编码器和解码器可以是

z=F(x)=a(WEx+bE)y=G(z)=a(WDz+bD)

学习的目标是尽量使输出向量和输入向量保持一致,或者说重建输入向量。认为学到的中间表示向量就是数据的表示。

L=1Ni=1NL(xi,G(F(xi)))

学习的算法一般是梯度下降。自动编码器学习实际进行的是对数据的压缩。

第3步:证明当编码器和解码器都是线性函数时,主成分分析可以作为自动编码器学习的方法

  假设给定样本矩阵X=(x1,x2,,xN),根据主成分分析算法,可以得到主成分为K个对应的单位特征向量V,样本主成分矩阵Y满足

Y=VTX

这样,样本主成分矩阵Y可以作为样本矩阵X的低维表示,V维度是N×K,其中V=(v1,v2,,vN)

  根据自动编码器中的编码阶段,编码器对数据进行压缩,当编码器是线性函数时,可用主成分矩阵Y表示编码器的输出,即

z=F(x)=WEx+bE=VTx

其中WE=VT,bE=0

  在解码阶段,解码器通过解压可以得到原始数据的近似,当解码器是线性函数时,可用单位特征向量V表示WD,即

y=G(z)=WDz+bD=Vz

其中WD=V,bD=0

  综上所述,当损失函数是平方损失函数时,自动编码器学习的目标函数是

L=1Ni=1NxiG(F(xi))=1Ni=1Nxivi(viTxi)=1Ni=1NxiviviTxi

  由于单位特征向量V可以使得样本矩阵X的所有线性变换中方差最大,故单位向量特征V能够使目标函数最小,因此,当编码器和解码器都是线性函数,且bE=bD=0时,主成分分析可以作为自动编码器的学习方法。

习题27.5

  解释为什么BERT预训练中的掩码语言模型化是基于去噪自动编码器原理的。

解答:

解答思路:

  1. 给出BERT预训练中的掩码语言模型化描述
  2. 给出去噪自动编码器的原理
  3. 解释BERT预训练中的掩码语言模型化是基于去噪自动编码器原理的原因

解答步骤:

第1步:BERT预训练中的掩码语言模型化的描述

  根据书中第27.2.2节的关于BERT预训练中的掩码语言模型化的描述:

  BERT模型的预训练由两部分组成,掩码语言模型化和下句预测。掩码语言模型化的目标是复原输入单词序列中被掩码的单词。可以看作是去噪自动编码器学习,对被掩码的单词独立地进行复原。掩码单词序列表示为x~
  掩码语言模型化在每一个掩码位置计算条件概率:

Pθ(xi|x~0,x~1,,x~m+n)=softmax(WxThi(L))=exp(wxiThi(L))xiexp(wxiThi(L))

假设第i个位置是掩码位置,hi(L)是在第L层第i个位置的表示,xi是预测的单词,Wx是单词的权重矩阵。

第2步:去噪自动编码器的原理

  根据书中第27章的本章概要的去噪自动编码器的描述:

  去噪自动编码器是自动编码器的一种扩展,去噪自动编码器不仅可以用于数据表示学习,而且可以用户数据去噪。学习时首先根据对输入向量进行的随机变化,得到有噪声的输入向量。编码器将有噪声的输入向量转换为中间表示向量,解码器再将中间表示向量转换为输出向量。编码器、解码器、目标函数分别是

z=F(x)=a(WEx~+bE)y=G(z)=a(WDz+bD)L=1Ni=1NL(xi,G(F((~x)i)))

学习的目标是尽量使输出向量和原始输入向量保持一致,或者说重建原始输入向量。因为学习的目标是排除噪声的干扰重建数据,去噪自动编码器能更有效地学到数据的主要特征。

第3步:解释BERT预训练中的掩码语言模型化是基于去噪自动编码器原理的原因

  BERT在预训练阶段,输入序列中的一些单词被随机遮掩,掩盖单词的过程相当于向输入序列中添加了一些噪声,得到有噪声的输入向量,模型需要预测并复原这些单词。这个过程类似于去噪自动编码器的学习过程,其中模型需要从缺失的或噪声的数据中重构原始数据。因此,掩码语言模型化提供了一些“噪音”,让模型学会从不完整的输入中推断出缺失的信息,从而提高模型的泛化能力。这也就说明了BERT预训练中的掩码语言模型化是基于去噪自动编码器原理的。

习题27.6

  比较BERT与Transformer编码器在模型上的异同。

解答:

解答思路:

  1. 给出BERT的模型描述
  2. 给出Transformer编码器的模型描述
  3. 比较BERT与Transformer编码器在模型上的异同

解答步骤:

第1步:BERT模型的描述

  根据书中第27.2.2节的关于BERT的描述:

  BERT是双向Transformer编码器表示(Bidirectional Encoder Representations from Transformers)的缩写。BERT的模型基于Transformer的编码器,是双向语言模型。BERT的预训练主要是掩码语言模型化,使用大规模语料基于自去噪自动编码器原理进行模型的参数估计,学习的目标是复原给定的掩码单词序列中被掩码的每一个单词。学习和预测都是非自回归过程(Non-autoregressive process)。   每一个单词序列是一个句子或一段文章。首先经过输入层,产生初始的单词表示向量的序列,记作矩阵H(0)

(27.22)H(0)=X+S+E

其中,矩阵X表示单词的词嵌入的序列X=(x0,x1,,xm+n);矩阵E表示单词的位置嵌入的序列E=(e0,e1,,em+n);矩阵S是区别前后单词的标记序列S=(a,a,,a,b,b,,b),含有m+1个向量an个向量bX,E,S,H(0)d×(m+n+1)矩阵,设词嵌入、位置嵌入、标记向量的维度是d

  根据书中第27章的本章概要的BERT模型的描述:

  BERT模型的输入是两个合并的单词序列。首先经过输入层,产生初始的单词表示向量的序列。之后经过L个Transformer编码层,得到单词的表示向量的序列。BERT模型的输出是在单词序列的各个位置上的条件概率。
  BERT模型的预训练由掩码语言模型化和下句预测组成。掩码语言模型化的目标是复原输入单词序列中被掩码的单词。下句预测的目标是判断输入单词序列是否来自同一篇文章。预训练以掩码语言模型化为主,其损失函数是

L1=i=0m+nδilogPθ(xi|x~0,x~1,,x~m+n)

  BERT微调时,通过优化下游任务的目标函数,进一步调节模型的参数。

第2步:Transformer编码器的模型描述

  根据书中第26.3.1节关于Transformer编码器的描述:

  Transformer(转换器)由编码器和解码器组成。编码器有1个输入层、6个编码层(一般是L层)。解码器有1个输入层、6个解码层、1个输出层。编码器的输入层与第1个编码层连接,第1个编码层再与第2个编码层连接,依次连接,直到第6个编码层。解码器的输入层与第1个解码层连接,第1个解码层再与第2个解码层连接,依次连接,直到第6个解码层。第6个解码层再与输出层连接。第6个编码层与各个解码层之间也有连接。
  编码器的6个编码层将输入单词序列进行转换,得到中间表示序列。
  编码器的6个编码层有相同的结构,每一个编码层由自注意力子层和前馈网络子层两部分组成。
  在编码器的输入层,输入序列的各个位置有单词的词嵌入(word embedding)和位置嵌入(position embedding),其中位置嵌入表示在序列中的位置。在每一个位置以词嵌入和位置嵌入的和作为该位置的输入向量。单词的词嵌入通常通过对单词的独热向量进行一个线性变化得到,即用一个矩阵乘以独热向量,矩阵称为词嵌入矩阵。

  根据书中第26.3.1节关于Transformer编码器计算公式:

  在编码器的输入层通过线性变换获得单词的词嵌入。

(26.26)e=Wew

其中,w是单词的独热向量,e是单词的词嵌入,We是嵌入矩阵。嵌入矩阵在学习中自动获得。
  编码器的输入层的每一个位置的输入向量是

(26.27)e+p

其中,e是该位置的词嵌入,p是该位置的位置嵌入。

  编码器的每一层的每一个位置的前馈网络是

(26.28)ffn(z)=W2relu(W1z+b1)+b2

其中,W1W2是权重矩阵,b1b2是偏置向量。

  编码器的每一层的每一个位置的层归一化函数是

norm(z)=γzu1σ2+ε+β1

其中,u是均值,σ2是方差,γβ是参数,ε是常量。
  编码器的输入层的计算可以写作

(26.31)HE(0)=EE+PE

  编码器的第l个编码器的多头自注意力子层和前馈网络子层计算可以写作

(26.32)ZE(l)=norm(HE(l1)+multi_attend(HE(l1),HE(l1),HE(l1)))(26.33)HE(l)=norm(ZE(l)+ffn(ZE(l)))

其中,HE(l)是第l个编码层的所有位置的输出,HE(l1)是所有位置的输入,ZE(l)是中间结果;ffn()norm()的计算针对矩阵的每一列,multi_attend()的计算针对矩阵整体进行。

第3步:比较BERT与Transformer编码器在模型上的异同

  • 相同点:

    1. 两者都是基于多头注意力机制的架构设计,使用了自注意力机制来处理输入序列。
    2. 两者都使用了残差连接和层归一化。
    3. 两者都可以使用预训练技术提高模型的泛化能力。
  • 不同点:

    1. BERT模型在Transformer模型的基础上添加了掩码语言模型和下句预测任务。
    2. BERT模型在训练的时候会在其后添加线性层和softmax层,用于预测掩码语言模型和下句预测任务;Transformer编码器则是直接和Transformer解码器连接,用于预测序列的下一个单词。
    3. BERT模型中输入序列的嵌入由词嵌入、位置嵌入、区别前后单词的标记序列的和组成;Transformer编码器模型的输入序列的嵌入由词嵌入、位置嵌入的和组成。
    4. BERT模型使用了双向语言模型,能够同时处理输入序列的左右两个方向;Transformer编码器通常只处理从左到右的输入序列。
    5. BERT模型的位置嵌入通过学习得到;Transformer的位置嵌入由公式计算得到。

参考文献

【1】Matthew E. Peters and Mark Neumann and Mohit Iyyer and Matt Gardner and Christopher Clark and Kenton Lee and Luke Zettlemoyer. 2018. Deep contextualized word representations. arXiv, abs/1802.05365
【2】ELMo:https://allenai.org/allennlp/software/elmo