文章目录
Bert 论文概述
bert 是 Pre-training of Deep Bidirectional Transformers for Language Understanding 的缩写,18年10月份Google出的神作,主要思想是:
- 建立双向 Transformer encoder 模型,前期通过大量语料,进行语言模型的训练,得到 pre-trained 的 word embedding
- 随后采用相同的 Transform 模型,进行下游任务(语句分类、语义分析、问答、命名实体识别)的 fine-tune,此时模型需要训练的参数只有 fine-tune 部分的参数,参数数据很少,从而大大减少了下游任务的训练时间;同时刷新了 11 项 NLP 任务的SOTA。
关于BERT论文的解读,此处推荐一篇写的很好的博客,我也从中受益很多。
此文源码解析已整理于 Github – bert source code understand ,包含了全部 bert 代码,可以单步调试进行学习,基本代码跳转不会很乱,不存在函数跳转至其他文件夹下的情况;也可以直接在 repo 中进行源码阅读。
Bert 模型结构
从名字种就可以看出来,BERT 模型的结构事双向 transformer 结构,至于 transformer 就是 Google 的的另一篇论文了:Attention is all you need ,这里不再叙述。
原论文中 bert 结构如下图,:
采用了双向的transformer,最下面一行是经过 embedding 之后的模型输入,把语句变为了词向量;随后12层的 transformer 结构,最后输出模型对语言的理解,即 T1, T2… 用来做下游的语言任务。
bert 子结构 transformer 结构体:
图片左边是 transformer 的 encoder 结构,右边是 decoder ,transformer 这个模型是用来做机器翻译的,使用的是 S2S 模型,所以又 encoder 和 decoder 结构,即先将要翻译的句子进行编码,得到句子语义的编码,随后更具编码结果再进行解码。 BERT 模型只使用了 encoder 结构,所以 bert 的 transfomer 是只有 multi-head attention、LayerNorm、FeedForward 及残差快链接而成。
使用 netron 工具,进行 mxnet 模型的可视化显示,结果如下:
总体结构
总体结构是12层的 tansformer encoder 结构,因为全局结构图太大,这里这截取一部分,只有两层:
attention 结构
截取了一层 encoder 结构,从中标出了 attention 的各个部分:
finetune classifier 结构
Bert 模型源码解析
由于代码太多,这里把类中、函数中不重要的代码进行省略,使用三行 … 表示有代码省略。
preprocess_data
train_data, dev_data, num_train_examples = preprocess_data(
bert_tokenizer, task, batch_size, dev_batch_size, args.max_len)
data preprocess 是用来生成训练数据集和测试数据集的,处理结果可以查看本节下面的 process result。
def preprocess_data(tokenizer, task, batch_size, dev_batch_size, max_len):
"""Data preparation function."""
# transformation
trans = BERTDatasetTransform(
tokenizer,
max_len,
labels=task.get_labels(),
pad=False,
pair=task.is_pair,
label_dtype='float32' if not task.get_labels() else 'int32')
if task.task_name == 'MNLI':
data_train = task('dev_matched').transform(trans, lazy=False)
data_dev = task('dev_mismatched').transform(trans, lazy=False)
else:
data_train = task('train').transform(trans, lazy=False)
data_dev = task('dev').transform(trans, lazy=False)
data_train_len = data_train.transform(
lambda input_id, length, segment_id, label_id: length)
num_samples_train = len(data_train)
# bucket sampler
batchify_fn = nlp.data.batchify.Tuple(
nlp.data.batchify.Pad(axis=0), nlp.data.batchify.Stack(),
nlp.data.batchify.Pad(axis=0),
nlp.data.batchify.Stack(
'float32' if not task.get_labels() else 'int32'))
batch_sampler = nlp.data.sampler.FixedBucketSampler(
data_train_len,
batch_size=batch_size,
num_buckets=10,
ratio=0,
shuffle=True)
# data loaders
dataloader = gluon.data.DataLoader(
dataset=data_train,
num_workers=1,
batch_sampler=batch_sampler,
batchify_fn=batchify_fn)
dataloader_dev = mx.gluon.data.DataLoader(
data_dev,
batch_size=dev_batch_size,
num_workers=1,
shuffle=False,
batchify_fn=batchify_fn)
return dataloader, dataloader_dev, num_samples_train
基本的逻辑是:
先通过 tokenize 进行分词并生成 tokens --> 通过 BERTDatasetTransform 生成对应 task 需要的数据 --> 进行不同的 bucket 分装 --> train or inference
tokenize
class BERTTokenizer(object):
r"""End-to-end tokenization for BERT models.
Parameters
----------
vocab : gluonnlp.Vocab or None, default None
Vocabulary for the corpus.
lower : bool, default True
whether the text strips accents and convert to lower case.
If you use the BERT pre-training model,
lower is set to Flase when using the cased model,
otherwise it is set to True.
max_input_chars_per_word : int, default 200
Examples
--------
>>> _,vocab = gluonnlp.model.bert_12_768_12(dataset_name='wiki_multilingual',pretrained=False)
>>> tokenizer = gluonnlp.data.BERTTokenizer(vocab=vocab)
>>> tokenizer(u"gluonnlp: 使NLP变得简单。")
['gl', '##uo', '##nn', '##lp', ':', '使', 'nl', '##p', '变', '得', '简', '单', '。']
"""
def __init__(self, vocab, lower=True, max_input_chars_per_word=200):
self.vocab = vocab
self.max_input_chars_per_word = max_input_chars_per_word
self.basic_tokenizer = BERTBasicTokenizer(lower=lower)
def __call__(self, sample):
"""
Parameters
----------
sample: str (unicode for Python 2)
The string to tokenize. Must be unicode.
Returns
-------
ret : list of strs
List of tokens
"""
return self._tokenizer(sample)
def _tokenizer(self, text):
split_tokens = []
for token in self.basic_tokenizer(text):
for sub_token in self._tokenize_wordpiece(token):
split_tokens.append(sub_token)
return split_tokens
def _tokenize_wordpiece(self, text):
"""Tokenizes a piece of text into its word pieces.
This uses a greedy longest-match-first algorithm to perform tokenization
using the given vocabulary.
For example:
input = "unaffable"
output = ["un", "##aff", "##able"]
Args:
text: A single token or whitespace separated tokens. This should have
already been passed through `BERTBasicTokenizer.
Returns:
A list of wordpiece tokens.
"""
...
...
...
def convert_tokens_to_ids(self, tokens):
"""Converts a sequence of tokens into ids using the vocab."""
return self.vocab.to_indices(tokens)
这里BERT分词的代码,从代码注释中可以看到分词的结果如下:
>>> tokenizer = gluonnlp.data.BERTTokenizer(vocab=vocab)
>>> tokenizer(u"gluonnlp: 使NLP变得简单。")
['gl', '##uo', '##nn', '##lp', ':', '使', 'nl', '##p', '变', '得', '简', '单', '。']
代码中调用的是 self._tokenizer(sample) ,把一句话先进行语句级的分割,得到词汇;随后在进行词汇级的分割,得到 tokens ,此时采用的是贪心算法:首次最大长度匹配(感觉以后可以改进一下,贪心算法在这里肯定不是最优的)。
具体做语句级分割的时候,采用的是 BERTBasicTokenizer 分割器,代码如下:
class BERTBasicTokenizer():
r"""Runs basic tokenization
performs invalid character removal (e.g. control chars) and whitespace.
tokenize CJK chars.
splits punctuation on a piece of text.
strips accents and convert to lower case.(If lower is true)
Parameters
----------
lower : bool, default True
whether the text strips accents and convert to lower case.
Examples
--------
>>> tokenizer = gluonnlp.data.BERTBasicTokenizer(lower=True)
>>> tokenizer(u" \tHeLLo!how \n Are yoU? ")
['hello', '!', 'how', 'are', 'you', '?']
>>> tokenizer = gluonnlp.data.BERTBasicTokenizer(lower=False)
>>> tokenizer(u" \tHeLLo!how \n Are yoU? ")
['HeLLo', '!', 'how', 'Are', 'yoU', '?']
"""
def __init__(self, lower=True):
self.lower = lower
def __call__(self, sample):
"""
Parameters
----------
sample: str (unicode for Python 2)
The string to tokenize. Must be unicode.
Returns
-------
ret : list of strs
List of tokens
"""
return