赞
踩
Bert,全称是 Bidirecctional Encoder Representation from Transformers。顾名思义,主要的亮点是双向编码 + Transformer 模型。在 Bert 诞生之前,有一个 GPT 模型,它是一个标准的语言模型,即用 context 来预测下一个词,这样做有两个主要的缺点:
因此,Bert 这样的双向网络应运而生,但双向带来的问题是损失函数的设置。GPT 的损失函数非常直观,预测下一个词正确的概率,而 Bert 则是见到了所有的词,因此采用了一种称之为 Masked Language Model 的预训练目标函数。另外,为了使模型更适用于句子级别的任务,Bert 中还采用了一种称之为 Next Serntence Prediction 的目标函数,来使得模型能更好的捕捉句子信息。
Bert 依然是依赖于 Transformer 模型结构的,GPT 采用的 Transformer 中 Decoder 部分的模型结构,当前位置只能 attend 到之前的位置,而 Bert 中则没有这样的限制,因此它是用的 Transformer 的 Encoder 部分。
Transformer 是由一个一个的 block 组成的,其主要参数如下:
有了这几个参数后,就可以定义不同配置的模型了,Bert 中定义了两个模型,base 和 large,其中:
为了让 Bert 能够处理下游任务,Bert 的输入是两个句子,中间用分隔符隔开,在开头加一个特殊的用于分类的字符。即 Bert 的输入是 [CLS] sentence1 [SEP] sentence2
。
其中,两个句子对应的词语的 embedding 还要加上位置 embedding 和标明 token 属于哪个句子的 embedding。如下图所示:
在 [CLS]
上的输出我们认为是输入句子的编码。输入最长是 512。
一般语言模型建模的方式是从左到右或者从右到左,这样的损失函数都很直观,即预测下一个词的概率。而 Bert 这种双向的网络,使得 下一个词
的概念消失了,没有了目标,如何做训练呢?答案是 完形填空
,在输入中,把一些词语遮挡住,遮挡的方法就是用 [Mask]
这个特殊的词语代替。而在预测的时候,就预测这些被遮挡住的词语,其中遮挡词语占所有词语的 15%,且是每次随机 Mask。但这有一个问题:在预训练中会 [Mask]
这个词语,但是在下游任务中,是没有这个词语的,这会导致预训练和下游任务的不匹配。
解决的办法就是不让模型意识到有这个任务的存在,具体做法就是在所有 Mask 的词语中,有 80% 的词语继续用 [Mask]
特殊词语,有 10% 用其他词语随机替换,有 10% 的概率保持不变。这样,模型就不知道当前句子中有没有 [Mask]
的词语了。
在很多下游任务中,需要判断两个句子之间的关系,比如 QA 问题,需要判断一个句子是不是另一个句子的答案,比如 NLI(Natural Language Inference)问题,直接就是两个句子之间的三种关系判断。
因此为了能更好的捕捉句子之间的关系,在预训练的时候,就做了一个句子级别的损失函数,这个损失函数的目的很简单,就是判断第二个句子是不是第一个句子的下一句。训练时,会随机选择生成训练预料,50% 的时候是下一句,50% 的不是。
上图是 Bert 的预训练和微调整体架构。除了输出层外,在预训练和微调阶段使用相同的体系结构,预训练的模型参数用于初始化不同下游任务的模型,在微调过程中,所有参数都会进行微调。上图右侧最上面展示的 QA 问题的设计,而 NER 的任务设计更为简单,我们只需要单个句子即可,顶层通常加上 CRF 层,如下图。
近年来,预训练的趋势从学习 embedding 变成了学习整个网络,即 word2vec 到 Bert 的 变迁。另一个趋势则是大模型对最后的效果非常重要,但在真实的场景中使用,则需要小一些的模型,因而,知识蒸馏变得比较流行。
之前在卷积上,把层次加深到一定程度后如果没有结构上的创新(ResNet等)就不能再进一步提升效果。与之类似,在 Bert 上,把模型大小调整到一定程度也无法再继续使效果变好,反而会变差。例如把 hidden size 从 1024 调整到 2048 会使得效果反而变差。(这个结论虽然在论文中得到了,但是没有考虑到数据的增长,后面有大量的论文通过增加数据训练更大的模型。)
于是,ALBert 中采用了三种手段来提升效果,其中,前两个方法通过模型上的改进降低了模型的大小。
原始的 Bert,因为需要做残差,所以每层的 Hidden size 都是一样的,同时也使得 Embedding size 和 hidden size 一样。但是因为词表比较大,Bert 中用的是 3w 的 word piece,这样会导致模型的参数中,embedding 会占一大部分。
从模型的角度来说,Embedding 的参数学习是上下文无关的表达,而 Bert 的模型结构则学习的是上下文相关的表达,在之前的研究成果中表明,Bert 及其类似的模型的好效果更多的来源于上下文信息的捕捉,因而,embedding 占据这么多的参数,会导致最终的参数非常稀疏。
因而,我们不用这么大的 Embedding,而是用一个较小的,但是这样你就没法在第一层去做残差了。没有关系,我们可以用另一个参数矩阵来把 embedding 的大小投射到 hidden 的大小上,即,假设 V 是词表大小,E 是 embedding 的大小,H 是 hidden 的大小,那么原始的 BERT 中, E = H E=H E=H,参数数目是 V × H V \times H V×H,而做了分解之后参数数据为 V × E + E × H V\times E+E\times H V×E+E×H,因为 E E E 远 小于 H H H,所以做了分解之后这一部分的参数变少了很多。
Bert 的层次很深,至少在 10 层以上,这样,层次间的参数如果能共享的话,会使得参数量大大减少。
不仅如此,在共享了参数之后,会使得模型更加稳定,这方面的表现就是模型的每层的输入和输出之间的 L 2 L_2 L2 距离更小了,当然关于参数的共享也有很多不同的选择,如:
经过一些实验对比之后,选择的是全部共享。
在原始的 Bert 中,除了 MLM 任务外,还有 NSP 任务,在 NSP 任务中,正例是两个连续的句子 AB,反例是两个句子 AC,其中 C 来自于另外的文章。这个任务在很多后续的工作中被证明是无效的,因此在 Albert 中,提出了一个更难的任务,即两个连续的句子 AB,判断它是正序还是逆序的,即 AB 还是 BA。
整个代码除了一些基础准备工作以及模型的损失函数、参数的学习更新等内容外,主要包含三大模块:
model.transformers.configuration*
,这是模型的配置类,主要负责模型的超参配置model.transformers.tokenization*
,这是模型的输入数据处理类,主要负责处理数据model.transformers.modeling*
,这是模型的架构设计类,负责搭建模型网络层项目组织架构:
./ ├── __init__.py ├── callback │ ├── __init__.py │ ├── lr_scheduler.py │ └── progressbar.py ├── datasets │ └── cluener │ ├── README.md │ ├── cluener_predict.json │ ├── dev.json │ ├── test.json │ ├── train.json │ └── vocab.pkl ├── losses │ ├── __init__.py │ ├── focal_loss.py │ └── label_smoothing.py ├── metrics │ ├── __init__.py │ └── ner_metrics.py ├── models │ ├── __init__.py │ ├── albert_for_ner.py │ ├── bert_for_ner.py │ ├── layers │ │ ├── __init__.py │ │ ├── crf.py │ │ └── linears.py │ └── transformers │ ├── __init__.py │ ├── configuration_albert.py # albert config 子类 │ ├── configuration_bert.py # bert config 子类 │ ├── configuration_utils.py # config 基类 │ ├── file_utils.py # 具体的文件下载、缓存以及文件校验等公共方法 │ ├── modeling_albert.py │ ├── modeling_bert.py │ ├── modeling_utils.py # model 基类 │ ├── tokenization_albert.py │ ├── tokenization_bert.py │ └── tokenization_utils.py # tokenizer 基类 ├── outputs │ └── cluener_output │ ├── albert │ └── bert ├── prev_trained_model │ ├── albert_base_zh │ │ ├── config.json │ │ ├── pytorch_model.bin │ │ └── vocab.txt │ └── bert-base-chinese │ ├── config.json │ ├── pytorch_model.bin │ └── vocab.txt ├── processors │ ├── __init__.py │ ├── ner_seq.py │ └── utils_ner.py ├── run_ner_crf.py ├── scripts │ └── run_ner_crf.sh └── tools ├── __init__.py ├── common.py # 提供一些公共基础函数,如日志设置,随机种子设置等 └── finetuning_argparse.py # 运行参数设置
run_ner_crf.main()
主函数下主要包括如下几个小模块:环境准备、上述三大模块的初始化、训练、验证、预测,共 5 个环节的内容。下面对每个部分的具体内容进行介绍。
args
保存的是程序运行时的一些参数,详见 tools.fineturning_argparse
,内含运行环境、数据IO、模型超参、模型优化等多个方面的参数设置,除了第一部分的必传参数外,其他参数可根据实际情况进行提供。接下来根据传递的参数初始化日志,以及运行设备的准备,随机种子设定等,并根据指定的任务,获取任务标签数。
接下来,就是针对指定的 model_type
获取三大模块的预训练相关的初始化配置情况,三大模块从基本架构上讲是基本一致的,分别由 model.transformers.configuration_utils
、model.transformers.tokenization_utils
和 model.transformers.model_utils
三个公共基类完成 config、tokenizer、model 的基类任务。并由具体模型下的子类进行继承扩展特定模型下的具体功能。所有的基类入口从 from_pretrained()
方法开始,意味着一个特定模型的相关内容是从加载预训练阶段的相关配置或模型参数等开始的。这部分后面再详细介绍。
见下文。
该部分内容详见 model.transformers.configuration_*
PretrainedConfig
from_pretrained()
:这是 config 模块的入口,该函数负责从指定的模型 URL或本地缓存或本地指定目录下加载特定模型在预训练阶段使用的一些配置信息(注意,预训练模型可从网络中下载,下载的文件中至少含有预训练模型的 config 文件、词典表文件、模型参数权重文件等,如果下载的是 TF 版本,也是有办法转为 PyTorch 格式的,此处默认下载 PyTorch 版本的,因此不提供具体的模型转换脚本)
如果提供的是模型简称,会根据模型名称寻找 URL 信息,URL 信息由特定的模型子类提供,如 configuration_bert.BERT_PRETRAINED_CONFIG_ARCHIVE_MAP
如果提供的是一个目录,会寻找该目录下符合特定 config 后缀的配置文件
除此之外会把参数作为 config 文件的绝对目录进行加载
接下来会由 file_utils.cached_path()
进行检查,如 URL 配置文件的下载或本地配置文件确认,最后返回可用的路径位置或者异常信息(无法下载或本地指定的参数无效等)
当配置文件可用时,进行加载
from_json_file()
、from_dict()
负责具体的模型加载,其中后者 config = cls(vocab_size_or_config_json_file=-1)
负责实例化,这里的 cls
指的是 BertConfig
,全部初始化后的配置信息大致如下:
{ "architectures": [ "BertForMaskedLM" ], "attention_probs_dropout_prob": 0.1, "directionality": "bidi", "finetuning_task": null, "hidden_act": "gelu", "hidden_dropout_prob": 0.1, "hidden_size": 768, "initializer_range": 0.02, "intermediate_size": 3072, "layer_norm_eps": 1e-12, "max_position_embeddings": 512, "model_type": "bert", "num_attention_heads": 12, "num_hidden_layers": 12, "num_labels": 34, "output_attentions": false, "output_hidden_states": false, "output_past": true, "pad_token_id": 0, "pooler_fc_size": 768, "pooler_num_attention_heads": 12, "pooler_num_fc_layers": 3, "pooler_size_per_head": 128, "pooler_type": "first_token_transform", "torchscript": false, "type_vocab_size": 2, "use_bfloat16": false, "vocab_size": 21128 }
to_*
和 save_pretrained()
用于配置的保存
需要注意的是,网络下载的 config 文件中的配置项,都是可以在具体子类传参或者外部方法中进行配置值修改的,或者加入自己的新的配置项,未经修改的配置项则使用默认的 config 文件的配置,config 文件中的信息不要直接进行修改,如 configuration_bert.BertConfig
,Albert 同理见 configuration_albert.AlbertConfig
。
该部分内容详见 model.transformers.tokenization_*
PreTrainedTokenizer
from_pretrained()
、_from_pretrained()
:同 Configuration,这是 tokenizer 模块的入口,该函数负责从指定的模型 URL或本地缓存或本地指定路径下加载预训练阶段使用的词典文件
tokenization_bert.PRETRAINED_VOCAB_FILES_MAP
file_utils.cached_path()
去处理tokenizer = cls(*init_inputs, **init_kwargs)
负责实例化,这里的 cls
就是具体的子类,此处的子类实际是 CLUENerTokenizer
,参照 run_ner_crf.MODEL_CLASSES
CLUENerTokenizer
详见 processors.utils_ner
,该类实际上继承的是 BertTokenizer
,BertTokenizer
又继承自 3.1 的基类。具体的逻辑关系是,基类负责通用的 Tokenization 方法,如加载、保存等,BertTokenizer
负责 BERT 模型的公共 Tokenization 方法,而 CLUENerTokenizer
负责特定数据集在特定 BERT 任务下的处理方式。
经过上述各个父类的实例化后,CLUENerTokenizer
的实例参数大致如下:
[UNK]
替代,关于分词,基类或父类中也实现了一些复杂的分词方法,它们可以应对更为复杂多语种的处理。BertTokenizer
负责具体的字典加载逻辑及其他更为复杂灵活的 Tokenizer 公共库
该部分内容详见 model.transformers.modeling_*
PreTrainModel
from_pretrained()
:同 Configuration 和 Tokenization ,这是 Model 模块的入口,负责加载指定模型的配置及模型参数加载等,其基本流程也与上述两个模块一致,依然通过 file_unils.cached_path()
去下载或验证模型参数文件
上述步骤完成后,最重要的一句话是 model = cls(config, *model_args, **model_kwargs)
,负责模型实例化,这里的 cls
指的是 BertCrfForNer
或AlbertCrfForNer
,参照 run_ner_crf.MODEL_CLASSES
进入 BertCrfForNer.__init__
后,实例化的过程中内容较多,首先是super
会调用 PreTrainedModel.__init__
,而后 self.bert
调用 modeling_bert.__init__
实现 self.embedding
、self.encoder
、self.pooler
三个大的模型架构初始化,至此展开了整个 bert 模型的架构。
以 Bert 模型为例,上述初始化过程实际完成了如下架构的初始化
bertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(21128, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0): BertLayer(....................... ....................................... (11): BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ) ) # 中间省略部分为完全一致的 12 层
最后回到 BertCrfForNer.__init__
,在 BERT 顶层添加线性层及 CRF 层后,最后执行 init_weight
初始化所有网络层的权重
state_dict
从下载的模型参数文件中加载权重信息,然后更新上述模型的参数,需要注意的是这个过程中可能存在 PyTorch 新旧版本中模型参数命名不一致的问题,需要做相关转换,经过替换后,会进行参数检测,例如未被预训练参数替换的模型参数以及未使用的参数
Weights of BertCrfForNer not initialized from pretrained model: ['classifier.weight', 'classifier.bias', 'crf.start_transitions', 'crf.end_transitions', 'crf.transitions']
11/12/2020 10:08:53 - INFO - models.transformers.modeling_utils - Weights from pretrained model not used in BertCrfForNer: ['cls.predictions.bias', 'cls.predictions.transform.dense.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.decoder.weight', 'cls.seq_relationship.weight', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.LayerNorm.bias']
可以看到未被替换的主要为顶层分类器和 CRF 层,这部分是我们自定义添加的,需要被训练,其次未使用的也是原顶层分类器信息。
model.eval()
主要是让模型在验证阶段取消 dropout 机制
BertCrfForNer
forward()
BertPreTrainedModel
run_ner_crf.load_and_cache_examples
当不存在缓存时,需要从头创建,已存在的话直接加载即可,下面重点介绍数据构建步骤:
首先通过 processors.ner_seq.CluenerProcessor.get_labels()
获取所有的标签信息
通过 processors.ner_seq.DataProcessor._read_json()
加载 cluener 数据集并进行标注,数据集示例如下:
{
"text": "浙商银行企业信贷部叶老桂博士则从另一个角度对五道门槛进行了解读。叶老桂认为,对目前国内商业银行而言,",
"label":
{
"name":
{
"叶老桂": [[9, 11]]
},
"company":
{
"浙商银行": [[0, 3]]
}
}
}
数据的标注采用的是 “BIOS” 标注法:
* B- begin,实体的开头位置
* I- insed,实体内部的内容
* S- single,单个字的情况
* O other,其他无效词
上述数据经过标注后转为:
words = ['浙', '商', '银', '行', '企', '业', '信', '贷', '部', '叶', '老', '桂', '博', '士', '则', '从', '另', '一', '个', '角', '度', '对', '五', '道', '门', '槛', '进', '行', '了', '解', '读', '。', '叶', '老', '桂', '认', '为', ',', '对', '目', '前', '国', '内', '商', '业', '银', '行', '而', '言', ',']
labels = ['B-company', 'I-company', 'I-company', 'I-company', 'O', 'O', 'O', 'O', 'O', 'B-name', 'I-name', 'I-name', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']
接下来经过 processors.ner_seq.CluenerProcessor._create_examples
和processors.ner_seq.InputExample
转为标准的数据格式,其中 InputExample
含有数据的三个属性 guid
、text_a
、labels
,其中测试阶段 labels
是不提供的,另外由于 NER 任务无需两个句子序列,因此 text_b
此处并未体现出来。
接下来需要将 words 以及 labels 序列转为输入向量,由 processors.ner_seq.convert_examples_to_features()
处理
首先词典确认,就是调用的 processors.utils_ner.CLUENERTOkenizer.tokenize()
方法完成的,实际上这里完成的工作仅仅是检查该汉字是否存在于字典中,不存在的字符用 [UNK]
替代
label_ids
是将标签序列转为标签的索引序列
接下来所有的事情都是将数据序列转为 bert 的输入格式,我们采用的是下列 (b) 格式,这个过程中需要注意句子特殊字符的填充以及过长句子的截断以及长度不足的句子进行填充等
# (a) 对于句子对
# tokens: [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
# type_ids: 0 0 0 0 0 0 0 0 1 1 1 1 1 1
# (b) 对于单个句子序列
# tokens: [CLS] the dog is hairy . [SEP]
# type_ids: 0 0 0 0 0 0 0
处理后的样例如下:
*** Example ***
guid: train-0
tokens: [CLS] 浙 商 银 行 企 业 信 贷 部 叶 老 桂 博 士 则 从 另 一 个 角 度 对 五 道 门 槛 进 行 了 解 读 。 叶 老 桂 认 为 , 对 目 前 国 内 商 业 银 行 而 言 , [SEP]
input_ids: 101 3851 1555 7213 6121 821 689 928 6587 6956 1383 5439 3424 1300 1894 1156 794 1369 671 702 6235 2428 2190 758 6887 7305 3546 6822 6121 749 6237 6438 511 1383 5439 3424 6371 711 8024 2190 4680 1184 1744 1079 1555 689 7213 6121 5445 6241 8024 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
input_mask: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
segment_ids: 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
label_ids: 31 3 13 13 13 31 31 31 31 31 7 17 17 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
上述最后几个特征集成到 processors.ner_sq.InputFeatures
缓存上述处理后的特征数据,下次可直接加载
最后将上述各个特征转为 Tensor,进一步集成为 TensorDataset,以用于数据训练/验证/预测
准备阶段
RandomSampler
,负责随机生成一个批量的数据DataLoader
:融合数据与采样器,一个周期数据步大致为:数据长度/数据批量大小,这里在加载后会执行 collate_fn
函数,即根据一个批量中数据的最大(如52)真实 labels 长度进行截断,实际参与训练的数据序列长度可能小于模型最大序列长度(128)训练
打印一些基本数据
***** Running training *****
Num examples = 10748
Num Epochs = 4
Instantaneous batch size per GPU = 32
Total train batch size (w. parallel, distributed & accumulation) = 32
Gradient Accumulation steps = 1
Total optimization steps = 1344
检查是否存在之前的 checkpoint,如果有加载它继续训练,需要注意这里 glocal_step
需要恢复到上一次训练的状态
model.train()
将模型设置为训练状态,含 dropout
接下来将数据推送到指定的设备中,如 GPU(原来数据是存储于 CPU 中)
inputs
,因此实际的输入数据为:
input_ids
: 对应原数据中的 input_ids
attention_mask
: 对应原数据中的 input_mask
token_type_ids
:对应原数据中的 segment_ids
labels
: 对应原数据中的 label_ids
input_lens
:每条数据的真实长度 + 2{'input_ids': tensor([[ 101, 1353, 3633, ..., 0, 0, 0], [ 101, 3683, 6612, ..., 6566, 511, 102], [ 101, 2199, 7213, ..., 0, 0, 0], ..., [ 101, 151, 147, ..., 0, 0, 0], [ 101, 149, 151, ..., 0, 0, 0], [ 101, 3173, 1872, ..., 0, 0, 0]], device='cuda:0'), # [32, 52] 'attention_mask': tensor([[1, 1, 1, ..., 0, 0, 0], [1, 1, 1, ..., 1, 1, 1], [1, 1, 1, ..., 0, 0, 0], ..., [1, 1, 1, ..., 0, 0, 0], [1, 1, 1, ..., 0, 0, 0], [1, 1, 1, ..., 0, 0, 0]], device='cuda:0'), # [32, 52] 'labels': tensor([[31, 31, 31, ..., 0, 0, 0], [31, 31, 31, ..., 31, 31, 31], [31, 31, 31, ..., 0, 0, 0], ..., [31, 31, 31, ..., 0, 0, 0], [31, 7, 17, ..., 0, 0, 0], [31, 31, 31, ..., 0, 0, 0]], device='cuda:0'), # [32, 52] 'input_lens': tensor([38, 52, 48, 14, 34, 40, 47, 33, 47, 43, 21, 37, 44, 38, 49, 32, 45, 44, 36, 36, 47, 40, 37, 48, 44, 40, 49, 49, 49, 32, 42, 30], device='cuda:0'), # (32, ) 'token_type_ids': tensor([[1, 0, 0, ..., 0, 0, 0], [1, 0, 0, ..., 0, 0, 0], [1, 0, 0, ..., 0, 0, 0], ..., [1, 0, 0, ..., 0, 0, 0], [1, 0, 0, ..., 0, 0, 0], [1, 0, 0, ..., 0, 0, 0]], device='cuda:0')} # [32, 52]
根据上述内容可以查看到:首先数据按照一个批量下最大序列长度进行对齐截断,此处是 52 维,其次所有的数据目前所处设备均为 cuda:0
,证明数据确实在 GPU 上。
outputs = model(**inputs)
正式开启耗时较长的训练过程,会将数据喂给网格,逐层开启 forward()
前馈计算模式,直至网络最后一层,输出结果,
models.bert_for_ner.BertCrfForNer.forward()
方法中,该方法的第一条语句将进入到 bert 模型中计算models.transformers.modeling_bert.BertModel.forward()
方法中,而后分别进入到 embedding
、encoder
、pooler
子模块及子子模块中接下来就是安排 loss.backward()
进行损失的反向传播
梯度计算、优化器进行参数更新、调度器同步更新、梯度值赋零,以及其他参数如 global_step
的更新等
迭代上述过程
在训练一定步数后,进入验证阶段,验证,验证阶段大部分内容与训练阶段一致,但 CRF 层需要利用维特比算法解码预测序列,接下来需要在 label
(真实标签序列)和tags
(预测的标签序列) 之间进行指标计算
首先提出一个批量中序列真实实体信息及预测的序列中实体信息,具体的提取逻辑见 processors.utils_ner.get_entity_bios()
,例如 ”B-“必须接同类型的 “I-”,”I-“ 不能出现在实体开始位置,凡不符合规范的均排除在外
founds = [['name', 0, 2], ['address', 15, 16], ['name', 0, 1], ['organization', 23, 24], ['organization', 27, 28], ['movie', 0, 7], ['name', 9, 15], ['name', 28, 32], ...]
origins = [['name', 0, 2], ['address', 15, 16], ['name', 0, 1], ['organization', 23, 24], ['game', 0, 7], ['name', 9, 15], ['name', 28, 32], ['address', 6, 8], ['scene', 4, 5],..]
rights = [['name', 0, 2], ['address', 15, 16], ['name', 0, 1], ['organization', 23, 24], ['name', 9, 15], ['name', 28, 32], ['address', 6, 8], ['scene', 43, 45], ['book', 0, 5], ['name', 15, 17], ['organization', 16, 18], ['company', 1, 3], ['position', 31, 32]...]
例如,founds 是预测的,共找到了 78 个实体信息,origins 是真实的,共 70 个实体信息,rights 是预测正确的,所谓的正确是指实体名字及边界完全正确,共 59 个(这是模型训练中的第一次验证结果)
而后针对每一种实体,分别计算真实总量以及预测总量,并计算各自实体的 rights 总量,最终计算 acc
、recall
、f1
指标。
预测阶段与验证阶段雷同,但没有真实标签做参考,所以主题部分到 CRF 解码基本就结束了。
通过运行下面的脚本即可
$ ./scripts/run_ner_crf.sh
如果使用 GPU 环境,python 运行命令前面加 CUDA_VISIBLE_DEVICES=1
,代码简化后目前不支持单机多卡或多机分布式训练。默认使用的是 bert 模型,如需切换 albert 模型,需要修改 --model_name_or_path=$ALBERT_BASE_DIR
和 --model_type=albert
,目前只支持 bert 和 albert 家族模型。
bert-base-chinese:
11/12/2020 19:13:46 - INFO - root - ***** Eval results ***** 11/12/2020 19:13:46 - INFO - root - acc: 0.7863 - recall: 0.8073 - f1: 0.7967 - loss: 6.0941 11/12/2020 19:13:46 - INFO - root - ***** Entity results ***** 11/12/2020 19:13:46 - INFO - root - ******* address results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.6521 - recall: 0.6783 - f1: 0.6649 11/12/2020 19:13:46 - INFO - root - ******* book results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.8267 - recall: 0.8052 - f1: 0.8158 11/12/2020 19:13:46 - INFO - root - ******* company results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.7872 - recall: 0.8122 - f1: 0.7995 11/12/2020 19:13:46 - INFO - root - ******* game results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.8176 - recall: 0.8814 - f1: 0.8483 11/12/2020 19:13:46 - INFO - root - ******* government results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.8045 - recall: 0.8664 - f1: 0.8343 11/12/2020 19:13:46 - INFO - root - ******* movie results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.8252 - recall: 0.7815 - f1: 0.8027 11/12/2020 19:13:46 - INFO - root - ******* name results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.8565 - recall: 0.8860 - f1: 0.8710 11/12/2020 19:13:46 - INFO - root - ******* organization results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.7763 - recall: 0.8038 - f1: 0.7898 11/12/2020 19:13:46 - INFO - root - ******* position results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.7977 - recall: 0.8106 - f1: 0.8041 11/12/2020 19:13:46 - INFO - root - ******* scene results ******** 11/12/2020 19:13:46 - INFO - root - acc: 0.7374 - recall: 0.6986 - f1: 0.7174 (torch38) yrobot@gpu_251:~/dfsj/program/pytorch_bert_crf$
albert_base_zh:
11/12/2020 19:17:46 - INFO - root - ***** Eval results ***** 11/12/2020 19:17:46 - INFO - root - acc: 0.7758 - recall: 0.7646 - f1: 0.7702 - loss: 6.4552 11/12/2020 19:17:46 - INFO - root - ***** Entity results ***** 11/12/2020 19:17:46 - INFO - root - ******* address results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.6324 - recall: 0.6273 - f1: 0.6299 11/12/2020 19:17:46 - INFO - root - ******* book results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.8108 - recall: 0.7792 - f1: 0.7947 11/12/2020 19:17:46 - INFO - root - ******* company results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.7676 - recall: 0.7778 - f1: 0.7727 11/12/2020 19:17:46 - INFO - root - ******* game results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.8328 - recall: 0.8271 - f1: 0.8299 11/12/2020 19:17:46 - INFO - root - ******* government results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.7901 - recall: 0.8381 - f1: 0.8134 11/12/2020 19:17:46 - INFO - root - ******* movie results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.8000 - recall: 0.7682 - f1: 0.7838 11/12/2020 19:17:46 - INFO - root - ******* name results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.8512 - recall: 0.8731 - f1: 0.8620 11/12/2020 19:17:46 - INFO - root - ******* organization results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.7458 - recall: 0.7193 - f1: 0.7323 11/12/2020 19:17:46 - INFO - root - ******* position results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.7961 - recall: 0.7483 - f1: 0.7714 11/12/2020 19:17:46 - INFO - root - ******* scene results ******** 11/12/2020 19:17:46 - INFO - root - acc: 0.7407 - recall: 0.6699 - f1: 0.7035
可以看到,albert 的结果略低于 bert,但是 albert 的模型大小相对于 bert 小的多,从所有文件整体上讲,差不多是 10 倍的关系,单从模型参数文件(pytorch_model.bin)上讲是 9.5 倍的关系,如下所示:
$ ls -alSh 总用量 117M -rw-rw-r-- 1 yrobot yrobot 77M 11月 12 19:17 optimizer.pt -rw-rw-r-- 1 yrobot yrobot 41M 11月 12 19:17 pytorch_model.bin -rw-rw-r-- 1 yrobot yrobot 107K 11月 12 19:17 vocab.txt drwxrwxr-x 2 yrobot yrobot 4.0K 11月 11 16:57 . drwxrwxr-x 5 yrobot yrobot 4.0K 11月 11 16:57 .. -rw-rw-r-- 1 yrobot yrobot 2.5K 11月 12 19:17 training_args.bin -rw-rw-r-- 1 yrobot yrobot 713 11月 12 19:17 config.json -rw-rw-r-- 1 yrobot yrobot 687 11月 12 19:17 scheduler.pt $ ls -alSh 总用量 1.2G -rw-rw-r-- 1 yrobot yrobot 777M 11月 12 19:13 optimizer.pt -rw-rw-r-- 1 yrobot yrobot 391M 11月 12 19:13 pytorch_model.bin -rw-rw-r-- 1 yrobot yrobot 107K 11月 12 19:13 vocab.txt drwxrwxr-x 2 yrobot yrobot 4.0K 11月 11 17:03 . drwxrwxr-x 5 yrobot yrobot 4.0K 11月 11 17:03 .. -rw-rw-r-- 1 yrobot yrobot 2.5K 11月 12 19:13 training_args.bin -rw-rw-r-- 1 yrobot yrobot 806 11月 12 19:13 config.json -rw-rw-r-- 1 yrobot yrobot 687 11月 12 19:13 scheduler.pt
以牺牲略低的性能损失(在某些情况下甚至 albert 会好于 bert)为代价,换来的是近 10 倍的体积压缩,albert 是 bert 小型化过程中的一个经典代表作。但是需要的注意的是,albert 仅仅是参数共享,所以导致原来 12 层的参数现在是 1层,所以小了近 10 倍,但是计算量相差不多,仍然需要计算 12 层,所以从速度上讲,没有太大的优势。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。