本文主要是记录学习bert的pytorch实现代码的一些心得
dataset
1. vocab
继承关系:TorchVocab --> Vocab --> WordVocab
- TorchVocab
该类主要是定义了一个词典对象,包含如下三个属性:
freqs
:是一个collections.Counter
对象,能够记录数据集中不同token所出现的次数
stoi
:是一个collections.defaultdict
对象,将数据集中的token映射到不同的index
itos
:是一个列表,保存了从index到token的映射信息
代码中sort与sorted的区别:
sort 是应用在 list 上的方法,sorted 可以对所有可迭代的对象进行排序操作。
list 的 sort 方法返回的是对已经存在的列表进行操作,而内建函数 sorted 方法返回的是一个新的 list,而不是在原来的基础上进行的操作。
sorted的语法:
sorted(iterable, key=None, reverse=False)
iterable -- 可迭代对象。
key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
reverse -- 排序规则,reverse = True 降序 , reverse = False 升序(默认)。
- Vocab
Vocab继承TorchVocab,该类主要定义了一些特殊token的表示
这里用到了一个装饰器,简单地说:装饰器就是修改其他函数的功能的函数。这里包含了一个序列化的操作
- WordVocab
WordVocab继承自Vocab,里面包含了两个方法to_seq
和from_seq
分别是将token转换成index和将index转换成token表示
2. dataset
主要实现了一个BERTDataset
类,继承自torch.utils.data.Dataset
,里面一些操作都是根据论文中的两种训练模型构建的:masked words 和 next sentence predict
model
1. attention
这里首先定义了一个单注意力的类,然后通过该类叠加构造一个多头注意力的类。
矩阵相乘有torch.mm和torch.matmul两个函数。其中前一个是针对二维矩阵,后一个是高维。当torch.mm用于大于二维时将报错。
masked_fill
:a.masked_fill(mask == 0, value) mask必须是一个 ByteTensor 而且shape必须和 a一样 并且元素只能是 0或者1 ,是将 mask中为1的 元素所在的索引,在a中相同的的索引处替换为 value ,mask value必须同为tensor
2. embedding
bert的embedding由三个部分组成:TokenEmbedding、PositionalEmbedding、SegmentEmbedding
pytorch中词嵌入使用nn.Embedding
,只需要调用 torch.nn.Embedding(m, n) 就可以了,m 表示单词的总数目,n 表示词嵌入的维度,其实词嵌入就相当于是一个大矩阵,矩阵的每一行表示一个单词,默认是随机初始化的,不过也可以使用已经训练好的词向量。不理解的话可以自己用简单的例子尝试一下即可
这里的位置编码是写死的,不会随着训练进行更新,这是论文 attention is all you need 中的做法,而bert模型的官方TensorFlow源码是将位置编码position embedding直接初始化成可训练的参数并通过训练进行学习的,所以此处的实现和bert模型不同
import torch.nn as nn
import torch
import mathclass PositionalEmbedding(nn.Module):def __init__(self, d_model, max_len=512):super().__init__()# Compute the positional encodings once in log space.pe = torch.zeros(max_len, d_model).float()pe.require_grad = Falseposition = 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)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)]
代码中对positionembedding的实现一开始看有点懵,感觉和原文中的表达式不太一样,后来查找资料发现是对原公式做了如下变换:
$$ 1/10000^{2i/d_{model}} = e^{log{10000^{-2i/d_{model}}}} = e^{-2i/d_{model}log{10000}} = e^{2i(-log^{10000}/d_{model})} $$
这样一来便和代码中的实现一致了,为什么要这么做,代码注释中说这样可以使用log空间一次性计算position embedding,即能够节省一定的内存开销
3. utils
这部分主要是实现了模型中的一些组件:FNN,gelu激励函数,layernorm正则化以及sublayer连接,理论部分论文中都介绍得很详细,可以结合论文原文学习这部分的实现应该比较好理解
bert语言模型构建
接下来就是bert模型的具体实现了:
首先是构造transformer block。然后组成bert模型
这里要复习一下repeat()方法:torch.Tensor有两个实例方法可以用来扩展某维的数据的尺寸,分别是repeat()和expand():
expand()方法返回当前张量在某维扩展更大后的张量。扩展(expand)张量不会分配新的内存,只是在存在的张量上创建一个新的视图(view),一个大小(size)等于1的维度扩展到更大的尺寸。
repeat()方法沿着特定的维度重复这个张量,和expand()不同的是,这个函数拷贝张量的数据,而不是在原来的张量上修改。
trainer
主要设置了optim以及封装了训练迭代的操作