看不懂我吃屎系列——BERT

Posted by 高庆东 on July 4, 2019

网络结构

先吹一波,这个网络牛逼就牛逼再虽然移除了循环神经网络但依然可以处理时序数据,牛逼。根本在于
注意力机制,这个机制确实是玄学,就是让每个输入的词都和其他词有一个关系,然后训练这个关系
牛逼这这了,这个关系也很好理解,就是所有词计算一个概率向量,然后原始的数据乘这个向量。 说白了这个网络结构就是个全连接网络的升级,本质上依然是全连接。简化点说,一个数据输入到全
连接网络中,然后获得一个向量,这个向量再做下一步操作,分类了,解码了等等。
这个网络主要有两部分构成,第一是映射编码,第二是Transformer 网络结构也很简单

import torch.nn as nn
from .transformer import TransformerBlock
from .embedding import BERTEmbedding
class BERT(nn.Module):
    def __init__(self, vocab_size, hidden=768, n_layers=12, attn_heads=12, dropout=0.1):
        super().__init__()
        self.hidden = hidden
        self.n_layers = n_layers
        self.attn_heads = attn_heads
        self.feed_forward_hidden = hidden * 4
        self.embedding = BERTEmbedding(vocab_size=vocab_size, embed_size=hidden)
        self.transformer_blocks = nn.ModuleList(
            [TransformerBlock(hidden, attn_heads, hidden * 4, dropout) for _ in range(n_layers)])
    def forward(self, x, segment_info):
        # attention masking for padded token
        # torch.ByteTensor([batch_size, 1, seq_len, seq_len)
        mask = (x > 0).unsqueeze(1).repeat(1, x.size(1), 1).unsqueeze(1)
        # embedding the indexed sequence to sequence of vectors
        x = self.embedding(x, segment_info)
        # running over multiple transformer blocks
        for transformer in self.transformer_blocks:
            x = transformer.forward(x, mask)
        return x

映射编码

这个部分就是把索引的数据映射成向量,
先说一下输入的数据,以NLP为例,现在有个词典,这个词典包含了一些词,假如有一千个词, 有一条数据这条数据是一句话,这句话可以用词表示出来,这句话中的每个词都可以在词典中
找到,重点来了,这句话可以表示成这句话中每个词在词典中的位置,
比如词典是这个{‘我’:0,’你’:1,’是’:2,’傻’:3,’逼’:4},有句话“你是傻逼” 这句话
可以表示成一个列表[1,2,3,4] 这就是我们的输入数据,需要待映射的数据,

class BERTEmbedding(nn.Module):

    def __init__(self, vocab_size, embed_size, dropout=0.1):
           #vocab_size 表示词典大小,embed_size表示我们想要的映射空间,
        super().__init__()
        self.token = nn.Embedding(vocab_size=vocab_size, embed_size=embed_size) #普通embedding 
        self.position = PositionalEmbedding(d_model=self.token.embedding_dim)
        self.segment = SegmentEmbedding(embed_size=self.token.embedding_dim)
        self.dropout = nn.Dropout(p=dropout)
        self.embed_size = embed_size

    def forward(self, sequence, segment_label):
        x = self.token(sequence) + self.position(sequence) + self.segment(segment_label)
        return self.dropout(x)

一共有三种映射,第一种映射就是普通映射,首先构建一个随机矩阵,每个词都对应印个向量
这个向量的维度我们自己定义,这个矩阵的行数是词的个数,会随着训练二不断更新矩阵
第二种映射是位置映射,我要明确一句话中每个字的相对位置,这个位置信息可以表示成一种类似
RNN的前后关系, 词出现的先后顺序不同,位置关系也不同,设一个最大长度,这个长度表示
一句话中最多包含的词的数量,假如一句话有5个词,[1,2,3,4,5] 词1 和词5必须有位置关系
如何定义这中位置关系呢,首先词的位置关系于词本身无关,所以位置映射是不需要训练的
它的本质其实是 先找到每个词的索引,然后处理一下这个索引。

class PositionalEmbedding(nn.Module):

    def __init__(self, d_model=768, max_len=512):
        super().__init__()

        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model).float()
        pe.require_grad = False#不需要训练

        position = torch.arange(0, max_len).float().unsqueeze(1)
        div_term = (torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)).exp()

        pe[:, 0::2] = torch.sin(position * div_term) #这个矩阵中每个词的位置从他们的排序中获得。最长是512个词
        pe[:, 1::2] = torch.cos(position * div_term)

        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)

    def forward(self, x):
        return self.pe[:, :x.size(1)] #返回前几个词的位置关系

第三种映射就是模式映射,比如问答你说的话是问还是答,需要输入这个映射这个映射就是一种模式映射,
帮助向量表达的句子更深刻。实现也非常简单

class SegmentEmbedding(nn.Embedding):
    def __init__(self, embed_size=512):
        super().__init__(3, embed_size, padding_idx=0)

Transformer

这块是他的核心,通过注意力机制将词和词关系连接起来代替RNN,在代码上解释这块

import torch.nn as nn

from .attention import MultiHeadedAttention
from .utils import SublayerConnection, PositionwiseFeedForward


class TransformerBlock(nn.Module):

    def __init__(self, hidden, attn_heads, feed_forward_hidden, dropout):

        super().__init__()
        self.attention = MultiHeadedAttention(h=attn_heads, d_model=hidden) #经过一个注意力网路
        self.feed_forward = PositionwiseFeedForward(d_model=hidden, d_ff=feed_forward_hidden, dropout=dropout)#经过一个全连接
        self.input_sublayer = SublayerConnection(size=hidden, dropout=dropout) #标准化
        self.output_sublayer = SublayerConnection(size=hidden, dropout=dropout)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x, mask):
        x = self.input_sublayer(x, lambda _x: self.attention.forward(_x, _x, _x, mask=mask))
        x = self.output_sublayer(x, self.feed_forward)
        return self.dropout(x)

注意力机制也很好理解,就是让每个词都和其他词有个概率关系,比如在机器翻译中一个词的翻译结果主要依赖
这个词很少依赖其他词。直接看代码

import torch.nn as nn
from .single import Attention


class MultiHeadedAttention(nn.Module):

    def __init__(self, h, d_model, dropout=0.1):
        super().__init__()
        assert d_model % h == 0
        self.d_k = d_model // h
        self.h = h

        self.linear_layers = nn.ModuleList([nn.Linear(d_model, d_model) for _ in range(3)]) #生成依赖矩阵
        self.output_linear = nn.Linear(d_model, d_model)
        self.attention = Attention() #计算每个词对应其他联系大小,这个词和其他词的关系,

        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        batch_size = query.size(0)

        query, key, value = [l(x).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)
                             for l, x in zip(self.linear_layers, (query, key, value))]

        # 2) Apply attention on all the projected vectors in batch.
        x, attn = self.attention(query, key, value, mask=mask, dropout=self.dropout) #相当于一个先验的结果传给输出,只不过这个先验是训练得到的

        # 3) "Concat" using a view and apply a final linear.
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.h * self.d_k)

        return self.output_linear(x)

Attention

import torch.nn as nn
import torch.nn.functional as F
import torch

import math


class Attention(nn.Module):

    def forward(self, query, key, value, mask=None, dropout=None):
        scores = torch.matmul(query, key.transpose(-2, -1)) \
                 / math.sqrt(query.size(-1))

        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