一起读Bert文本分类代码 (pytorch篇 三)

一起读Bert文本分类代码 (pytorch篇 三)

Bert是去年google发布的新模型,打破了11项纪录,关于模型基础部分就不在这篇文章里多说了。这次想和大家一起读的是huggingface的pytorch-pretrained-BERT代码examples里的文本分类任务run_classifier。

关于源代码可以在huggingface的github中找到。

huggingface/pytorch-pretrained-BERTgithub.com图标


在前两篇文章中我分别介绍了数据预处理部分和部分的模型

周剑:一起读Bert文本分类代码 (pytorch篇 一)zhuanlan.zhihu.com图标周剑:一起读Bert文本分类代码 (pytorch篇 二)zhuanlan.zhihu.com图标


接上一篇文章,我会和大家一起继续读模型部分。

我整理了BertForSequenceClassification类中调用关系,如下图所示。本篇文章中,我会和大家一起读BertModel这段代码中调用的BertEmbeddings,BertEncoder和 BertPooler类。


打开pytorch_pretrained_bert.modeling.py,找到BertEmbeddings类,代码如下:

class BertEmbeddings(nn.Module):
    """Construct the embeddings from word, position and token_type embeddings.
    """
    def __init__(self, config):
        super(BertEmbeddings, self).__init__()
        self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size)
        self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
        self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
        self.LayerNorm = BertLayerNorm(config.hidden_size, eps=1e-12)
        self.dropout = nn.Dropout(config.hidden_dropout_prob)

    def forward(self, input_ids, token_type_ids=None):
        seq_length = input_ids.size(1)
        position_ids = torch.arange(seq_length, dtype=torch.long, device=input_ids.device)
        position_ids = position_ids.unsqueeze(0).expand_as(input_ids)
        if token_type_ids is None:
            token_type_ids = torch.zeros_like(input_ids)

        words_embeddings = self.word_embeddings(input_ids)
        position_embeddings = self.position_embeddings(position_ids)
        token_type_embeddings = self.token_type_embeddings(token_type_ids)

        embeddings = words_embeddings + position_embeddings + token_type_embeddings
        embeddings = self.LayerNorm(embeddings)
        embeddings = self.dropout(embeddings)
        return embeddings

从Bert的论文中我们可以知道,Bert的词向量主要是由三个向量相加组合而成,他们分别是单词本身的向量,单词所在句子中位置的向量和句子所在单个训练文本中位置的向量。这样做的好处主要可以解决只有词向量时碰见多义词时模型预测不准的问题。

我们从forward函数开始看,先有一个torch.arange函数。查询pytorch官方文档发现torch.arange函数是用来生成一个tensor类型数组的。

torch.arange(seq_length, dtype=torch.long, device=input_ids.device)

例如seq_length=256,生成的就是每个类型为torch.long的tensor([0.000,1.000,...,255.000])的一串向量


下一句position_ids.unsqueeze(0).expand_as(input_ids)中unsqueeze(0)的意思是将原来的一维的position_ids向量变为二维的矩阵。经过这一步后如果seq_length=256,position_ids.unsqueeze(0)的shape是tensor([256, 0])。

expand_as(input_ids)的意思是将这个新增的纬度的长度变为input_ids的长度。例如每个单词的词向量长度(input_ids=100),则这里position_ids.unsqueeze(0).expand_as(input_ids)的shape变为tensor([256, 100])。即,每个位置的单词拥有100维的位置向量。


然后是通过词典查询每个单词的词向量(words_embeddings)和设立句子位置向量(token_type_embeddings)。如果每个训练数据只有一句话,则这里句子位置向量会变成全部为0的向量。

三个向量相加后,进行一个归一化和dropout处理后就会最为input输入BertEncoder模型中。


BertEncoder类的代码如下:

class BertEncoder(nn.Module):
    def __init__(self, config):
        super(BertEncoder, self).__init__()
        layer = BertLayer(config)
        self.layer = nn.ModuleList([copy.deepcopy(layer) for _ in range(config.num_hidden_layers)])

    def forward(self, hidden_states, attention_mask, output_all_encoded_layers=True):
        all_encoder_layers = []
        for layer_module in self.layer:
            hidden_states = layer_module(hidden_states, attention_mask)
            if output_all_encoded_layers:
                all_encoder_layers.append(hidden_states)
        if not output_all_encoded_layers:
            all_encoder_layers.append(hidden_states)
        return all_encoder_layers

这段代码的大概意思是将原有的BertLayer一层一层剥开,如果想要输出每层的状态(output_all_encoded_layers=True),则把每层的状态添加到all_encoder_layers中。

如果不想输出每层的状态(output_all_encoded_layers=False),则只把最后一层的状态添加到all_encoder_layers中。

nn.ModuleList函数的意思是把一整个模型按层分开,变成一个list。查询pytorch官方文档,ModuleList函数如下:

因此,我们看到BertEncoder类,其实只是用来输出BertLayer类的状态的一个函数。真正模型内部的东西在BertLayer类中。这篇文章不会讲到BertLayer类,具体的BertLayer类会在下一篇文章中和大家一起阅读。


我们继续看BertPooler类,代码如下:

class BertPooler(nn.Module):
    def __init__(self, config):
        super(BertPooler, self).__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.activation = nn.Tanh()

    def forward(self, hidden_states):
        # We "pool" the model by simply taking the hidden state corresponding
        # to the first token.
        first_token_tensor = hidden_states[:, 0]
        pooled_output = self.dense(first_token_tensor)
        pooled_output = self.activation(pooled_output)
        return pooled_output

发现这是一个Linear线形层加一个Tanh()的激活函数,用来池化BertEncoder的输出。


这篇文章我和大家一起读了BertModel这段代码中调用的BertEmbeddings,BertEncoder和 BertPooler类。而在下一篇文章中,我会和大家一起读BertEncoder类中调用的BertLayer。

周剑:一起读Bert文本分类代码 (pytorch篇 四)zhuanlan.zhihu.com图标周剑:一起读Bert文本分类代码 (pytorch篇 五)zhuanlan.zhihu.com图标周剑:一起读Bert文本分类代码 (pytorch篇 六)zhuanlan.zhihu.com图标

编辑于 2019-02-09

文章被以下专栏收录