深度学习原理篇 第一章:transformer入门

简介: 简要介绍词向量和transformer原理。

参考教程:

从Embedding开始

词的向量化

transformer最初主要还是用于NLP任务中。在NLP任务中,对于一个文本的输入,通常会使用embedding对它进行一个向量化的表示。
embedding的目标,就是用一组向量来刻画现有的数据集。那么就对这个embedding有了一点要求。

  • 要有词的先后顺序。
  • 相似的词在向量空间的表达应该也是相似的。
  • 同一类的词在空间内应该是相近的。

第一个要求很好理解,比如你可以说“这个房子里有一条狗”但是你不能说“这条狗里有一个房子”。顺序的改变是会影响文本的含义的,因此我们希望对文本中单词进行编码时,能记录他们的位置信息。
第二个要求举个例子,“月亮”和“月球”两个词虽然组成它们的文字不一样,但是它们都是“月”的一个称呼,意思也都是“月”。因此我们希望它们的向量也比较相近。
第三个要求是限制同一类词的距离,比如说“篮球,足球,泰山”。那么篮球和足球作为一种体育用品,并且都是球类,它们两者在向量空间内肯定是比它们和泰山的距离要近的。泰山和这两个词本来就是八竿子打不着,如果它距离篮球比足球距离篮球还要近,这就很违背常人的逻辑印象。

同时呢,我们也希望得到的这个embedding的向量表示的维度也相对高一点,维度高意味着信息更多,那么在使用这个向量表示时,我们得到的结果也会相对更可靠一些。

词向量模型

我们的词向量,也是是文本中词汇的embedding化表示,是可以通过多种方法实现的。比如说简单的one-hot编码,又或者使用频率表示。但是这两种方法都是有缺陷的。

  • 使用one-hot编码,假如你的词库非常大,那么为了表示一个单词,就需要一个极其长的向量表示,造成极大的内存浪费。
  • 频率是通过用某单词在文章中出现次数除以文章的单词个数获得的,它的优点是简单快速,但是忽略了词的位置信息和相关性。

我们可以通过所谓的词向量模型来获得词向量。
词向量模型是一种需要训练的模型,使用这个模型,可以帮助我们获得对词库中的单词的向量化表达。这种模型的输入也是一个embedding,可以理解成对指定单词的一个标志,用于把单词区分开来。而它的输出结果是一个概率,表示在给定输入的情况下,最可能返回的单词是什么。

当然我们想要的并不是这个输出的结果,而是藏在模型中间的embedding。

整体的过程可以理解为,我们想要的embedding是这个模型的一个中间结果,它会随着模型的更新迭代而变化,我们判断这个embedding是否好用的根据是模型的输出结果是否准确。

而在这个任务中,模型的输入是给定的文本编码,模型的输出是目标单词出现的概率。所以它其实是一个多分类的问题,最终的概率是由softmax函数得到的。在你的分类目标很多的情况下,softmax计算会很麻烦,因此又有一个优化的方法:

输入A和B两个单词,判断这两个词是不是前后邻居。

这样这个任务就变成了二分类的任务。

过程介绍

数据的获取

常用的方法是:
使用滑动窗口构建输入数据。
image.png

从别的地方偷了一张图。这里就给出了一个很直观的例子。
在上面一节我们说词向量的模型输入是给定的文本编码,输出是目标单词的概率。而图中就是一个用前文预测下文的例子,它的前文单词数长度为2,也就说说用前两个词预测第三个词。
对于一段文本,从第一个单词开始,用连续的两个词预测下一个词,所以才会有inputs是“thou shalt”,output(target)是“not”。

具体input和output是如何获得的,还是由使用者自行决定的。除了用前n个词预测下一个词外,也可以使用前n个加后n个词,来预测中间的词。

word2vec

常用的两种模型介绍

  • CBOW:输入目标单词前面和后面的n个词,进行单词本身的预测。
  • skip-gram:输入一个单词,预测它前n个词和后n个词。

下方左边就是CBOW的例子,右边是Skip-gram的例子。
image.png

两个模型的原理是相似的。

对于CBOW,我们传入四个单词的编码(这里的编码不是我们想要的embedding,只是为了区分不同单词的标号),通过模型获得四个单词的embedding,对这个embedding进行处理,并用于预测四个单词正中间的单词,获得“目标单词为词库中的第n个词的概率”,最大概率对应的单词就是我们的输出。

对于Skip-gram,我们传入中间单词的编码,通过模型获得embedding,再用于预测它周围的四个单词。

代码介绍

torch.nn.Embedding()

torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None, max_norm=None, norm_type=2.0, scale_grad_by_freq=False, sparse=False, _weight=None, _freeze=False, device=None, dtype=None)

先来看一下这个class中的参数都有什么用。

  • 第一个参数 num_embeddings:代表你的embedding有多少个,也可以等价于你期望的词库大小。
  • 第二个参数embedding_dim是你希望得到的embedding向量的维度。
  • padding_idx:可以指定不用于梯度更新的index的位置。也就是在模型训练的过程中,别的位置会随着backward更新,而这个位置不会。
  • max-norm:用于对你的vector进行归一化
  • p-norm:归一化使用L2还是L1。
  • _weight: 你可以自己传入一个weights,这里的weight就是生成的embedding矩阵的值,如果这个weight和你预设的大小不一致,也会重新生成。

它的前向传播得到的结果是

def forward(self, input: Tensor) -> Tensor:
        return F.embedding(
            input, self.weight, self.padding_idx, self.max_norm,
            self.norm_type, self.scale_grad_by_freq, self.sparse)

对于在前向传播中使用的函数F.embedding,它的解释是This module is often used to retrieve word embeddings using indices. The input to the module is a list of indices, and the embedding matrix, and the output is the corresponding word embeddings. 也就是说我们传入的input是一组index,F.embedding会根据这个index去我们的weight中取vector。
比如你用的index是[1,3,5],返回结果就是self.weights的第二行,第四行和第六行,三行长度为embedding_dim的向量。

代码实现

代码实现可以直接参考pytorch tutorial中给出的例子,在这里会对代码进行解释。

下面是一个CBOW的例子

首先获取我们的数据。完成输入输出对的构建。

context_size = 2   # 代表我们会使用前面和后面各两个单词在预测中间的词。
raw_text = """...""".split()  # 原始文章的string,这里因为字数太多没有列出来,可以直接替换成自己的
vocab = set(raw_text) # 文章中都使用了那些单词,这个也就是我们的词库
vocab_size = len(vocab) # 我们的词库大小

word_to_ix = {
   
   word:i for i, word in enumerate(vocab)} # 这里构建了一个字典,该字典是key是单词,value是单词的index。

# 下一步来构建我们的dataset
for i in range(context_size, len(raw_text) - context_size):
# 遍历整个文章
    context = (
    [raw_text[i-j-1] for j in range(context_size)]  # 第i个单词的前context_size个词
    + [raw_text[i+j+1] for j in range(context_size)] # 第i个单词的后context_size个词
    )
    target = raw_text[i] # 第i个单词
    data.append((context, target)) # 将我们的input和target按对放入data中

然后定义我们的模型。
在这里给出一个forward流程的更详细的介绍。
假如我们输入的是[2,1,3,4]。target是0。预设的vocab_size = 500, embedding_dim = 64。
那么我们期望得到的一个整个词库的embedding大小就是500*64,也就是nn.Embedding中weights的大小。
现在输入[2,1,3,4],经过第一步self.embeddings(inputs),得到的结果大小为4*64。然后求和并用view(1,-1)后,得到的结果为1*64,也就是四个embedding的融合。
经过两个线性层后,得到的out大小是1*500。

class CBOW(nn.Module):
    # 定义CBOW类,继承nn.Module
    def __init__(self, vocab_size, embedding_dim):
        super(CBOW,self).__init__()
        # vocab_size: 我们的词库的大小,因为输出结果是在每个词上的概率,所以如果直接用softmax求概率,我们的输出维度应该=词库大小。
        # embedding_dim:你自己决定的embedding向量的长度。
        self.embeddings = nn.Embedding(vocab_size, embedding_dim) # 我们想要的embedding结果在这里,它可以看作是一个高为vocab_size, 高为embedding_dim的矩阵。
        self.proj = nn.Linear(embedding_dim, 128) # 中间层
        self.output = nn.Linear(128, vocab_size) # 用于输出的全连接层,对vocab_size个类别进行预测。
    def forward(self, inputs):
        embeds = sum(self.embeddings(inputs)).view(1,-1) # 找到inputs的embedding并求和处理
        out = F.relu(self.proj(embeds)) # 中间结果
        out = self.output(out) # 预测的pred
        prob = F.log_softmax(out, dim=-1) # 这里直接进行了softmax计算。
        return prob

在上面的部分中解释过这里的nn.Embedding()在进行前向传播时,传入的input是词的index。而我们刚刚构建的dataset中datat[i][0]是输入的词,data[i][1]是对应的target。并不是index。所以我们要对输入再转换一下。

def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]  # 从我们之前创建的word_to_index的dict中根据word取index
    return torch.tensor(idxs, dtype=torch.long) # 要注意输入是tensor


make_context_vector(data[0][0], word_to_ix)  # example

可以看一下转换的结果
image.png

下方就是一个比较常规的训练代码。

EMBEDDING_DIM = 64

loss_function = nn.NLLLoss()
model = CBOW(len(vocab), EMBEDDING_DIM)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
    total_loss = 0
    for context, target in data:
        context_idxs = make_context_vector(context, word_to_ix)

        optimizer.zero_grad()
        log_probs = model(context_idxs)

        loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

        loss.backward()
        optimizer.step()

        total_loss += loss.item()

这里给出了一个可视化的例子,因为训练的轮数比较少,得到的embedding可能不是很准确,但是也勉强可以看到单词‘we’和‘We’比‘computer’要更接近。
image.png

Transformer 与 self-attention

self-attention

传统的向量会遇到一个问题,就是在生成了embedding后,这个embedding不会发生变化了。那么在不同的语境情况下,固定的embedding肯定是不合适的,它代表的语义不会随着情境改变,就会产生错误。

注意力的加入可以帮助解决这个问题,这样在不同的情境下,语句内部的元素的关注度是不一样的。

传统的注意力机制是发生在input和output之间的,而self-attention则是发生在输入的内部,或者输出的内部。相当于你输入一长串文字时,每个文字对上下文元素的关注度是不一样的,你可以通过self-attention来得到元素间的关注度。

image.png

在self-attention中有三个很重要的矩阵,分别是queries, keys, values。也就是上图中的q, k, v。

  • queries: 你要去查询的向量。
  • keys:要被查询的向量。
  • values: 实际的特征信息。

这个解释可能比较难理解。具体来讲,qi是你的目标,你想知道q对别的元素的关注度attention,那么你就用qi和别的所有的k进行比较,当然也包括了ki。

它们的获得方法也很简单,就是与权重矩阵相乘。下图为一个比较完整的步骤。

第一步,对于输入的向量I,分别与Wq, Wk和Wv相乘,得到备用的Q, K, V。
具体来说,假如你现在有N个1*C的输入,W系列的权重大小均是C*V,那么你分别会得到N个大小为1*V的向量。也就是你的{q1...qn},{k1...kn},{v1...vn}。
第二步,用Q与K进行内积,算出匹配度。上图给了更具体的细节,对于输入向量a1,除了用它的q1和k1做内积外,还要用它的q1和别的输入向量的k做内积。
因为一共有n个k,所以这一步完成后,你得到的是一个大小为1*n的向量。因为你除了a1外还有n-1个输出,所以整体的结果应该是一个n*n的矩阵,反应的是n个输入之间彼此的相关性。这个结果会用softmax处理,得到一个概率分布,代表每个元素相对当前目标的重要程度。
$$ softmax(\frac{Q\times K^T}{\sqrt{dk}}) $$
补充一下为什么要除以根号dk,因为长度很大的时候,点积的结果也会很大,那么在大数和小数差距很大的情况下,softmax容易被推到梯度很小的区域,所以这里除以dk来平衡一下。

第三步,将v按照上一步得到的结果进行加权求和后得到一个新的输出。
image.png

multi-head attention

一组q,k,v能得到一个当前特征的表达。假如有多个q, k, v 就能得到多个表达。每一组的计算过程仍然是一样的。
对于多组的输出,将结果直接concat在一起。
具体如下图所示。
我们的bi,1是通过[qi1ki1,qi1kj1]与vi1,vj1结果加权求和得到的。
我们的bi,2是通过[qi2ki2, qi2kj2]与vi2,vj2结果加权求和得到的。
image.png

两个结果concat起来,再使用全连接降维,就得到最终的输出。

position-embedding

上面讲到的输入a1, a2, ... an,我们是可以知道顺序的,但是在模型的计算中没有反应出这种顺序,甚至也利用不到这个信息。因此为了体现出前后顺序的区别,可以在输入ai中加入位置的编码信息。
image.png

Transformer

可以直接参考:https://zhuanlan.zhihu.com/p/460588687
我感觉解释得比较详细。
一个完整的transformer结构,包括了encoder和decoder两部分。
这两部分都是由堆叠的 multi-head attention + BN组成的。当然还有不可忽略的残差结构。

Encoder

编码器在原始输入序列中进行了两个预处理:

  • 词嵌入:将词转为成向量。
  • 位置编码:将词在序列中的位置信息加入考量。

预处理的结果会被传入到block里,一个block里面有两个子层。

  1. 多头注意力+残差和+层归一化
  2. 全连接+残差和+层归一化

写一个公式化的流程

# 输入ai 并生成position embedding pi
ai  = ai + pi # 把ai和pi合成一个新的ai
while n: # 假设有n个block

    x = multi_head_attention(ai)
    x = layer_norm(x + ai)

    x2 = feed_forward(x)
    x2 = layer_norm(x2 + x)
    ai = x
    n-=1

image.png

Decoder

解码器虽然和编码器看起来很相似,但是还是有一点区别。

  1. Masked Multi-Head Attention
    Decoder中第一个注意力机制使用的Masked Multi-Head Attention,Masked Multi-Head attention的计算顺序其实是和Decoder的串行计算顺序相对应。
    因为在进行预测的时候,我们并不是能直接看见所有的文本的,文本的结果是一个一个出现的,因此masked multi-head attention就是为了模拟这个过程【我是这么理解的,也可能不对】。
    你只能使用已知的信息做预测,所以masked multi-head attention遮住了理论上应该未知的信息。

  2. Cross Attention
    在这个地方,queries, keys, values来源不同。

    • keys: encoder输出中计算
    • values:encoder输出中计算
    • queries:decoder输出中计算。
      image.png

代码实现

参考教程:https://zhuanlan.zhihu.com/p/339207092

跟着教程写一遍,加深理解。

整体结构

首先要思考一下在这个过程中都需要完成哪些组件。

  • embedding,我们encoder和decoder的输入都是词向量,而非词本身,所以我们最开始要使用embeddinglayer进行单词的转换。
  • positionembedding,我们的输入要带有位置信息,因此还需要和位置进行编码。
  • 网络由多个block组成,每个block都是一样的结构。
    • multi-head attention和attention。
    • layernorm 用于对输出做归一化
    • 残差结构,输出要和输入相加。
    • feedforward部分,有的网络中也会使用更高级的forward,我还没有学到那里。
  • 对于decoder,它使用的是带mask的attention。

在这里就不多关注decoder了。

input-part

在数据传入网络前,我们要对它进行编码,并加上位置信息。因此这里需要一个embedding和一个positionembedding。

class Embedding(nn.Module):
    def __init__(self, embed_dim, vocab):
        super(Embedding, self).__init__()
        self.embed = nn.Embedding(vocal, d_model)
        self.embed_dim = embed_dim

    def forward(self, x):
        return self.embed(x)*math.sqrt(self.embed_dim)
        # 这里用权重乘以向量长度开根,其实不是很理解为啥。
def PositionalEncoding(nn.Module):
    def __init__(self, embed_dim, dropout, max_len = 5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, embed_dim)
        # pe是我们的位置编码,它是max_len个长度为embed_dim的vector。
        # 因为pe的位置编码是要和词向量直接相加的,所以他们的长度也要保持一致。
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, embed_dim, 2)*
                            (-math.log(10000.0)/embed_dim))
        pe[:,0::2] = torch.sin(position * div_term)
        pe[:,1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)


    def forward(self, x):
        # 这里的上限x.size(1)应该是怕出现维度过长的情况。
        # 可以看到这里是直接相加的,那么x应该是词向量。
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad = False)
        return self.dropout(x)

attention and multi-head attention

attention的计算是比较好理解的,对于得到的q,k,v。q和k做内积和softmax求概率值,然后和v加权求和。

def attention(query, key, value, mask = None, dropout = None):

    # 向量长度
    d_k = query.size(-1)
    # 内积
    scores = torch.matmul(query, key.transpose(-2,-1))/math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)

    p_attn = F.softmax(scores, dim = -1) # 求得不同元素相对目标的重要性

    if dropout is not None:
        p_attn = dropout(p_attn)

    # 特征加权求和。得到输出
    return torch.matmul(p_attn, value), p_attn

多头attention会把每个attention的结果concat在一起然后使用全连接层。多头attention计算的时候也是直接使用的矩阵,它的效率更高。并不需要我们用什么遍历的方法一个一个计算。

在这里的concat,也只是把结果reshape了一下就可以得到了。

class MultiHeadAttention(nn.Module):
    def __init__(self, h, embed_dim, dropout = 0.1):
        super(MultiHeadAttention, self).__init__()
        self.d_k = embed_dim//h # 这里是每个head关注embedding的一部分
        self.h = h # head的个数

        self.q = nn.Linear(embed_dim, embed_dim)
        self.k = nn.Linear(embed_dim, embed_dim)
        self.v = nn.Linear(embed_dim, embed_dim) 

        self.linear = nn.Linear(embed_dim, embed_dim) # 全连接的部分
        # 用于计算Q,K,V
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, q, k, v, mask = None):

        if mask is not None:
            mask = mask.unsqueeze(1)

        nbatches = query.size(0)

        query = self.q(q).view(nbatches, -1, self.h, self.d_k).transpose(1,2)
        key= self.k(k).view(nbatches, -1, self.h, self.d_k).transpose(1,2)
        value = self.v(v).view(nbatches, -1, self.h, self.d_k).transpose(1,2)

        x, self.attn = attention(query, key, value, mask = mask, dropout = self.dropout)

        x = x.transpose(1,2).contiguout().view(nbatches, -1, self.h * self.d_k)

        return self.linear(x)

LN + add

LN的作用是对同一物体的不同维度进行归一化。

class LayerNorm(nn.Module):
    def __init__(self, features, eps = 1e-6):
        super(LayerNrom,self).__init__()
        self.gamma = nn.Parameter(torch.ones(features))
        self.beta = nn.Parameter(torch.zeros(features))

        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim = True)
        std = x.std(-1, keepdim = True)
        return self.gamma * (x - mean) / (std+self.eps) + self.beta

add是一个残差结构,将输入和输出直接相加在一起。

class SublayerConnection(nn.Module):

    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, residual,  x):

        return self.norm(x + residual)
#     def forward(self, x, sublayer):
#         return x + self.dropout(sublayer(self.norm(x)))
# 在教程提供的代码中,x可以理解成上一层的未经过norm的输出。所以在这里先做了norm。
# 然后这里的sublayer是我们的的多头注意力,所以这里返回的是未作norm的残差和

encoderlayer

class EncoderLayer(nn.Module):

    def __init__(self, size, self_attn, feed_dorward, dropout):
        super(EncoderLayer, self).__init__()

        self.self_attn = self_attn  # 这个self_attn就是我们的多头attention
        self.feed_forward = feed_forward
        self.sublayer1 = SublayerConnection(size, dropout) # 我们的sublayer1是多头+add+norm
        self.sublayer2 = SublayerConnection(size, dropout) # 我们的sublayer2是feedforward+add+norm
        self.size = size

    def forward(self, x, mask):

        x1 = self.self_attn(x, x, x, mask)
        # 得到新的value
        x1 = self.sublayer1(x, x1)

        x2 = self.feed_forward(x1)
        x2 = self.sublayer2(x1, x2)

        return x2
相关文章
|
2月前
|
机器学习/深度学习 算法 计算机视觉
基于深度学习的停车位关键点检测系统(代码+原理)
基于深度学习的停车位关键点检测系统(代码+原理)
129 0
|
6月前
|
机器学习/深度学习 PyTorch TensorFlow
[深度学习入门]Numpy基础(上)
[深度学习入门]Numpy基础(上)
|
7月前
|
机器学习/深度学习 自然语言处理 算法框架/工具
如何入门深度学习
如何入门深度学习
62 0
|
2天前
|
机器学习/深度学习 Python
【深度学习入门】- 神经网络
【深度学习入门】- 神经网络
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
从零开始学习深度学习:入门指南与实践建议
本文将引导读者进入深度学习领域的大门,从基础概念到实际应用,为初学者提供全面的学习指南和实践建议。通过系统化的学习路径规划和案例实践,帮助读者快速掌握深度学习的核心知识和技能,迈出在人工智能领域的第一步。
|
2月前
|
机器学习/深度学习 算法 算法框架/工具
基于深度学习的交通标志检测和识别(从原理到环境配置/代码运行)
基于深度学习的交通标志检测和识别(从原理到环境配置/代码运行)
198 0
|
2月前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习疆界:探索基本原理与算法,揭秘应用力量,展望未来发展与智能交互的新纪元
深度学习疆界:探索基本原理与算法,揭秘应用力量,展望未来发展与智能交互的新纪元
35 0
|
2月前
|
机器学习/深度学习 TensorFlow 算法框架/工具
深度学习入门:Python 与神经网络
深度学习是机器学习的一个分支,它涉及使用多层神经网络来处理和学习数据。在 Python 中,有许多流行的深度学习库和框架可以帮助我们轻松地构建和训练神经网络模型。在本文中,我们将介绍深度学习的基本概念,并使用 Python 中的 TensorFlow 和 Keras 库来构建一个简单的神经网络模型。
|
3月前
|
机器学习/深度学习 分布式计算 搜索推荐
深度学习入门:一篇概述深度学习的文章
深度学习入门:一篇概述深度学习的文章
|
4月前
|
机器学习/深度学习 数据采集 算法
【深度学习基础】反向传播BP算法原理详解及实战演示(附源码)
【深度学习基础】反向传播BP算法原理详解及实战演示(附源码)
76 0