论文地址
写在前面
由于官网给的是TensorFlow版本的,github也有pytorch版本,但是给出的pytorch的代码是需要根据实际情况进行修改的。
词表文件vocab.txt文件读取的问题
vocab.py代码的class WordVocab(Vocab)类中的def load_vocab(vocab_path)函数为:
def load_vocab(vocab_path):
with open(vocab_path, 'rb') as f:
return pickle.load(f)
因为在HuggingFace上下载的词表文件是vocab.txt,这里读取的话会有报错:
_pickle.UnpicklingError: invalid load key, '['
所以需要进行如下修改
@staticmethod
def convert_to_unicode(text):
if isinstance(text, str):
return text
elif isinstance(text, bytes):
return text.decode("uft-8", "ignore")
else:
raise ValueError("不支持的数据类型")
@staticmethod
def load_vocab(vocab):
vocab = collections.OrderedDict()
index = 0
with open(vocab_path, 'rb') as f:
while True:
token = conver_to_unicode(f.readline())
if not token:
break
token = token.strip()
vocab[token] = index
index += 1
return WordVocab(vocab)
这里用递归返回,把HuggingFace的词表文件读取到WordVocab中,通过WordVocab.stoi调用,这里的词表顺序不再是vocab.txt中的顺序,会有些许差别。
数据准备
1. vocab.txt
HuggingFace的上下载的词表文件,形如下图:
然后再vocab.py中转换成字典,其中的顺序可能会有些许不同,可以通过如下代码调用这个转换后的字典
vocab = WordVocab("vocab.txt")
print(vocab.stoi)
输出如下:
2. training_dataset
数据处理部分在dataset.py中,实际上的读取为:
class BERTDataset(Dataset):
def __init__(self, corpus_path, corpus_lines, encoding="uft-8", ...):
with open(corpus_path, "r", encoding=encoding) as f:
self.lines = [line[:-1].split("\t") for line in tqdm.tqdm(f, desc="Loading Dataset", total=corpus_lines)]
def get_corpus_line(self, item):
return self.lines[item][0], self.lines[item][1]
上面代码中的 def get_corpus_line就是实际上读取代码,这里的item就是DataLoader循环的次数,固定参数,不用管,需要注意的是后面的索引值,这也就意味着:
- 自己的数据必须要有且有两个维度,才能保证self.lines[item][0], self.lines[item][1]
- 如果自己数据有多个维度,一定要考虑问答在哪个维度上
举例子解释:
- 逐维度读取的文本为:“龟甲缚如何打绳结?\t”
- 去掉首位的操作符: “龟甲缚如何打绳结?”
- 在vocab.stoi查询排序值,把[“龟”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”]转换成[20993, 17510, 18361, 14965, 13865, 15804, 18336, 18312]
如何称之为Masked Language Model?
说起来就是一个很简单的事情,就是把读取的文本,进行遮罩!
具体方法为:
def random_word(self, sentence):
tokens = sentence.split()
output_label = []
for i, token in enumerate(tokens):
prob = random.random()
if prob < 0.15:
# 有15%的概率执行这里
prob /= 0.15
# 80% randomly change token to mask token
# 15%的执行概率中,有80%的概率执行这里,也就是12%的概率执行这里
if prob < 0.8:
# 这里才是核心,就是12%的概率执行这里
# 也就是说,有12%的词会被遮罩(mask),这里就是把词的代号换成了self.vocab.mask_index(就是4)
tokens[i] = self.vocab.mask_index
# 10% randomly change token to random token
# 15%的执行概率中,有10%的概率执行这里,也就是1.5%的概率执行这里
elif prob < 0.9:
# 也就是有1.5%的词被随机替换成任意值,可能是起始符、终止符 、遮罩符、任意文字等
tokens[i] = random.randrange(len(self.vocab))
# 10% randomly change token to current token
# 15%的执行概率中,有10%的概率执行这里,也就是1.5%的概率执行这里
else:
# 也就是有1.5%的概率随机抹除这个文字,设置为未知, unknown
tokens[i] = self.vocab.stoi.get(token, self.vocab.unk_index)
output_label.append(self.vocab.stoi.get(token, self.vocab.unk_index))
else:
# 有85%的概率执行这里
# 在vocab.py中把句子中的词转换成了词和排序值对应的字典
# 这里把token对应的排序值抽出来,如果没有对应的键(词语),那么就返回self.vocab.unk_index
tokens[i] = self.vocab.stoi.get(token, self.vocab.unk_index)
output_label.append(0)
具体效果就是:
- 输入文本为:[“龟”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”] ===> [20993, 17510, 18361, 14965, 13865, 15804, 18336, 18312]
- 12 % 12\% 12%的词汇会被替换成mask: [xxx, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”] [4, 17510, 18361, 14965, 13865, 15804, 18336, 18312]
- 1.5 % 1.5\% 1.5%的词汇会被替换成一个词表中的随机词:[“@”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”] ===> [21059, 17510, 18361, 14965, 13865, 15804, 18336, 18312]
- 1.5 % 1.5\% 1.5%的词汇会被随机替换成一个未知(未知有自己的索引)[unknown, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”] ===> [1, 17510, 18361, 14965, 13865, 15804, 18336, 18312]
- 在这句话上添加开始和结束符,[sos, “龟”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”, eos] ===> [3, 20993, 17510, 18361, 14965, 13865, 15804, 18336, 18312, 2]
- pad_index = 0
- unk_index = 1
- eos_index = 2
- sos_index = 3
- mask_index = 4
这里有另一个问题了,文档不似图片,尺寸是固定的,这里如何保证NLP的输入是具有固定尺寸的呢?
答案很简单,就是截断和padding,就是太长了我就给他剪短,太短了我就给他补长, 最终得到一个固定长度为seq_len的数字列表。
- seq_len=16
- pad_index = 0
[sos, “龟”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”, eos] ===> [3, 20993, 17510, 18361, 14965, 13865, 15804, 18336, 18312, 2, 0, 0, 0, 0, 0, 0]
于是,我们就得到了可以直接输入嵌入层的数据[3, 20993, 17510, 18361, 14965, 13865, 15804, 18336, 18312, 2, 0, 0, 0, 0, 0, 0],其意思就是[sos, “龟”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”, eos]
如何嵌入?
self.token = TokenEmbedding(vocab_size=vocab_size, embed_size=embed_size)
self.position = PositionalEmbedding(d_model=self.token.embedding_dim)
self.segment = SegmentEmbedding(embed_size=self.token.embedding_dim)
x = self.token(sequence) + self.position(sequence) + self.segment(segment_label)
第一个,TokenEmbedding作用就是把 [ b a t c h , v o c a b _ s i z e ] [batch, vocab\_size] [batch,vocab_size]的向量,嵌入成 [ b a t c h , v o c a b _ s i z e , e m b e d _ s i z e ] [batch, vocab\_size, embed\_size] [batch,vocab_size,embed_size]的嵌入向量,例如:
[“龟”, “甲”, “缚”, “如”, “何”, “打”, “绳”, “结”,“?”] ===> [3, 20993, 17510, 18361, 14965, 13865, 15804, 18336, 18312, 2, 0, 0, 0, 0, 0, 0] ===> [ b a t c h = 1 , 16 , e m b e d _ s i z e = 512 ] [batch=1, \quad 16, \quad embed\_size=512] [batch=1,16,embed_size=512]
意思就是把上面那16个向量,每个都用512维度的向量进行表示,构成 b a t c h × 16 × 512 batch \times16\times512 batch×16×512 的向量
第二个,PositionalEmbedding位置向量,可以参考
Transformer之位置编码的通俗理解
Bert代码结构?
Bert的核心在上面的红色标题:如何称之为Masked Language Model?,具体的网络结构和代码结构没有难点,就一个12层Transformer,毫无画图的必要