目录

  • Bert/ALBert+CRF
    • 一、模型简介
      • 1、Bert:双向预训练 + 微调
        • 1.1 背景
        • 1.2 模型结构
        • 1.3 输入输出
        • 1.4 Masked Language Model
        • 1.5 Next Sentence Prediction
        • 1.6 Bert 微调
      • 2、ALBert:轻量级 Bert
        • 2.1 背景
        • 2.2 Embedding 矩阵分解
        • 2.3 层次间的参数共享
        • 2.4 句子顺序预测损失
    • 二、代码详解
      • 1、主函数 `run_ner_crf.main()`
        • 1.1 环境准备
        • 1.2 三大组件
        • 1.3 train/eval/predict
      • 2、Configuration
        • 2.1 基类 `PretrainedConfig`
        • 2.2 子类
      • 3、Tokenization
        • 3.1 基类 `PreTrainedTokenizer`
        • 3.2 孙子辈的子类 `CLUENerTokenizer`
        • 3.3 儿子辈的子类 `BertTokenizer`
      • 4、Model
        • 4.1 基类 `PreTrainModel`
        • 4.2 孙子辈子类 `BertCrfForNer`
        • 4.3 儿子辈子类 `BertPreTrainedModel`
      • 5、Train
        • 5.1 数据的加载与处理 `run_ner_crf.load_and_cache_examples`
        • 5.2 训练阶段
        • 5.3 验证阶段
        • 5.4 预测阶段
    • 三、代码运行
    • 四、测试结果
    • 参考资料:

Bert/ALBert+CRF

一、模型简介

1、Bert:双向预训练 + 微调

1.1 背景

Bert,全称是 Bidirecctional Encoder Representation from Transformers。顾名思义,主要的亮点是双向编码 + Transformer 模型。在 Bert 诞生之前,有一个 GPT 模型,它是一个标准的语言模型,即用 context 来预测下一个词,这样做有两个主要的缺点:

  • 限制了模型结构的选择,只有从左到右方向的模型才能够被选择
  • 对句子级别的任务不是最优的

因此,Bert 这样的双向网络应运而生,但双向带来的问题是损失函数的设置。GPT 的损失函数非常直观,预测下一个词正确的概率,而 Bert 则是见到了所有的词,因此采用了一种称之为 Masked Language Model 的预训练目标函数。另外,为了使模型更适用于句子级别的任务,Bert 中还采用了一种称之为 Next Serntence Prediction 的目标函数,来使得模型能更好的捕捉句子信息。

1.2 模型结构

Bert 依然是依赖于 Transformer 模型结构的,GPT 采用的 Transformer 中 Decoder 部分的模型结构,当前位置只能 attend 到之前的位置,而 Bert 中则没有这样的限制,因此它是用的 Transformer 的 Encoder 部分。

Transformer 是由一个一个的 block 组成的,其主要参数如下:

  • L L L:多少个 block
  • H H H:隐含状态尺寸,不同 block 上的隐含状态尺寸一般相等,这个尺寸单指多头注意力层的尺寸,有一个惯例就是在 Transformer Block 中全连接层的尺寸是多头注意力层的 4 倍。所以指定了 H H H 相当于把 Transformer Block 中的两层隐含状态尺寸都指定了
  • A A A:多头注意力头的个数

有了这几个参数后,就可以定义不同配置的模型了,Bert 中定义了两个模型,base 和 large,其中:

  • Base: L = 12 , H = 768 , A = 12 L=12, H=768, A=12 L=12,H=768,A=12,参数量 110M
  • Large: L = 24 , H = 1024 , A = 16 L=24, H=1024,A=16 L=24,H=1024,A=16,参数量 340M

1.3 输入输出

为了让 Bert 能够处理下游任务,Bert 的输入是两个句子,中间用分隔符隔开,在开头加一个特殊的用于分类的字符。即 Bert 的输入是 [CLS] sentence1 [SEP] sentence2

其中,两个句子对应的词语的 embedding 还要加上位置 embedding 和标明 token 属于哪个句子的 embedding。如下图所示:

[CLS] 上的输出我们认为是输入句子的编码。输入最长是 512。

1.4 Masked Language Model

一般语言模型建模的方式是从左到右或者从右到左,这样的损失函数都很直观,即预测下一个词的概率。而 Bert 这种双向的网络,使得 下一个词 的概念消失了,没有了目标,如何做训练呢?答案是 完形填空,在输入中,把一些词语遮挡住,遮挡的方法就是用 [Mask] 这个特殊的词语代替。而在预测的时候,就预测这些被遮挡住的词语,其中遮挡词语占所有词语的 15%,且是每次随机 Mask。但这有一个问题:在预训练中会 [Mask] 这个词语,但是在下游任务中,是没有这个词语的,这会导致预训练和下游任务的不匹配。

解决的办法就是不让模型意识到有这个任务的存在,具体做法就是在所有 Mask 的词语中,有 80% 的词语继续用 [Mask] 特殊词语,有 10% 用其他词语随机替换,有 10% 的概率保持不变。这样,模型就不知道当前句子中有没有 [Mask] 的词语了。

1.5 Next Sentence Prediction

在很多下游任务中,需要判断两个句子之间的关系,比如 QA 问题,需要判断一个句子是不是另一个句子的答案,比如 NLI(Natural Language Inference)问题,直接就是两个句子之间的三种关系判断。

因此为了能更好的捕捉句子之间的关系,在预训练的时候,就做了一个句子级别的损失函数,这个损失函数的目的很简单,就是判断第二个句子是不是第一个句子的下一句。训练时,会随机选择生成训练预料,50% 的时候是下一句,50% 的不是。

1.6 Bert 微调

上图是 Bert 的预训练和微调整体架构。除了输出层外,在预训练和微调阶段使用相同的体系结构,预训练的模型参数用于初始化不同下游任务的模型,在微调过程中,所有参数都会进行微调。上图右侧最上面展示的 QA 问题的设计,而 NER 的任务设计更为简单,我们只需要单个句子即可,顶层通常加上 CRF 层,如下图。

2、ALBert:轻量级 Bert

2.1 背景

近年来,预训练的趋势从学习 embedding 变成了学习整个网络,即 word2vec 到 Bert 的 变迁。另一个趋势则是大模型对最后的效果非常重要,但在真实的场景中使用,则需要小一些的模型,因而,知识蒸馏变得比较流行。

之前在卷积上,把层次加深到一定程度后如果没有结构上的创新(ResNet等)就不能再进一步提升效果。与之类似,在 Bert 上,把模型大小调整到一定程度也无法再继续使效果变好,反而会变差。例如把 hidden size 从 1024 调整到 2048 会使得效果反而变差。(这个结论虽然在论文中得到了,但是没有考虑到数据的增长,后面有大量的论文通过增加数据训练更大的模型。)

于是,ALBert 中采用了三种手段来提升效果,其中,前两个方法通过模型上的改进降低了模型的大小。

  • Embedding 矩阵分解
  • 层次间的参数共享
  • 损失函数添加了句子顺序预测的 loss

2.2 Embedding 矩阵分解

原始的 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,所以做了分解之后这一部分的参数变少了很多。

2.3 层次间的参数共享

Bert 的层次很深,至少在 10 层以上,这样,层次间的参数如果能共享的话,会使得参数量大大减少。

不仅如此,在共享了参数之后,会使得模型更加稳定,这方面的表现就是模型的每层的输入和输出之间的 L 2 L_2 L2 距离更小了,当然关于参数的共享也有很多不同的选择,如:

  • 全部共享
  • 只共享全连接层
  • 只共享多头注意力层
  • 不共享

经过一些实验对比之后,选择的是全部共享。

2.4 句子顺序预测损失

在原始的 Bert 中,除了 MLM 任务外,还有 NSP 任务,在 NSP 任务中,正例是两个连续的句子 AB,反例是两个句子 AC,其中 C 来自于另外的文章。这个任务在很多后续的工作中被证明是无效的,因此在 Albert 中,提出了一个更难的任务,即两个连续的句子 AB,判断它是正序还是逆序的,即 AB 还是 BA。


二、代码详解

整个代码除了一些基础准备工作以及模型的损失函数、参数的学习更新等内容外,主要包含三大模块:

  • Configuration 类:参照 model.transformers.configuration*,这是模型的配置类,主要负责模型的超参配置
  • Tokenization 类:参照 model.transformers.tokenization* ,这是模型的输入数据处理类,主要负责处理数据
  • Model 类:参照 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              # 运行参数设置

1、主函数 run_ner_crf.main()

主函数下主要包括如下几个小模块:环境准备、上述三大模块的初始化、训练、验证、预测,共 5 个环节的内容。下面对每个部分的具体内容进行介绍。

1.1 环境准备

args 保存的是程序运行时的一些参数,详见 tools.fineturning_argparse ,内含运行环境、数据IO、模型超参、模型优化等多个方面的参数设置,除了第一部分的必传参数外,其他参数可根据实际情况进行提供。接下来根据传递的参数初始化日志,以及运行设备的准备,随机种子设定等,并根据指定的任务,获取任务标签数。

1.2 三大组件

接下来,就是针对指定的 model_type 获取三大模块的预训练相关的初始化配置情况,三大模块从基本架构上讲是基本一致的,分别由 model.transformers.configuration_utilsmodel.transformers.tokenization_utilsmodel.transformers.model_utils 三个公共基类完成 config、tokenizer、model 的基类任务。并由具体模型下的子类进行继承扩展特定模型下的具体功能。所有的基类入口从 from_pretrained() 方法开始,意味着一个特定模型的相关内容是从加载预训练阶段的相关配置或模型参数等开始的。这部分后面再详细介绍。

1.3 train/eval/predict

见下文。


2、Configuration

该部分内容详见 model.transformers.configuration_*

2.1 基类 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() 用于配置的保存

2.2 子类

需要注意的是,网络下载的 config 文件中的配置项,都是可以在具体子类传参或者外部方法中进行配置值修改的,或者加入自己的新的配置项,未经修改的配置项则使用默认的 config 文件的配置,config 文件中的信息不要直接进行修改,如 configuration_bert.BertConfig,Albert 同理见 configuration_albert.AlbertConfig


3、Tokenization

该部分内容详见 model.transformers.tokenization_*

3.1 基类 PreTrainedTokenizer

  • from_pretrained()_from_pretrained():同 Configuration,这是 tokenizer 模块的入口,该函数负责从指定的模型 URL或本地缓存或本地指定路径下加载预训练阶段使用的词典文件

    • 如果提供的是模型简称,会根据模型名称寻找 URL 信息,这部分信息也是由具体的子类提供,如 tokenization_bert.PRETRAINED_VOCAB_FILES_MAP
    • 如果提供的是目录,会检索该目录下符合特定词典后缀的词典文件
    • 除此之外,认为参数是本地词典文件的路径,直接加载
    • 除了主要分词词典外,一些模型中可能还会存在其他特殊词典,也可以分别加载,我们目前用的模型暂不涉及
    • 接下来仍会交由 file_utils.cached_path() 去处理
    • 剩下的内容最重要的一句话是 tokenizer = cls(*init_inputs, **init_kwargs) 负责实例化,这里的 cls 就是具体的子类,此处的子类实际是 CLUENerTokenizer,参照 run_ner_crf.MODEL_CLASSES

3.2 孙子辈的子类 CLUENerTokenizer

详见 processors.utils_ner,该类实际上继承的是 BertTokenizerBertTokenizer 又继承自 3.1 的基类。具体的逻辑关系是,基类负责通用的 Tokenization 方法,如加载、保存等,BertTokenizer 负责 BERT 模型的公共 Tokenization 方法,而 CLUENerTokenizer 负责特定数据集在特定 BERT 任务下的处理方式。

经过上述各个父类的实例化后,CLUENerTokenizer 的实例参数大致如下:

  • tokenize:这里的处理方式很简单,只是检查了一下大小写转换问题并进行词典匹配,存在于词典中保留原字,否则该位置用 [UNK]替代,关于分词,基类或父类中也实现了一些复杂的分词方法,它们可以应对更为复杂多语种的处理。

3.3 儿子辈的子类 BertTokenizer

负责具体的字典加载逻辑及其他更为复杂灵活的 Tokenizer 公共库


4、Model

该部分内容详见 model.transformers.modeling_*

4.1 基类 PreTrainModel

  • from_pretrained():同 Configuration 和 Tokenization ,这是 Model 模块的入口,负责加载指定模型的配置及模型参数加载等,其基本流程也与上述两个模块一致,依然通过 file_unils.cached_path() 去下载或验证模型参数文件

    • 上述步骤完成后,最重要的一句话是 model = cls(config, *model_args, **model_kwargs),负责模型实例化,这里的 cls 指的是 BertCrfForNerAlbertCrfForNer,参照 run_ner_crf.MODEL_CLASSES

      • 进入 BertCrfForNer.__init__后,实例化的过程中内容较多,首先是super 会调用 PreTrainedModel.__init__,而后 self.bert 调用 modeling_bert.__init__实现 self.embeddingself.encoderself.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 机制

4.2 孙子辈子类 BertCrfForNer

  • 实例化部分详见基类中的实例化
  • forward()

4.3 儿子辈子类 BertPreTrainedModel

  • 实例化部分详见基类中的实例化,负责实现具体模型网络架构的整体组织结构

5、Train

5.1 数据的加载与处理 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_examplesprocessors.ner_seq.InputExample转为标准的数据格式,其中 InputExample 含有数据的三个属性 guidtext_alabels ,其中测试阶段 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
      • 序列长度为 128
      • tokens:是处理后的序列,前后分别被填充了 [CLS] 以及 [SEP]
      • input_ids:序列中每个 token 在词典中的索引位置,padding 位置用 0 填充
      • input_mask:1 表示真实的 token, 0 表示 padding
      • segment_ids:这里只有一个句子,所以除了第一个字符外,都是 0
      • label_ids:标签的索引
    • 上述最后几个特征集成到 processors.ner_sq.InputFeatures

  • 缓存上述处理后的特征数据,下次可直接加载

  • 最后将上述各个特征转为 Tensor,进一步集成为 TensorDataset,以用于数据训练/验证/预测

5.2 训练阶段

准备阶段

  • 样本采样生成器:RandomSampler,负责随机生成一个批量的数据
  • DataLoader:融合数据与采样器,一个周期数据步大致为:数据长度/数据批量大小,这里在加载后会执行 collate_fn 函数,即根据一个批量中数据的最大(如52)真实 labels 长度进行截断,实际参与训练的数据序列长度可能小于模型最大序列长度(128)
  • 实际总得计算步:一个周期数据步 / 梯度累积步(通常为1)* 周期数
  • 参数划分:参数来自三个模块:bert(各层参数共199项),CRF(开始、结束、转移,共3项),线性层(权重,偏置共2项),依据是否设有权重衰减机制,各分为 2 组,整体共区分为 6 组参数。
  • warmup 学习率机制,一开始训练不太稳定,因此从更小的学习率开始,一段时间后将学习率恢复到设定的学习率,再进行衰减,整体学习率曲线是先增后减
  • 优化器:AdamW

训练

  • 打印一些基本数据

    ***** 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() 方法中,而后分别进入到 embeddingencoderpooler 子模块及子子模块中
    • 直至 CRF 层的输出,计算出损失值
  • 接下来就是安排 loss.backward() 进行损失的反向传播

  • 梯度计算、优化器进行参数更新、调度器同步更新、梯度值赋零,以及其他参数如 global_step 的更新等

  • 迭代上述过程

5.3 验证阶段

在训练一定步数后,进入验证阶段,验证,验证阶段大部分内容与训练阶段一致,但 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 总量,最终计算 accrecallf1 指标。

5.4 预测阶段

预测阶段与验证阶段雷同,但没有真实标签做参考,所以主题部分到 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 层,所以从速度上讲,没有太大的优势。

参考资料:

  • CLUENER 细粒度命名实体识别
  • huggingface/transformers
  • 微信公众号:”雨石记“,关于 bert 和 albert 的简介

NER —— Bert/ALBert+CRF相关推荐

  1. 信息抽取实战:命名实体识别NER【ALBERT+Bi-LSTM模型 vs. ALBERT+Bi-LSTM+CRF模型】(附代码)

    实战:命名实体识别NER 目录 实战:命名实体识别NER 一.命名实体识别(NER) 二.BERT的应用 NLP基本任务 查找相似词语 提取文本中的实体 问答中的实体对齐 三.ALBERT ALBER ...

  2. bert+crf可以做NER,那么为什么还有bert+bi-lstm+crf ?

    我在自己人工标注的一份特定领域的数据集上跑过,加上bert确实会比只用固定的词向量要好一些,即使只用BERT加一个softmax层都比不用bert的bilstm+crf强.而bert+bilstm+c ...

  3. 基于BERT+BiLSTM+CRF的中文景点命名实体识别

    赵平, 孙连英, 万莹, 葛娜. 基于BERT+BiLSTM+CRF的中文景点命名实体识别. 计算机系统应用, 2020, 29(6): 169-174.http://www.c-s-a.org.cn ...

  4. Bert+BiLSTM+CRF实体抽取

    文章目录 一.环境 二.预训练词向量 三.模型 1.BiLSTM - 不使用预训练字向量 - 使用预训练字向量 2.CRF 3.BiLSTM + CRF - 不使用预训练词向量 - 使用预训练词向量 ...

  5. 跟我读论文丨ACL2021 NER BERT化隐马尔可夫模型用于多源弱监督命名实体识别

    摘要:本文是对ACL2021 NER BERT化隐马尔可夫模型用于多源弱监督命名实体识别这一论文工作进行初步解读. 本文分享自华为云社区<ACL2021 NER | BERT化隐马尔可夫模型用于 ...

  6. bert+lstm+crf ner实体识别

    https://blog.csdn.net/weixin_42357472/article/details/108010305

  7. bert+lstm+crf ner实体识别 带源码

    https://blog.csdn.net/weixin_42357472/article/details/108010305 正版手册 https://www.cnpython.com/pypi/k ...

  8. 对GCN,Transformer, XLNet, ALBERT, CRF等技术仍然一知半解?再不学习就OUT了!

    谷歌Lab近日发布了一个新的预训练模型"ALBERT"全面在SQuAD 2.0.GLUE.RACE等任务上超越了BERT.XLNet.RoBERTa再次刷新了排行榜!ALBERT是 ...

  9. 词向量, BERT, ALBERT, XLNet全面解析(ALBERT第一作者亲自讲解)

    Datawhale Datawhale编辑 现在是国家的非常时期,由于疫情各地陆续延迟复工,以及各大院校延期开学.作为一家 AI 教育领域的创业公司,贪心学院筹划了5期NLP专题直播课程,希望在这个非 ...

最新文章

  1. opencv python 官方文档里的“sa”关键字是什么意思?(see also)
  2. DSP 投放的基本流程和算法
  3. C#生成新浪微博短网址 示例源码
  4. array专题3-一道题目不断分析就会慢慢有了思路
  5. mysql 语句账号注入_mysql中SQL语句的注入问题
  6. mysql查看sql代价_mysql 代价
  7. 如何查看电脑是几核几线程
  8. 【Proteus仿真】矩阵键盘中断扫描
  9. android刷机教程 华为,华为手机刷机教程(华为手机强制刷机步骤图文教程)
  10. 排序算法:编程算法助程序员走上高手之路
  11. MP3比特率编码模式
  12. 3000字神经网络论文
  13. 大数据/云计算 行业报告
  14. Treap树应用-bzoj 1862 GameZ游戏排名系统问题
  15. 还想野蛮生长?互联网金融有《意见》了
  16. (赴日流程)家属滞在签证
  17. 计算多项式的值编程c语言,Newton插值多项式计算函数的近似值
  18. JAVA中native方法调用C语言实现学习
  19. 基于GMapping的栅格地图的构建
  20. 11g新增加的后台进程

热门文章

  1. 净水厂自动化、信息化解决方案
  2. 示波器1m和50欧姆示阻抗匹配_为什么示波器阻抗偏偏是1M和50欧?
  3. 湖北省计算机无纸化考试试题,湖北自考计算机无纸化考试试题.pdf
  4. 知物由学 | Lua脚本保护的前世今生
  5. 奥浦迈科创板过会:毛利率高,实控人肖志华、贺芸芬持美国绿卡
  6. iOS 微信 第三方登录实现
  7. matlab中矩阵SVD分解
  8. 存文件的服务器叫什么区别,文件服务器区别
  9. 6款AI绘画生成器,让你的创作更有灵感
  10. SPSS Statistics 26.0 for Mac/Win 最强大的统计分析最新版下载安装 使用教程